From 6cbf59bc1fbc5d31a45d37e58fcf5c57aba18057 Mon Sep 17 00:00:00 2001 From: Kavalar Date: Fri, 20 Mar 2026 19:22:34 +0300 Subject: [PATCH] SEO helper --- seo_helpers.py | 320 +++++++++++++++++++++++++++++++++++++++++++++++++ server.py | 144 ++++++++++++++++++++-- 2 files changed, 454 insertions(+), 10 deletions(-) create mode 100644 seo_helpers.py diff --git a/seo_helpers.py b/seo_helpers.py new file mode 100644 index 0000000..278d171 --- /dev/null +++ b/seo_helpers.py @@ -0,0 +1,320 @@ +# seo_helpers.py +import html +import json +import re # Добавляем импорт re +from typing import Dict, List, Any, Optional + + +def generate_resume_seo_tags(resume_data: Dict[str, Any], resume_id: int) -> Dict[str, str]: + """ + Генерация SEO-тегов для страницы резюме + + Args: + resume_data: данные резюме из базы + resume_id: ID резюме + + Returns: + Dict с SEO-тегами + """ + # Декодируем и экранируем данные + full_name = html.escape(resume_data.get("full_name", "") or "") + name_parts = full_name.split(' ') + first_name = name_parts[0] if name_parts else '' + last_name = ' '.join(name_parts[1:]) if len(name_parts) > 1 else '' + position = html.escape(resume_data.get("desired_position", "Специалист") or "Специалист") + salary = html.escape(resume_data.get("desired_salary", "Зарплата не указана") or "Зарплата не указана") + about = html.escape( + resume_data.get("about_me", "Профессиональный опыт и навыки") or "Профессиональный опыт и навыки") + + # Формируем описание + experience_count = len(resume_data.get("work_experience", [])) + tags = resume_data.get("tags", []) + skills_list = ', '.join(tags) if tags else '' + short_about = about[:157] + '...' if len(about) > 160 else about + + seo_description = f"{full_name} - {position}. {salary}. Опыт работы: {experience_count} мест. Навыки: {skills_list}. {short_about}" + seo_description = seo_description[:320] + + # Формируем ключевые слова + keywords = f"{full_name}, {position}, резюме, поиск сотрудников, навыки: {skills_list[:200]}" + + # Формируем структурированные данные + work_experience_json = [] + for exp in resume_data.get("work_experience", []): + period = exp.get("period", "") + period_parts = period.split('–') if period else [] + work_experience_json.append({ + "@type": "OrganizationRole", + "roleName": exp.get("position", ""), + "startDate": period_parts[0] if len(period_parts) > 0 else None, + "endDate": period_parts[1] if len(period_parts) > 1 else None, + "organization": {"@type": "Organization", "name": exp.get("company", "")} + }) + + education_json = [] + for edu in resume_data.get("education", []): + education_json.append({ + "@type": "EducationalOccupationalCredential", + "credentialCategory": "Degree", + "name": edu.get("specialty", ""), + "educationalLevel": edu.get("institution", ""), + "dateCreated": edu.get("graduation_year", "") + }) + + structured_data = { + "@context": "https://schema.org", + "@type": "Person", + "name": full_name, + "jobTitle": position, + "description": resume_data.get("about_me", ""), + "worksFor": work_experience_json, + "alumniOf": education_json, + "knowsAbout": tags, + "url": f"https://yarmarka.rabota.today/resume/{resume_id}" + } + + return { + "title": f"{full_name} - {position} | Rabota.Today", + "description": seo_description, + "keywords": keywords, + "og_title": f"{full_name} - {position}", + "og_description": seo_description[:300], + "og_url": f"https://yarmarka.rabota.today/resume/{resume_id}", + "profile_first_name": first_name, + "profile_last_name": last_name, + "twitter_title": f"{full_name} - {position}", + "twitter_description": seo_description[:300], + "canonical_url": f"https://yarmarka.rabota.today/resume/{resume_id}", + "structured_data": json.dumps(structured_data, ensure_ascii=False, indent=2) + } + + +def generate_vacancy_seo_tags(vacancy_data: Dict[str, Any], vacancy_id: int) -> Dict[str, str]: + """ + Генерация SEO-тегов для страницы вакансии + + Args: + vacancy_data: данные вакансии из базы + vacancy_id: ID вакансии + + Returns: + Dict с SEO-тегами + """ + # Декодируем и экранируем данные + title = html.escape(vacancy_data.get("title", "") or "") + company = html.escape(vacancy_data.get("company_name", "Компания") or "Компания") + salary = html.escape(vacancy_data.get("salary", "Зарплата не указана") or "Зарплата не указана") + description = html.escape( + vacancy_data.get("description", "Подробная информация о вакансии") or "Подробная информация о вакансии") + + # Формируем описание + tags = vacancy_data.get("tags", []) + tags_str = ', '.join(tags) if tags else '' + short_description = description[:157] + '...' if len(description) > 160 else description + + seo_description = f"{title} в компании {company}. {salary}. {short_description}" + seo_description = seo_description[:320] + + # Формируем ключевые слова + keywords = f"{title}, {company}, вакансия, работа, {tags_str}" + + # Формируем структурированные данные для вакансии + salary_value = 0 + if salary: + # Используем re для поиска чисел + salary_match = re.search(r'(\d+)', salary) + if salary_match: + salary_value = int(salary_match.group(1)) + + structured_data = { + "@context": "https://schema.org", + "@type": "JobPosting", + "title": title, + "description": description, + "datePosted": vacancy_data.get("created_at"), + "validThrough": vacancy_data.get("valid_through"), + "employmentType": "FULL_TIME", + "hiringOrganization": { + "@type": "Organization", + "name": company, + "sameAs": vacancy_data.get("company_website", ""), + "logo": vacancy_data.get("company_logo", "https://yarmarka.rabota.today/static/images/logo.png") + }, + "jobLocation": { + "@type": "Place", + "address": { + "@type": "PostalAddress", + "addressLocality": vacancy_data.get("company_address", "Москва"), + "addressCountry": "RU" + } + }, + "baseSalary": { + "@type": "MonetaryAmount", + "currency": "RUB", + "value": { + "@type": "QuantitativeValue", + "value": salary_value, + "unitText": "MONTH" + } + }, + "workHours": "Полный день" + } + + return { + "title": f"{title} в {company} | Rabota.Today", + "description": seo_description, + "keywords": keywords, + "og_title": f"{title} в {company}", + "og_description": seo_description[:300], + "og_url": f"https://yarmarka.rabota.today/vacancy/{vacancy_id}", + "twitter_title": f"{title} в {company}", + "twitter_description": seo_description[:300], + "canonical_url": f"https://yarmarka.rabota.today/vacancy/{vacancy_id}", + "structured_data": json.dumps(structured_data, ensure_ascii=False, indent=2) + } + + +def inject_seo_tags(html_template: str, seo_tags: Dict[str, str]) -> str: + """ + Внедрение SEO-тегов в HTML шаблон + + Args: + html_template: исходный HTML + seo_tags: словарь с SEO-тегами + + Returns: + HTML с замененными SEO-тегами + """ + replacements = { + 'Резюме | Rabota.Today': f'{seo_tags.get("title", "Rabota.Today")}', + 'Вакансия | Rabota.Today': f'{seo_tags.get("title", "Rabota.Today")}', + '', desc_start) + if desc_end != -1: + result = result[ + :desc_start] + f'' + result[ + desc_end + 2:] + + if 'keywords' in seo_tags: + keywords_pattern = '', keywords_start) + if keywords_end != -1: + result = result[:keywords_start] + f'' + result[ + keywords_end + 2:] + + if 'og_title' in seo_tags: + og_title_pattern = '', og_title_start) + if og_title_end != -1: + result = result[ + :og_title_start] + f'' + result[ + og_title_end + 2:] + + if 'og_description' in seo_tags: + og_desc_pattern = '', og_desc_start) + if og_desc_end != -1: + result = result[ + :og_desc_start] + f'' + result[ + og_desc_end + 2:] + + if 'og_url' in seo_tags: + og_url_pattern = '', og_url_start) + if og_url_end != -1: + result = result[:og_url_start] + f'' + result[ + og_url_end + 2:] + + if 'profile_first_name' in seo_tags: + first_name_pattern = '', first_name_start) + if first_name_end != -1: + result = result[ + :first_name_start] + f'' + result[ + first_name_end + 2:] + + if 'profile_last_name' in seo_tags: + last_name_pattern = '', last_name_start) + if last_name_end != -1: + result = result[ + :last_name_start] + f'' + result[ + last_name_end + 2:] + + if 'twitter_title' in seo_tags: + twitter_title_pattern = '', twitter_title_start) + if twitter_title_end != -1: + result = result[ + :twitter_title_start] + f'' + result[ + twitter_title_end + 2:] + + if 'twitter_description' in seo_tags: + twitter_desc_pattern = '', twitter_desc_start) + if twitter_desc_end != -1: + result = result[ + :twitter_desc_start] + f'' + result[ + twitter_desc_end + 2:] + + if 'canonical_url' in seo_tags: + canonical_pattern = '', canonical_start) + if canonical_end != -1: + result = result[ + :canonical_start] + f'' + result[ + canonical_end + 2:] + + # Вставляем структурированные данные + if 'structured_data' in seo_tags: + structured_pattern = '', structured_start) + if structured_end != -1: + result = result[ + :structured_start] + f'\n