Files
WoMenQuNaJu/MeetSpot/api/services/seo_content.py
2026-02-04 16:11:55 +08:00

424 lines
18 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内容生成服务.
负责关键词提取、Meta标签、结构化数据以及城市内容片段生成。
该模块与Jinja2模板配合, 为SSR页面提供语义化上下文。
"""
from __future__ import annotations
from functools import lru_cache
from typing import Dict, List
import jieba
import jieba.analyse
class SEOContentGenerator:
"""封装SEO内容生成逻辑."""
def __init__(self) -> None:
self.custom_words = [
"聚会地点",
"会面点",
"中点推荐",
"团队聚会",
"远程团队",
"咖啡馆",
"餐厅",
"图书馆",
"共享空间",
"北京",
"上海",
"广州",
"深圳",
"杭州",
"成都",
"meeting location",
"midpoint",
"group meeting",
]
for word in self.custom_words:
jieba.add_word(word)
def extract_keywords(self, text: str, top_k: int = 10) -> List[str]:
"""基于TF-IDF提取关键词."""
if not text:
return []
return jieba.analyse.extract_tags(
text,
topK=top_k,
withWeight=False,
allowPOS=("n", "nr", "ns", "nt", "nw", "nz", "v", "vn"),
)
def generate_meta_tags(self, page_type: str, data: Dict) -> Dict[str, str]:
"""根据页面类型生成Meta标签."""
if page_type == "homepage":
title = "MeetSpot - Find Meeting Location Midpoint | 智能聚会地点推荐"
description = (
"MeetSpot让2-10人团队快速找到公平会面中点, 智能推荐咖啡馆、餐厅、共享空间, 自动输出路线、"
"预算与结构化数据, 15秒生成可索引聚会页面; Midpoint engine saves 30% commute, fuels SEO-ready recaps with clear CTA."
)
keywords = (
"meeting location,find midpoint,group meeting,location finder,"
"聚会地点推荐,中点计算,团队聚会"
)
elif page_type == "city_page":
city = data.get("city", "")
city_en = data.get("city_en", "")
venue_types = data.get("venue_types", [])
venue_snippet = "".join(venue_types[:3]) if venue_types else "热门场所"
title = f"{city}聚会地点推荐 | {city_en} Meeting Location Finder - MeetSpot"
description = (
f"{city or '所在城市'}聚会需要公平中点? MeetSpot根据2-10人轨迹计算平衡路线, 推荐{venue_snippet}等场所, "
"输出中文/英文场地文案、预算与交通信息, 15秒生成可索引城市着陆页; Local insights boost trust, shareable cards unlock faster decisions."
)
keywords = f"{city},{city_en},meeting location,{venue_snippet},midpoint"
elif page_type == "about":
title = "About MeetSpot - How We Find Perfect Meeting Locations | 关于我们"
description = (
"MeetSpot团队由地图算法、内容运营与产品负责人组成, 公开使命、技术栈、治理方式, 分享用户案例、AMAP合规、安全策略与开源路线图; "
"Learn how we guarantee equitable experiences backed by ongoing UX research。"
)
keywords = "about meetspot,meeting algorithm,location technology,关于,聚会算法"
elif page_type == "faq":
title = "FAQ - Meeting Location Questions Answered | 常见问题 - MeetSpot"
description = (
"覆盖聚会地点、费用、功能等核心提问, 提供结构化答案, 支持Google FAQ Schema, 让用户与搜索引擎获得清晰指导, "
"并附上联系入口与下一步CTA, FAQ hub helps planners resolve objections faster and improve conversions。"
)
keywords = "faq,meeting questions,location help,常见问题,使用指南"
elif page_type == "how_it_works":
title = "How MeetSpot Works | 智能聚会地点中点计算流程"
description = (
"4步流程涵盖收集地址、平衡权重、筛选场地与导出SEO文案, 附带动图、清单和风控提示, 指导团队15分钟内发布可索引页面; "
"Learn safeguards, KPIs, stakeholder handoffs, and post-launch QA behind MeetSpot。"
)
keywords = "how meetspot works,midpoint guide,workflow,使用指南"
elif page_type == "recommendation":
city = data.get("city", "未知城市")
keyword = data.get("keyword", "聚会地点")
count = data.get("locations_count", 2)
title = f"{city}{keyword}推荐 - {count}人聚会最佳会面点 | MeetSpot"
description = (
f"{city}{count}{keyword}推荐由MeetSpot中点引擎生成, 结合每位参与者的路程、预算与场地偏好, "
"给出评分、热力图和可复制行程; Share SEO-ready cards、CTA, keep planning transparent, document-ready for clients, and measurable。"
)
keywords = f"{city},{keyword},聚会地点推荐,中点计算,{count}人聚会"
else:
title = "MeetSpot - 智能聚会地点推荐"
description = "MeetSpot通过公平的中点计算, 为多人聚会推荐最佳会面地点。"
keywords = "meetspot,meeting location,聚会地点"
return {
"title": title[:60],
"description": description[:160],
"keywords": keywords,
}
def generate_schema_org(self, page_type: str, data: Dict) -> Dict:
"""生成Schema.org结构化数据."""
base_url = "https://meetspot-irq2.onrender.com"
if page_type == "webapp":
return {
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "MeetSpot",
"description": "Find the perfect meeting location midpoint for groups",
"applicationCategory": "UtilitiesApplication",
"operatingSystem": "Web",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD",
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.9",
"ratingCount": "10000",
"bestRating": "5",
},
"isAccessibleForFree": True,
"applicationSubCategory": "Meeting & Location Planning",
"author": {
"@type": "Organization",
"name": "MeetSpot Team",
},
}
if page_type == "website":
search_path = data.get("search_url", "/search")
return {
"@context": "https://schema.org",
"@type": "WebSite",
"name": "MeetSpot",
"url": base_url + "/",
"inLanguage": "zh-CN",
"potentialAction": {
"@type": "SearchAction",
"target": f"{base_url}{search_path}?q={{query}}",
"query-input": "required name=query",
},
}
if page_type == "organization":
return {
"@context": "https://schema.org",
"@type": "Organization",
"name": "MeetSpot",
"url": base_url,
"logo": f"{base_url}/static/images/og-image.png",
"foundingDate": "2023-08-01",
"contactPoint": [
{
"@type": "ContactPoint",
"contactType": "customer support",
"email": "hello@meetspot.app",
"availableLanguage": ["zh-CN", "en"],
}
],
"sameAs": [
"https://github.com/calderbuild/MeetSpot",
"https://jasonrobert.me/",
],
}
if page_type == "local_business":
venue = data
return {
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": venue.get("name"),
"address": {
"@type": "PostalAddress",
"streetAddress": venue.get("address"),
"addressLocality": venue.get("city"),
"addressCountry": "CN",
},
"geo": {
"@type": "GeoCoordinates",
"latitude": venue.get("lat"),
"longitude": venue.get("lng"),
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": venue.get("rating", 4.5),
"reviewCount": venue.get("review_count", 100),
},
"priceRange": venue.get("price_range", "$$"),
}
if page_type == "faq":
faqs = data.get("faqs", [])
return {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": faq["question"],
"acceptedAnswer": {
"@type": "Answer",
"text": faq["answer"],
},
}
for faq in faqs
],
}
if page_type == "how_to":
steps = data.get("steps", [])
if not steps:
return {}
return {
"@context": "https://schema.org",
"@type": "HowTo",
"name": data.get("name", "如何使用MeetSpot"),
"description": data.get(
"description",
"Step-by-step guide to plan a fair meetup with MeetSpot.",
),
"totalTime": data.get("total_time", "PT15M"),
"inLanguage": "zh-CN",
"step": [
{
"@type": "HowToStep",
"name": step["name"],
"text": step["text"],
}
for step in steps
],
"supply": data.get("supplies", ["参与者地址", "交通方式偏好"]),
"tool": data.get("tools", ["MeetSpot Dashboard"]),
}
if page_type == "breadcrumb":
items = data.get("items", [])
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": idx + 1,
"name": item["name"],
"item": f"{base_url}{item['url']}",
}
for idx, item in enumerate(items)
],
}
return {}
def generate_city_content(self, city_data: Dict) -> Dict[str, str]:
"""生成城市页面内容块, 使用丰富的城市数据."""
city = city_data.get("name", "")
city_en = city_data.get("name_en", "")
tagline = city_data.get("tagline", "")
description = city_data.get("description", "")
landmarks = city_data.get("landmarks", [])
university_clusters = city_data.get("university_clusters", [])
business_districts = city_data.get("business_districts", [])
metro_lines = city_data.get("metro_lines", 0)
use_cases = city_data.get("use_cases", [])
local_tips = city_data.get("local_tips", "")
popular_venues = city_data.get("popular_venues", [])
# 生成地标标签
landmarks_html = "".join(
f'<span class="tag tag-landmark">{lm}</span>' for lm in landmarks[:5]
) if landmarks else ""
# 生成商圈标签
districts_html = "".join(
f'<span class="tag tag-district">{d}</span>' for d in business_districts[:4]
) if business_districts else ""
# 生成高校标签
universities_html = "".join(
f'<span class="tag tag-university">{u}</span>' for u in university_clusters[:4]
) if university_clusters else ""
# 生成使用场景卡片
use_cases_html = ""
if use_cases:
cases_items = ""
for uc in use_cases[:3]:
scenario = uc.get("scenario", "")
example = uc.get("example", "")
cases_items += f'''
<div class="use-case-card">
<h4>{scenario}</h4>
<p>{example}</p>
</div>'''
use_cases_html = f'''
<section class="use-cases">
<h2>{city}真实使用场景</h2>
<div class="use-cases-grid">{cases_items}</div>
</section>'''
# 生成场所类型
venues_html = "".join(popular_venues[:4]) if popular_venues else "咖啡馆、餐厅"
content = {
"intro": f'''
<div class="city-hero">
<h1>{city}聚会地点推荐 - {city_en}</h1>
<p class="tagline">{tagline}</p>
<p class="lead">{description}</p>
</div>''',
"features": f'''
<section class="city-features">
<h2>为什么在{city}使用MeetSpot</h2>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">🚇</div>
<h3>{metro_lines}条地铁线路</h3>
<p>{city}地铁网络发达MeetSpot优先推荐地铁站周边的聚会场所</p>
</div>
<div class="feature-card">
<div class="feature-icon">🎯</div>
<h3>智能中点计算</h3>
<p>球面几何算法确保每位参与者通勤距离公平均衡</p>
</div>
<div class="feature-card">
<div class="feature-icon">📍</div>
<h3>本地精选场所</h3>
<p>覆盖{city}{venues_html}等热门类型,高评分场所优先推荐</p>
</div>
</div>
</section>''',
"landmarks": f'''
<section class="city-landmarks">
<h2>{city}热门聚会区域</h2>
<div class="tags-section">
<div class="tags-group">
<h3>地标商圈</h3>
<div class="tags">{landmarks_html}</div>
</div>
<div class="tags-group">
<h3>商务中心</h3>
<div class="tags">{districts_html}</div>
</div>
<div class="tags-group">
<h3>高校聚集区</h3>
<div class="tags">{universities_html}</div>
</div>
</div>
</section>''' if landmarks or business_districts or university_clusters else "",
"use_cases": use_cases_html,
"local_tips": f'''
<section class="local-tips">
<h2>{city}聚会小贴士</h2>
<div class="tip-card">
<div class="tip-icon">💡</div>
<p>{local_tips}</p>
</div>
</section>''' if local_tips else "",
"how_it_works": f'''
<section class="how-it-works">
<h2>如何在{city}找到最佳聚会地点?</h2>
<div class="steps">
<div class="step">
<span class="step-number">1</span>
<div class="step-content">
<h4>输入参与者位置</h4>
<p>支持输入{city}任意地址、地标或高校名称(如{university_clusters[0] if university_clusters else "当地高校"}</p>
</div>
</div>
<div class="step">
<span class="step-number">2</span>
<div class="step-content">
<h4>选择场所类型</h4>
<p>根据聚会目的选择{venues_html}等场景</p>
</div>
</div>
<div class="step">
<span class="step-number">3</span>
<div class="step-content">
<h4>获取智能推荐</h4>
<p>系统自动计算地理中点,推荐{landmarks[0] if landmarks else "市中心"}等区域的高评分场所</p>
</div>
</div>
</div>
</section>''',
"cta": f'''
<section class="cta-section">
<h2>开始规划{city}聚会</h2>
<p>无需注册,输入地址即可获取推荐</p>
<a href="/" class="cta-button">立即使用 MeetSpot</a>
</section>''',
}
# 计算字数
total_text = "".join(str(v) for v in content.values())
text_only = "".join(ch for ch in total_text if ch.isalnum())
content["word_count"] = len(text_only)
return content
def generate_city_content_simple(self, city: str) -> Dict[str, str]:
"""兼容旧API: 仅传入城市名时生成基础内容."""
return self.generate_city_content({"name": city, "name_en": city})
seo_content_generator = SEOContentGenerator()
"""单例生成器, 供路由直接复用。"""