SEO helper
This commit is contained in:
320
seo_helpers.py
Normal file
320
seo_helpers.py
Normal file
@@ -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 = {
|
||||
'<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
|
||||
Reference in New Issue
Block a user