#!/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(" 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""" {esc(title)}
三笙AI情报局
{body}
""" def tags_html(values: list[str] | tuple[str, ...] | None) -> str: return "".join(f'{esc(value)}' 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'{esc(label)}' ) 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"""

{esc(item.get('decision_label') or '经营观察')} · {esc(item.get('planning_stage'))}

备货窗口 {esc(item.get('stocking_start'))} 至 {esc(item.get('stocking_end'))}

{tags_html(item.get('affected_countries'))}{tags_html(item.get('product_suggestions'))}
""" elif section == "policies": extra = f'
{tags_html(item.get("affected_markets"))}{tags_html(item.get("platforms"))}
' elif section == "platforms": extra = f'

{esc(item.get("access_level"))} {esc(item.get("access_label"))} · {esc(item.get("confidence"))}

{tags_html(item.get("markets"))}{tags_html(item.get("fit_sellers"))}
' elif section == "activities": extra = f'

{esc(item.get("source_level"))} · {esc(item.get("activity_type"))} · {esc(item.get("format"))}

{tags_html(item.get("platforms"))}
' return f"""

{esc(meta)}

{esc(item.get('title'))}

{esc(item.get('summary'))}

{extra} {esc(item.get('authority'))} · {esc(item.get('confidence'))}可信度
""" 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"""

每日更新 · 权威源验证

{esc(title)}

{esc(description)}。当前静态收录 {len(selected)} 条,可直接被搜索引擎读取。

快速答案

{esc(answer_first(section))}

{''.join(cards)}
""" 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'{esc(item.get(' if image else f'
{esc(section_title)}{esc(item.get("title"))}{esc(item.get("region"))}
' ) section_details = "" original_title = str(item.get("title_original") or "") original_html = ( f'

{esc(original_title)}

' 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"
  • {esc(row.get('platform'))}:{esc(row.get('action'))}
  • " for row in item.get("platform_actions") or [] ) section_details = f"""

    {esc(item.get('decision_label') or '经营观察')} · {esc(item.get('planning_stage'))}

    这个节日是什么

    {esc(item.get('what_it_is') or item.get('summary'))}

    重点国家/地区
    {tags_html(item.get('affected_countries'))}
    法定适用范围

    {esc(';'.join(scope_lines) or '以官方来源公布范围为准')}

    主要语种
    {tags_html(item.get('languages'))}
    相关平台
    {tags_html(item.get('platforms'))}
    消费场景
    {tags_html(item.get('consumption_scenes'))}

    家纺备货与设计机会

    {esc(item.get('stocking_start'))} 至 {esc(item.get('stocking_end'))} · {esc(item.get('next_action') or item.get('opportunity'))}

    {tags_html(item.get('product_suggestions'))}

    设计方向:{esc(';'.join(item.get('design_opportunities') or []))}

    文化与表达注意:{esc(';'.join(item.get('cultural_avoidances') or []))}

    {f'

    平台经营动作

    ' if platform_rows else ''}

    事实与分析边界

    官方事实:日期、适用国家和节日名称由来源核验。系统分析:消费场景、产品方向、设计建议和备货节奏属于经营辅助判断。

    """ 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"""

    {esc(item.get('attendance_recommendation') or '先评估是否值得参加')}

    {esc(item.get('attendance_reason') or item.get('opportunity'))}

    {tags_html(item.get('event_focus'))}
      {''.join(f'
    1. {esc(value)}
    2. ' for value in item.get('trip_checklist') or [])}

    机器翻译仅用于辅助阅读;报名、验证码、付款和资料提交请回到官方域名完成。

    """ 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"""

    发生了什么

    {esc(item.get('change_summary') or item.get('summary'))}

    不处理的后果:{esc(item.get('consequence'))}

    {esc(item.get('policy_type'))} · {esc(item.get('change_status'))} · 生效日期 {esc(item.get('effective_at') or '以官方原文为准')}

    适用市场与平台

    {tags_html(item.get('affected_markets'))}{tags_html(item.get('platforms'))}
    """ 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"""

    家纺类目与入驻判断

    {esc(item.get('access_level'))} {esc(item.get('access_label'))} · {esc(item.get('cross_border'))}

    {esc(item.get('textile_category'))}

    {tags_html(item.get('markets'))}{tags_html(item.get('fit_sellers'))}
    """ 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"""

    活动价值与参加方式

    {esc(item.get('source_level'))} · {esc(item.get('activity_type'))} · {esc(item.get('format'))} · {esc(item.get('location'))}

    {esc(item.get('next_action'))}

    {tags_html(item.get('platforms'))}

    行业媒体只用于发现活动;日期、议程、费用和报名状态以主办方或平台官方页面为准。机器翻译仅辅助阅读,正式提交请回原网站。

    """ 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"""

    这条趋势如何使用

    该内容属于行业新闻或设计灵感参考,可用于选色、选材、图案和市场判断;打样与合规决策仍需结合产品规格及官方标准。

    内容类别:{esc(item.get('category') or '趋势新闻')}

    """ else: tasks = "".join(f"
  • {esc(value)}
  • " for value in item.get("brief_items") or []) section_details = f"""

    产品开发与检测动作

    {esc(item.get('application') or item.get('decision_note') or item.get('summary'))}

    {f'
      {tasks}
    ' if tasks else ''}
    """ body = f"""

    先看结论

    {esc(answer_first(section, item))}

    机器翻译仅用于辅助阅读;报名、付款、合规提交和日期确认以官方原文为准。

    {esc(item.get('authority'))} · {esc(item.get('confidence'))}可信度

    {esc(item.get('title'))}

    {original_html}

    {esc(item.get('summary'))}

    {visual}
    时间
    {esc(date_label(item))}
    区域
    {esc(item.get('region'))}
    来源
    {esc(item.get('source'))}
    核验
    {esc(str(item.get('verified_at') or '')[:19].replace('T', ' '))}
    {section_details}

    家纺行动建议

    {esc(action)}

    打开官方来源
    """ 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"""

    {markets} 个国家和地区 · {len(selected)} 个精准机会

    {year} 全球家纺市场备货决策日历

    同一节日跨国聚合,按国家、语种、平台与消费场景倒排选品、设计、备货、入仓和营销窗口,并明确区分官方事实与经营分析。

    {cards}
    """ 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'{esc(SECTION_CONFIG[section][0])}{len(items)} 条数据') 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"""

    {esc(payload.get('source_success'))} 个新增权威源 · 总体 {esc(payload.get('total_source_baseline'))} 源基线

    家纺跨境情报

    每日更新全球备货日历、家纺与纺织展会、跨境活动、平台渠道、跨境政策、知识产权和趋势标准,所有发布数据均保留来源等级与核验时间。

    """ 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" {esc(absolute(site_url, path))}{modified}\n" for path in sitemap_paths ) sitemap = f'\n\n{urls}\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())