Files
yarmarka/seo_helpers.py
2026-03-20 19:22:34 +03:00

320 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 = {
'<title id="pageTitle">Резюме | Rabota.Today</title>': f'<title>{seo_tags.get("title", "Rabota.Today")}</title>',
'<title id="pageTitle">Вакансия | Rabota.Today</title>': f'<title>{seo_tags.get("title", "Rabota.Today")}</title>',
'<meta name="description" id="metaDescription"': f'<meta name="description"',
'<meta name="keywords"': f'<meta name="keywords"',
'<meta property="og:title" id="ogTitle"': f'<meta property="og:title"',
'<meta property="og:description" id="ogDescription"': f'<meta property="og:description"',
'<meta property="og:url" id="ogUrl"': f'<meta property="og:url"',
'<meta property="profile:first_name" id="profileFirstName"': f'<meta property="profile:first_name"',
'<meta property="profile:last_name" id="profileLastName"': f'<meta property="profile:last_name"',
'<meta name="twitter:title" id="twitterTitle"': f'<meta name="twitter:title"',
'<meta name="twitter:description" id="twitterDescription"': f'<meta name="twitter:description"',
'<link rel="canonical" id="canonicalUrl"': f'<link rel="canonical"'
}
result = html_template
# Заменяем общие теги
for old, new in replacements.items():
if old in result:
result = result.replace(old, new)
# Вставляем конкретные значения
if 'description' in seo_tags:
desc_pattern = '<meta name="description" content="'
desc_start = result.find(desc_pattern)
if desc_start != -1:
desc_end = result.find('">', desc_start)
if desc_end != -1:
result = result[
:desc_start] + f'<meta name="description" content="{seo_tags["description"]}">' + result[
desc_end + 2:]
if 'keywords' in seo_tags:
keywords_pattern = '<meta name="keywords" content="'
keywords_start = result.find(keywords_pattern)
if keywords_start != -1:
keywords_end = result.find('">', keywords_start)
if keywords_end != -1:
result = result[:keywords_start] + f'<meta name="keywords" content="{seo_tags["keywords"]}">' + result[
keywords_end + 2:]
if 'og_title' in seo_tags:
og_title_pattern = '<meta property="og:title" content="'
og_title_start = result.find(og_title_pattern)
if og_title_start != -1:
og_title_end = result.find('">', og_title_start)
if og_title_end != -1:
result = result[
:og_title_start] + f'<meta property="og:title" content="{seo_tags["og_title"]}">' + result[
og_title_end + 2:]
if 'og_description' in seo_tags:
og_desc_pattern = '<meta property="og:description" content="'
og_desc_start = result.find(og_desc_pattern)
if og_desc_start != -1:
og_desc_end = result.find('">', og_desc_start)
if og_desc_end != -1:
result = result[
:og_desc_start] + f'<meta property="og:description" content="{seo_tags["og_description"]}">' + result[
og_desc_end + 2:]
if 'og_url' in seo_tags:
og_url_pattern = '<meta property="og:url" content="'
og_url_start = result.find(og_url_pattern)
if og_url_start != -1:
og_url_end = result.find('">', og_url_start)
if og_url_end != -1:
result = result[:og_url_start] + f'<meta property="og:url" content="{seo_tags["og_url"]}">' + result[
og_url_end + 2:]
if 'profile_first_name' in seo_tags:
first_name_pattern = '<meta property="profile:first_name" content="'
first_name_start = result.find(first_name_pattern)
if first_name_start != -1:
first_name_end = result.find('">', first_name_start)
if first_name_end != -1:
result = result[
:first_name_start] + f'<meta property="profile:first_name" content="{seo_tags["profile_first_name"]}">' + result[
first_name_end + 2:]
if 'profile_last_name' in seo_tags:
last_name_pattern = '<meta property="profile:last_name" content="'
last_name_start = result.find(last_name_pattern)
if last_name_start != -1:
last_name_end = result.find('">', last_name_start)
if last_name_end != -1:
result = result[
:last_name_start] + f'<meta property="profile:last_name" content="{seo_tags["profile_last_name"]}">' + result[
last_name_end + 2:]
if 'twitter_title' in seo_tags:
twitter_title_pattern = '<meta name="twitter:title" content="'
twitter_title_start = result.find(twitter_title_pattern)
if twitter_title_start != -1:
twitter_title_end = result.find('">', twitter_title_start)
if twitter_title_end != -1:
result = result[
:twitter_title_start] + f'<meta name="twitter:title" content="{seo_tags["twitter_title"]}">' + result[
twitter_title_end + 2:]
if 'twitter_description' in seo_tags:
twitter_desc_pattern = '<meta name="twitter:description" content="'
twitter_desc_start = result.find(twitter_desc_pattern)
if twitter_desc_start != -1:
twitter_desc_end = result.find('">', twitter_desc_start)
if twitter_desc_end != -1:
result = result[
:twitter_desc_start] + f'<meta name="twitter:description" content="{seo_tags["twitter_description"]}">' + result[
twitter_desc_end + 2:]
if 'canonical_url' in seo_tags:
canonical_pattern = '<link rel="canonical" href="'
canonical_start = result.find(canonical_pattern)
if canonical_start != -1:
canonical_end = result.find('">', canonical_start)
if canonical_end != -1:
result = result[
:canonical_start] + f'<link rel="canonical" href="{seo_tags["canonical_url"]}">' + result[
canonical_end + 2:]
# Вставляем структурированные данные
if 'structured_data' in seo_tags:
structured_pattern = '<script type="application/ld+json" id="structuredData">'
structured_start = result.find(structured_pattern)
if structured_start != -1:
structured_end = result.find('</script>', structured_start)
if structured_end != -1:
result = result[
:structured_start] + f'<script type="application/ld+json">\n{seo_tags["structured_data"]}\n</script>\n<script type="application/ld+json" id="structuredData" style="display:none;">' + result[
structured_end + 9:]
return result