feat: GEO/SEO structured data + search ranking tracker
This commit is contained in:
+90
-4
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user