Files
Sansheng-Intelligence/scripts/build_seo.py
T
Jack 7e74bd989e fix: 对齐平台渠道与最新情报快照
2026-08-14 平台渠道专项更新:前端和静态SEO只展示平台官网、官方入驻及非中文入驻页辅助翻译;移除类目入口翻译、消费者官网翻译和内部成熟度/新兴参考分文案。

同步59/59权威源、65场跨境活动、28个平台和208条趋势内容;修正IBTE、CCBEC、法国教育部、Shopee和Shopify语种判断,更新README与CHANGELOG并通过Python、Node及静态构建验证。
2026-08-14 22:18:51 +08:00

595 lines
30 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.
#!/usr/bin/env python3
"""为家纺情报生成无需 JavaScript 的静态 SEO 页面。"""
from __future__ import annotations
import argparse
import html
import json
from datetime import datetime, timezone
from pathlib import Path
from shutil import rmtree
from typing import Any
from urllib.parse import urljoin
from seo_config import BRAND_NAME, answer_first, keywords_for
DEFAULT_SITE_URL = "http://127.0.0.1:8765"
SECTION_CONFIG = {
"calendar": ("市场备货日历", "按国家、地区与消费场景聚合的家纺备货决策情报"),
"exhibitions": ("展会通", "国内外家纺、纺织、面料与家居展会情报"),
"policies": ("政策预警", "跨境电商、运输、关税、合规与知识产权预警"),
"standards": ("趋势雷达", "色彩、材料、认证与纺织测试标准信号"),
"platforms": ("平台渠道", "重点跨境平台、家纺类目、入驻条件与官方入口"),
"activities": ("跨境活动", "家纺出海、平台招商、合规、知识产权与物流活动"),
}
def esc(value: Any) -> str:
return html.escape(str(value or ""), quote=True)
def absolute(site_url: str, path: str) -> str:
return urljoin(site_url.rstrip("/") + "/", path.lstrip("/"))
def json_script(payload: dict[str, Any]) -> str:
return json.dumps(payload, ensure_ascii=False, separators=(",", ":")).replace("</", "<\\/")
def date_label(item: dict[str, Any]) -> str:
start = str(item.get("start_date") or item.get("published_at") or "")[:10]
end = str(item.get("end_date") or "")[:10]
if start and end and end != start:
return f"{start} 至 {end}"
return start or "持续监测"
def item_schema(item: dict[str, Any], canonical: str) -> dict[str, Any]:
section = item.get("section")
common = {
"@context": "https://schema.org",
"name": item.get("title"),
"description": item.get("summary"),
"url": canonical,
"mainEntityOfPage": canonical,
"inLanguage": "zh-CN",
"keywords": keywords_for(str(section or ""), item),
"isAccessibleForFree": True,
}
if section in ("calendar", "exhibition", "activity"):
schema = {"@type": "Event", **common}
schema["startDate"] = item.get("start_date")
schema["endDate"] = item.get("end_date") or item.get("start_date")
schema["eventStatus"] = "https://schema.org/EventScheduled"
online = item.get("format") == "线上"
schema["eventAttendanceMode"] = (
"https://schema.org/OnlineEventAttendanceMode" if online
else "https://schema.org/OfflineEventAttendanceMode"
)
schema["location"] = (
{"@type": "VirtualLocation", "url": item.get("registration_url") or item.get("url")}
if online else {
"@type": "Place",
"name": item.get("venue") or item.get("region") or "活动地点",
"address": item.get("region") or "",
}
)
schema["organizer"] = {"@type": "Organization", "name": item.get("source"), "url": item.get("source_url")}
return schema
return {
"@type": "Article",
**common,
"headline": item.get("title"),
"dateModified": item.get("verified_at"),
"datePublished": item.get("published_at") or item.get("verified_at"),
"author": {"@type": "Organization", "name": item.get("source")},
"publisher": {"@type": "Organization", "name": "三笙AI情报局"},
}
def page_shell(*, title: str, description: str, canonical: str, body: str, schema: dict[str, Any], site_url: str, section: str = "") -> str:
image = absolute(site_url, "/assets/logo.svg")
keywords = ",".join(keywords_for(section))
return f"""<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{esc(title)}</title>
<meta name="description" content="{esc(description[:155])}">
<meta name="robots" content="index,follow,max-image-preview:large">
<meta name="author" content="{esc(BRAND_NAME)}">
<meta name="keywords" content="{esc(keywords)}">
<meta name="ai-content-declaration" content="official-facts-separated-from-machine-translation-and-analysis">
<link rel="canonical" href="{esc(canonical)}">
<link rel="alternate" hreflang="zh-CN" href="{esc(canonical)}">
<link rel="alternate" hreflang="x-default" href="{esc(canonical)}">
<meta property="og:type" content="article">
<meta property="og:locale" content="zh_CN">
<meta property="og:site_name" content="{esc(BRAND_NAME)}">
<meta property="og:title" content="{esc(title)}">
<meta property="og:description" content="{esc(description[:200])}">
<meta property="og:url" content="{esc(canonical)}">
<meta property="og:image" content="{esc(image)}">
<meta name="twitter:card" content="summary_large_image">
<link rel="stylesheet" href="/assets/css/seo-textile.css?v=2.0">
<script type="application/ld+json">{json_script(schema)}</script>
<script type="module" src="/assets/js/seo-visitor-region.js?v=20260814f"></script>
</head>
<body>
<header class="seo-topbar">
<a class="seo-brand" href="/?intel=today">三笙AI情报局</a>
<nav aria-label="家纺情报栏目">
<a href="/textile/calendar/">备货日历</a>
<a href="/textile/exhibitions/">展会通</a>
<a href="/textile/policies/">政策预警</a>
<a href="/textile/standards/">趋势雷达</a>
<a href="/textile/platforms/">平台渠道</a>
<a href="/textile/activities/">跨境活动</a>
</nav>
</header>
<main class="seo-shell">{body}</main>
<footer class="seo-footer">官方事实、机器翻译与本站经营分析分区展示;业务决策前请打开官方来源复核适用范围与生效日期。</footer>
</body>
</html>
"""
def tags_html(values: list[str] | tuple[str, ...] | None) -> str:
return "".join(f'<span>{esc(value)}</span>' for value in (values or []) if value)
TRANSLATED_LINK_LABEL = "查看译文版"
def links_html(links: list[tuple[str, str]], class_name: str = "", translation_label: str = TRANSLATED_LINK_LABEL) -> str:
seen: set[str] = set()
output: list[str] = []
translation_group = 0
for label, url in links:
if not url or url in seen:
continue
seen.add(url)
provider = "yandex" if "translate.yandex.com/" in url else ("google" if "translate.google.com/" in url else "")
if provider:
label = translation_label
else:
translation_group += 1
translation_gate = f' data-translation-provider="{provider}" data-translation-group="{translation_group}" hidden' if provider else ""
output.append(
f'<a class="{esc(class_name)}" href="{esc(url)}" target="_blank"{translation_gate} '
f'rel="noopener noreferrer nofollow">{esc(label)}</a>'
)
return "".join(output)
def load_trend_items(data_path: Path) -> list[dict[str, Any]]:
trend_path = data_path.with_name("textile-trends.json")
if not trend_path.exists():
return []
payload = json.loads(trend_path.read_text(encoding="utf-8"))
verified_at = str(payload.get("generated_at") or "")
items: list[dict[str, Any]] = []
for category in (payload.get("categories") or {}).values():
for raw in category.get("items") or []:
item = dict(raw)
item.update({
"id": f"trend-{raw['id']}",
"section": "trend",
"image": raw.get("thumbnail") or "",
"source_url": raw.get("url") or "",
"authority": "行业媒体或博物馆原始来源",
"confidence": "参考",
"verified_at": verified_at,
"region": "全球",
"trend_kind": "news",
})
items.append(item)
return items
def card_html(item: dict[str, Any], detail_path: str, section: str) -> str:
accent = esc((item.get("poster") or {}).get("accent") or "#ff6b35")
meta = " · ".join(filter(None, (date_label(item), str(item.get("region") or ""), str(item.get("source") or ""))))
culture = esc(item.get("culture_key") or "global")
extra = ""
if section == "calendar":
extra = f"""<p class="seo-decision">{esc(item.get('decision_label') or '经营观察')} · {esc(item.get('planning_stage'))}</p>
<p class="seo-window">备货窗口 {esc(item.get('stocking_start'))} 至 {esc(item.get('stocking_end'))}</p>
<div class="seo-tags">{tags_html(item.get('affected_countries'))}{tags_html(item.get('product_suggestions'))}</div>"""
elif section == "policies":
extra = f'<div class="seo-tags">{tags_html(item.get("affected_markets"))}{tags_html(item.get("platforms"))}</div>'
elif section == "platforms":
extra = f'<p class="seo-decision">{esc(item.get("access_level"))} {esc(item.get("access_label"))} · {esc(item.get("confidence"))}</p><div class="seo-tags">{tags_html(item.get("markets"))}{tags_html(item.get("fit_sellers"))}</div>'
elif section == "activities":
extra = f'<p class="seo-decision">{esc(item.get("source_level"))} · {esc(item.get("activity_type"))} · {esc(item.get("format"))}</p><div class="seo-tags">{tags_html(item.get("platforms"))}</div>'
return f"""<article class="seo-card is-{esc(section)} culture-{culture}" style="--card-accent:{accent}">
<p class="seo-card-meta">{esc(meta)}</p>
<h2><a href="/{esc(detail_path)}">{esc(item.get('title'))}</a></h2>
<p>{esc(item.get('summary'))}</p>
{extra}
<span class="seo-proof">{esc(item.get('authority'))} · {esc(item.get('confidence'))}可信度</span>
</article>"""
def build_collection(section: str, items: list[dict[str, Any]], site_url: str) -> tuple[str, list[tuple[str, str]]]:
title, description = SECTION_CONFIG[section]
selected = items
detail_entries: list[tuple[str, str]] = []
cards: list[str] = []
for item in selected:
detail_path = f"textile/intelligence/{section}/{item['id']}/index.html"
cards.append(card_html(item, detail_path.removesuffix("index.html"), section))
detail_entries.append((detail_path, item["id"]))
canonical = absolute(site_url, f"/textile/{section}/")
item_list = {
"@context": "https://schema.org",
"@type": "CollectionPage",
"name": f"家纺跨境情报 · {title}",
"description": description,
"url": canonical,
"mainEntity": {
"@type": "ItemList",
"numberOfItems": len(selected),
"itemListElement": [
{"@type": "ListItem", "position": index + 1, "name": item.get("title"), "url": absolute(site_url, f"/{path.removesuffix('index.html')}")}
for index, ((path, _item_id), item) in enumerate(zip(detail_entries, selected))
],
},
}
body = f"""<section class="seo-hero">
<p class="seo-eyebrow">每日更新 · 权威源验证</p>
<h1>{esc(title)}</h1>
<p>{esc(description)}。当前静态收录 {len(selected)} 条,可直接被搜索引擎读取。</p>
</section>
<section class="seo-answer" data-answer-first="true"><h2>快速答案</h2><p>{esc(answer_first(section))}</p></section>
<section class="seo-grid">{''.join(cards)}</section>"""
return page_shell(
title=f"{title}|家纺跨境情报|每日机会与风险提醒",
description=description,
canonical=canonical,
body=body,
schema=item_list,
site_url=site_url,
section=section,
), detail_entries
def build_detail(item: dict[str, Any], section: str, site_url: str) -> str:
section_title = SECTION_CONFIG[section][0]
canonical = absolute(site_url, f"/textile/intelligence/{section}/{item['id']}/")
action = item.get("opportunity") or item.get("action") or item.get("application") or "打开官方来源复核后纳入选品、备货与合规流程。"
image = item.get("image")
visual = (
f'<img class="seo-detail-image" src="{esc(image)}" alt="{esc(item.get("title"))}" loading="eager">'
if image
else f'<div class="seo-poster" style="--poster-accent:{esc((item.get("poster") or {}).get("accent"))}"><span>{esc(section_title)}</span><strong>{esc(item.get("title"))}</strong><small>{esc(item.get("region"))}</small></div>'
)
section_details = ""
original_title = str(item.get("title_original") or "")
original_html = (
f'<p class="seo-original" lang="en">{esc(original_title)}</p>'
if original_title and original_title != str(item.get("title") or "")
else ""
)
if section == "calendar":
scope_lines = [
f"{detail.get('country')}:{detail.get('scope') or '全国适用'}"
for detail in item.get("country_details") or []
]
official_links = links_html([
(f"{source.get('name') or '官方日期来源'}", str(source.get("url") or ""))
for source in item.get("official_sources") or []
])
platform_rows = "".join(
f"<li><strong>{esc(row.get('platform'))}</strong>:{esc(row.get('action'))}</li>"
for row in item.get("platform_actions") or []
)
section_details = f"""<section class="seo-context">
<p class="seo-decision">{esc(item.get('decision_label') or '经营观察')} · {esc(item.get('planning_stage'))}</p>
<h2>这个节日是什么</h2>
<p>{esc(item.get('what_it_is') or item.get('summary'))}</p>
<div class="seo-context-grid">
<div><strong>重点国家/地区</strong><div class="seo-tags">{tags_html(item.get('affected_countries'))}</div></div>
<div><strong>法定适用范围</strong><p>{esc(';'.join(scope_lines) or '以官方来源公布范围为准')}</p></div>
<div><strong>主要语种</strong><div class="seo-tags">{tags_html(item.get('languages'))}</div></div>
<div><strong>相关平台</strong><div class="seo-tags">{tags_html(item.get('platforms'))}</div></div>
<div><strong>消费场景</strong><div class="seo-tags">{tags_html(item.get('consumption_scenes'))}</div></div>
</div>
</section>
<section class="seo-context">
<h2>家纺备货与设计机会</h2>
<p><strong>{esc(item.get('stocking_start'))} 至 {esc(item.get('stocking_end'))}</strong> · {esc(item.get('next_action') or item.get('opportunity'))}</p>
<div class="seo-tags">{tags_html(item.get('product_suggestions'))}</div>
<p><strong>设计方向:</strong>{esc(';'.join(item.get('design_opportunities') or []))}</p>
<p class="seo-caution"><strong>文化与表达注意:</strong>{esc(';'.join(item.get('cultural_avoidances') or []))}</p>
{f'<h2>平台经营动作</h2><ul>{platform_rows}</ul>' if platform_rows else ''}
</section>
<section class="seo-context seo-proof-layer">
<h2>事实与分析边界</h2>
<p>官方事实:日期、适用国家和节日名称由来源核验。系统分析:消费场景、产品方向、设计建议和备货节奏属于经营辅助判断。</p>
<div class="seo-link-row">{official_links}</div>
</section>"""
elif section == "exhibitions":
exhibition_links = links_html([
(str(item.get("registration_mode") or "参展信息"), str(item.get("registration_url") or "")),
("中文查看报名说明", str(item.get("registration_translation_url") or "")),
(TRANSLATED_LINK_LABEL, str(item.get("registration_translation_fallback_url") or "")),
(str(item.get("visitor_mode") or "观展信息"), str(item.get("visitor_url") or "")),
("中文查看观众登记", str(item.get("visitor_translation_url") or "")),
(TRANSLATED_LINK_LABEL, str(item.get("visitor_translation_fallback_url") or "")),
("官方展会原文", str(item.get("url") or item.get("source_url") or "")),
("官网简体中文阅读", str(item.get("translation_url") or "")),
(TRANSLATED_LINK_LABEL, str(item.get("translation_fallback_url") or "")),
])
section_details = f"""<section class="seo-context">
<h2>{esc(item.get('attendance_recommendation') or '先评估是否值得参加')}</h2>
<p>{esc(item.get('attendance_reason') or item.get('opportunity'))}</p>
<div class="seo-tags">{tags_html(item.get('event_focus'))}</div>
<ol>{''.join(f'<li>{esc(value)}</li>' for value in item.get('trip_checklist') or [])}</ol>
<p class="seo-caution">机器翻译仅用于辅助阅读;报名、验证码、付款和资料提交请回到官方域名完成。</p>
<div class="seo-link-row">{exhibition_links}</div>
</section>"""
elif section == "policies":
policy_links = links_html([
("官方政策原文", str(item.get("url") or item.get("source_url") or "")),
("官方来源简体中文阅读", str(item.get("translation_url") or "")),
(TRANSLATED_LINK_LABEL, str(item.get("translation_fallback_url") or "")),
])
section_details = f"""<section class="seo-context">
<h2>发生了什么</h2>
<p>{esc(item.get('change_summary') or item.get('summary'))}</p>
<p><strong>不处理的后果:</strong>{esc(item.get('consequence'))}</p>
<p>{esc(item.get('policy_type'))} · {esc(item.get('change_status'))} · 生效日期 {esc(item.get('effective_at') or '以官方原文为准')}</p>
<h2>适用市场与平台</h2>
<div class="seo-tags">{tags_html(item.get('affected_markets'))}{tags_html(item.get('platforms'))}</div>
<div class="seo-link-row">{policy_links}</div>
</section>"""
elif section == "platforms":
platform_links = links_html([
("打开平台官网", str(item.get("homepage_url") or "") if item.get("homepage_url") != item.get("seller_url") else ""),
("去官方入驻", str(item.get("seller_url") or "")),
(TRANSLATED_LINK_LABEL, str(item.get("seller_translation_url") or "")),
(TRANSLATED_LINK_LABEL, str(item.get("seller_translation_fallback_url") or "")),
], translation_label="辅助翻译入驻说明")
section_details = f"""<section class="seo-context">
<h2>家纺类目与入驻判断</h2>
<p><strong>{esc(item.get('access_level'))} {esc(item.get('access_label'))}</strong> · {esc(item.get('cross_border'))}</p>
<p>{esc(item.get('textile_category'))}</p>
<div class="seo-tags">{tags_html(item.get('markets'))}{tags_html(item.get('fit_sellers'))}</div>
<div class="seo-link-row">{platform_links}</div>
</section>"""
elif section == "activities":
activity_links = links_html([
("查看并报名", str(item.get("registration_url") or item.get("url") or "")),
(TRANSLATED_LINK_LABEL, str(item.get("translation_url") or "")),
(TRANSLATED_LINK_LABEL, str(item.get("translation_fallback_url") or "")),
("查看来源页", str(item.get("source_url") or "")),
(TRANSLATED_LINK_LABEL, str(item.get("source_translation_url") or "")),
(TRANSLATED_LINK_LABEL, str(item.get("source_translation_fallback_url") or "")),
])
section_details = f"""<section class="seo-context">
<h2>活动价值与参加方式</h2>
<p><strong>{esc(item.get('source_level'))} · {esc(item.get('activity_type'))}</strong> · {esc(item.get('format'))} · {esc(item.get('location'))}</p>
<p>{esc(item.get('next_action'))}</p>
<div class="seo-tags">{tags_html(item.get('platforms'))}</div>
<p class="seo-caution">行业媒体只用于发现活动;日期、议程、费用和报名状态以主办方或平台官方页面为准。机器翻译仅辅助阅读,正式提交请回原网站。</p>
<div class="seo-link-row">{activity_links}</div>
</section>"""
elif section == "standards":
trend_links = links_html([
("查看原文", str(item.get("url") or item.get("source_url") or "")),
(TRANSLATED_LINK_LABEL, str(item.get("translation_url") or "")),
(TRANSLATED_LINK_LABEL, str(item.get("translation_fallback_url") or "")),
])
if item.get("trend_kind") == "news":
section_details = f"""<section class="seo-context">
<h2>这条趋势如何使用</h2>
<p>该内容属于行业新闻或设计灵感参考,可用于选色、选材、图案和市场判断;打样与合规决策仍需结合产品规格及官方标准。</p>
<p><strong>内容类别:</strong>{esc(item.get('category') or '趋势新闻')}</p>
<div class="seo-link-row">{trend_links}</div>
</section>"""
else:
tasks = "".join(f"<li>{esc(value)}</li>" for value in item.get("brief_items") or [])
section_details = f"""<section class="seo-context">
<h2>产品开发与检测动作</h2>
<p>{esc(item.get('application') or item.get('decision_note') or item.get('summary'))}</p>
{f'<ol>{tasks}</ol>' if tasks else ''}
<div class="seo-link-row">{trend_links}</div>
</section>"""
body = f"""<article class="seo-detail">
<nav class="seo-breadcrumb" aria-label="面包屑"><a href="/textile/intelligence/">家纺跨境情报</a><span>/</span><a href="/textile/{esc(section)}/">{esc(section_title)}</a></nav>
<section class="seo-answer" data-answer-first="true"><h2>先看结论</h2><p>{esc(answer_first(section, item))}</p></section>
<p class="seo-machine-translation-note">机器翻译仅用于辅助阅读;报名、付款、合规提交和日期确认以官方原文为准。</p>
<p class="seo-eyebrow">{esc(item.get('authority'))} · {esc(item.get('confidence'))}可信度</p>
<h1>{esc(item.get('title'))}</h1>
{original_html}
<p class="seo-lead">{esc(item.get('summary'))}</p>
{visual}
<dl class="seo-facts">
<div><dt>时间</dt><dd>{esc(date_label(item))}</dd></div>
<div><dt>区域</dt><dd>{esc(item.get('region'))}</dd></div>
<div><dt>来源</dt><dd>{esc(item.get('source'))}</dd></div>
<div><dt>核验</dt><dd>{esc(str(item.get('verified_at') or '')[:19].replace('T', ' '))}</dd></div>
</dl>
{section_details}
<section class="seo-action"><h2>家纺行动建议</h2><p>{esc(action)}</p></section>
<a class="seo-source-link" href="{esc(item.get('url'))}" target="_blank" rel="noopener noreferrer nofollow">打开官方来源</a>
</article>"""
schema = {
"@context": "https://schema.org",
"@graph": [
item_schema(item, canonical),
{
"@type": "BreadcrumbList",
"itemListElement": [
{"@type": "ListItem", "position": 1, "name": "家纺跨境情报", "item": absolute(site_url, "/textile/intelligence/")},
{"@type": "ListItem", "position": 2, "name": section_title, "item": absolute(site_url, f"/textile/{section}/")},
{"@type": "ListItem", "position": 3, "name": item.get("title"), "item": canonical},
],
},
],
}
return page_shell(
title=f"{item.get('title')}|{section_title}|家纺跨境情报",
description=str(item.get("summary") or ""),
canonical=canonical,
body=body,
schema=schema,
site_url=site_url,
section=section,
)
def build_calendar_year(year: int, items: list[dict[str, Any]], site_url: str) -> str:
canonical = absolute(site_url, f"/textile/calendar/{year}/")
selected = sorted(items, key=lambda item: (str(item.get("start_date") or ""), -int(item.get("priority_score") or 0)))
cards = "".join(card_html(item, f"textile/intelligence/calendar/{item['id']}/", "calendar") for item in selected)
markets = len({country for item in selected for country in item.get("affected_countries") or []})
schema = {
"@context": "https://schema.org",
"@type": "CollectionPage",
"name": f"{year} 全球家纺市场备货决策日历",
"description": f"{year} 年全球节日、购物季、体育事件与家纺备货机会,覆盖 {markets} 个国家和地区。",
"url": canonical,
"mainEntity": {
"@type": "ItemList",
"numberOfItems": len(selected),
"itemListElement": [
{
"@type": "ListItem",
"position": index + 1,
"name": item.get("title"),
"url": absolute(site_url, f"/textile/intelligence/calendar/{item['id']}/"),
}
for index, item in enumerate(selected)
],
},
}
body = f"""<section class="seo-hero">
<p class="seo-eyebrow">{markets} 个国家和地区 · {len(selected)} 个精准机会</p>
<h1>{year} 全球家纺市场备货决策日历</h1>
<p>同一节日跨国聚合,按国家、语种、平台与消费场景倒排选品、设计、备货、入仓和营销窗口,并明确区分官方事实与经营分析。</p>
</section>
<nav class="seo-year-nav" aria-label="备货日历年份">
{''.join(f'<a href="/textile/calendar/{year_value}/">{year_value}</a>' for year_value in sorted({int(str(item.get("start_date") or "0")[:4]) for item in selected if str(item.get("start_date") or "")[:4].isdigit()}))}
</nav>
<section class="seo-grid">{cards}</section>"""
return page_shell(
title=f"{year} 全球家纺市场备货决策日历|家纺跨境情报",
description=f"{year} 年全球家纺备货决策日历,覆盖 {markets} 个国家和地区、{len(selected)} 个精准机会,包含文化解释、语种平台、产品设计建议和权威来源。",
canonical=canonical,
body=body,
schema=schema,
site_url=site_url,
section="calendar",
)
def write_text(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8", newline="\n")
def main() -> int:
parser = argparse.ArgumentParser(description="生成家纺情报静态 SEO 页面")
parser.add_argument("--data", default="data/textile-intelligence.json", help="情报 JSON")
parser.add_argument("--root", default=".", help="站点根目录")
parser.add_argument("--site-url", default=DEFAULT_SITE_URL, help="生产站点绝对地址")
args = parser.parse_args()
root = Path(args.root).resolve()
data_path = Path(args.data)
payload = json.loads(data_path.read_text(encoding="utf-8"))
site_url = args.site_url.rstrip("/")
trend_items = sorted(load_trend_items(data_path), key=lambda item: str(item.get("published_at") or ""), reverse=True)
section_items = {
"calendar": list(payload.get("calendar_insights") or payload.get("calendar") or []),
"exhibitions": list(payload.get("exhibitions") or []),
"policies": list(payload.get("policies") or []),
"standards": [*(payload.get("standards") or []), *trend_items],
"platforms": list(payload.get("platforms") or []),
"activities": list(payload.get("activities") or []),
}
for section, items in section_items.items():
if section != "standards":
items.sort(key=lambda item: (str(item.get("start_date") or "9999"), str(item.get("title") or "")))
sitemap_paths = ["textile/intelligence/"]
generated_detail_root = root / "textile" / "intelligence"
for section in SECTION_CONFIG:
target = (generated_detail_root / section).resolve()
if target.parent == generated_detail_root.resolve() and target.exists():
rmtree(target)
all_items_by_id = {
str(item["id"]): item
for items in section_items.values()
for item in items
if item.get("id")
}
detail_jobs: list[tuple[str, str, str]] = []
collection_links: list[str] = []
for section, items in section_items.items():
page, entries = build_collection(section, items, site_url)
write_text(root / "textile" / section / "index.html", page)
sitemap_paths.append(f"textile/{section}/")
collection_links.append(f'<a href="/textile/{section}/"><strong>{esc(SECTION_CONFIG[section][0])}</strong><span>{len(items)} 条数据</span></a>')
for item in items:
detail_jobs.append((section, f"textile/intelligence/{section}/{item['id']}/index.html", item["id"]))
calendar_by_year: dict[int, list[dict[str, Any]]] = {}
for item in section_items["calendar"]:
year = int(str(item.get("start_date") or "0")[:4] or 0)
if year:
calendar_by_year.setdefault(year, []).append(item)
for year, items in sorted(calendar_by_year.items()):
write_text(root / "textile" / "calendar" / str(year) / "index.html", build_calendar_year(year, items, site_url))
sitemap_paths.append(f"textile/calendar/{year}/")
overview_canonical = absolute(site_url, "/textile/intelligence/")
overview_schema = {
"@context": "https://schema.org",
"@type": "CollectionPage",
"name": "家纺跨境情报 · 每日机会与风险提醒",
"description": "每日更新全球备货日历、家纺展会、跨境活动、平台渠道、跨境政策、知识产权和纺织趋势标准。",
"url": overview_canonical,
}
overview_body = f"""<section class="seo-hero">
<p class="seo-eyebrow">{esc(payload.get('source_success'))} 个新增权威源 · 总体 {esc(payload.get('total_source_baseline'))} 源基线</p>
<h1>家纺跨境情报</h1>
<p>每日更新全球备货日历、家纺与纺织展会、跨境活动、平台渠道、跨境政策、知识产权和趋势标准,所有发布数据均保留来源等级与核验时间。</p>
</section><section class="seo-section-links">{''.join(collection_links)}</section>"""
write_text(
root / "textile" / "intelligence" / "index.html",
page_shell(
title="家纺跨境情报|每日机会、风险与产品趋势",
description="每日更新全球备货日历、家纺展会、跨境活动、平台渠道、跨境政策、知识产权和纺织趋势标准。",
canonical=overview_canonical,
body=overview_body,
schema=overview_schema,
site_url=site_url,
),
)
for section, path, item_id in detail_jobs:
item = all_items_by_id[item_id]
write_text(root / path, build_detail(item, section, site_url))
sitemap_paths.append(path.removesuffix("index.html"))
modified = datetime.now(tz=timezone.utc).date().isoformat()
urls = "".join(
f" <url><loc>{esc(absolute(site_url, path))}</loc><lastmod>{modified}</lastmod></url>\n"
for path in sitemap_paths
)
sitemap = f'<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n{urls}</urlset>\n'
write_text(root / "sitemap.xml", sitemap)
write_text(root / "robots.txt", f"User-agent: *\nAllow: /\nSitemap: {absolute(site_url, '/sitemap.xml')}\n")
print(f"静态 SEO 生成完成:入口 {len(SECTION_CONFIG) + 1} 个,详情 {len(detail_jobs)} 个,Sitemap URL {len(sitemap_paths)} 个")
return 0
if __name__ == "__main__":
raise SystemExit(main())