feat: GEO/SEO structured data + search ranking tracker

This commit is contained in:
Yuzhiran Dev
2026-06-09 17:18:09 +08:00
parent 23ff63baa9
commit 8595bbc521
26 changed files with 961 additions and 51 deletions
+90 -4
View File
@@ -5,7 +5,7 @@
"""
import json, datetime, logging, sys, re
from pathlib import Path
from typing import Dict, List
from typing import Dict, List, Optional
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
@@ -69,6 +69,90 @@ def _load_platform_config() -> dict:
PLATFORM_CONFIG = _load_platform_config()
PLATFORM_NAMES = {
"zhihu": "知乎专栏",
"wechat": "微信公众号",
"xiaohongshu": "小红书",
}
def _extract_description(content: str, max_len: int = 200) -> str:
"""从 markdown 正文提取第一段有意义的文字作为 description"""
text = re.sub(r'^#\s+.*$', '', content, flags=re.MULTILINE)
text = re.sub(r'[#*>`~\[\]()\n]', ' ', text)
paragraphs = [p.strip() for p in text.split('\n\n') if p.strip()]
for p in paragraphs:
p = re.sub(r'\s+', ' ', p).strip()
if len(p) >= 15 and not p.startswith('http'):
return p[:max_len]
return content.replace('\n', ' ')[:max_len]
def _extract_tags_list(tags_html: str) -> list:
"""从 HTML tags 块提取纯标签列表"""
return re.findall(r'<span class="tag">([^<]+)</span>', tags_html)
def inject_geo_metadata(html: str, title: str, content: str, platform: str, tags_html: str = "") -> str:
"""向 HTML <head> 注入 SEO/GEO 结构化元数据"""
description = _extract_description(content)
tags_list = _extract_tags_list(tags_html)
platform_name = PLATFORM_NAMES.get(platform, platform)
today = datetime.datetime.now().strftime("%Y-%m-%d")
# JSON-LD Article schema
json_ld = {
"@context": "https://schema.org",
"@type": "Article",
"headline": title,
"description": description,
"datePublished": today,
"dateModified": today,
"author": {
"@type": "Organization",
"name": "宇之然",
"url": "https://yu-zhi-ran.com"
},
"publisher": {
"@type": "Organization",
"name": "宇之然",
"url": "https://yu-zhi-ran.com"
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": f"https://yu-zhi-ran.com/article/{platform}"
},
}
if tags_list:
json_ld["keywords"] = ", ".join(tags_list[:8])
json_ld_str = json.dumps(json_ld, ensure_ascii=False)
meta_tags = f"""
<meta name="description" content="{description}">
<meta name="keywords" content="{', '.join(tags_list[:8]) if tags_list else ''}">
<meta property="og:type" content="article">
<meta property="og:title" content="{title}">
<meta property="og:description" content="{description[:150]}">
<meta property="og:site_name" content="宇之然 | {platform_name}">
<meta property="article:published_time" content="{today}">
<meta property="article:author" content="宇之然">
<script type="application/ld+json">
{json_ld_str}
</script>"""
# 注入到 </head> 之前
html = html.replace("</head>", meta_tags + "\n</head>")
# 为 wechat + xiaohongshu 追加 Weibo/Wechat 兼容 meta
if platform in ("wechat", "xiaohongshu"):
html = html.replace("</head>", """
<meta property="og:image" content="https://yu-zhi-ran.com/og-image.png">
<meta name="weibo:webpage:source" content="宇之然">
</head>""")
return html
class Writer:
def __init__(self, topic_id: str):
self.topic_id = topic_id
@@ -443,11 +527,12 @@ class Writer:
else:
html = html.replace("<!-- TAGS -->", "")
html = inject_geo_metadata(html, title, adapted, platform, tags_html)
return html
def save_html(self, html: str, platform: str) -> str:
def save_html(self, html: str, platform: str, *, title: str = "", content: str = "") -> str:
try:
save_article(self.topic_id, platform, html)
save_article(self.topic_id, platform, html, title=title, content=content)
logger.info(f"文章写入数据库: {platform}_{self.topic_id}")
return f"db:{platform}_{self.topic_id}"
except Exception as e:
@@ -463,8 +548,9 @@ class Writer:
results = {}
for platform in ["zhihu", "wechat", "xiaohongshu"]:
markdown = self.generate_platform_markdown(platform)
title = self._optimize_title(platform)
html = self.generate_platform_html(markdown, platform)
results[platform] = str(self.save_html(html, platform))
results[platform] = str(self.save_html(html, platform, title=title, content=markdown))
self.mark_draft()
logger.info(f"撰写完成,状态已更新为待审查")
return {"ok": True, "files": results}