# 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 шаблон """ result = html_template # Заменяем title title_pattern = '.*?' result = re.sub(title_pattern, f'{seo_tags.get("title", "Rabota.Today")}', result) # Заменяем description desc_pattern = '' result = re.sub(desc_pattern, f'', result) # Заменяем keywords keywords_pattern = '' result = re.sub(keywords_pattern, f'', result) # Заменяем og:title og_title_pattern = '' result = re.sub(og_title_pattern, f'', result) # Заменяем og:description og_desc_pattern = '' result = re.sub(og_desc_pattern, f'', result) # Заменяем og:url og_url_pattern = '' result = re.sub(og_url_pattern, f'', result) # Заменяем profile:first_name first_name_pattern = '' if 'profile_first_name' in seo_tags: result = re.sub(first_name_pattern, f'', result) # Заменяем profile:last_name last_name_pattern = '' if 'profile_last_name' in seo_tags: result = re.sub(last_name_pattern, f'', result) # Заменяем twitter:title twitter_title_pattern = '' result = re.sub(twitter_title_pattern, f'', result) # Заменяем twitter:description twitter_desc_pattern = '' result = re.sub(twitter_desc_pattern, f'', result) # Заменяем canonical canonical_pattern = '' result = re.sub(canonical_pattern, f'', result) # Заменяем структурированные данные - заменяем весь блок от structured_pattern = r'' # Находим и заменяем первый блок структурированных данных result = re.sub(structured_pattern, f'', result, count=1) # Удаляем второй пустой блок, если он есть empty_structured_pattern = r'' result = re.sub(empty_structured_pattern, '', result) return result