Task 15: product detail anchor tabs + per-product custom sections

User report: product detail page was wrong — section titles are
per-product custom data (CMS wppop_sections migration: Packing &
Delivery / Our Exhibition / Our Certifications / Our Company / FAQ),
not fixed headings; they must drive an anchor tab system like the
CMS [locale]/products/[slug].astro detailTabs.

Data side (both layers, task option 1 + 2):

- theme_pro.product_detail_sections: fields.sections
  [{title, content}] JSON is the structured source; legacy sync
  output that appended sections as trailing body H2 blocks is
  recognized by known/data titles and split out (overview keeps the
  rest; same-title blocks dedupe to the structured version; Features
  body block drops when fields.features renders it)
- sync_cms_data.build_product: store sections/features as structured
  fields JSON, stop appending H2 blocks to the body; stats now count
  entries with a sections field
- Specifications table no longer leaks structural JSON field keys
  (sections/features/specs/gallery/categories) — scalar spec keys
  only

Template side (trade-pro product.html, replaces 12a panel
switching):

- tab sequence is fully data-driven and matches the CMS: Product
  Details (always) -> custom sections (data titles) ->
  Specifications / Features (shown only when data exists)
- tabs are #anchor links, not panel toggles: all sections render
  flat with visible titles (readable and crawlable without JS; native
  anchor jump), JS adds smooth scrolling + scrollspy highlight +
  tab centering in the horizontal scroll row
- section ids are slug-derived (sec-packing-delivery), duplicate
  titles get an index suffix; features grid ports the CMS
  detail-feature-row

Tests: test_trade_pro_product_layout_structure rewritten for the
anchor-tab contract — sections via fields, legacy body-H2 fallback,
spec/features conditional tabs, no-sections product showing only
Product Details + Specifications, spec table free of JSON key leaks,
updated pro.css needles. 30 passed; the 1 failure
(test_trade_pro_blocks_visual_bridge) fails identically on the
previous commit (pre-existing, unrelated).

Co-Authored-By: Claude Code <noreply@example.com>
This commit is contained in:
浅念
2026-10-29 13:49:08 +08:00
parent 3ce4292794
commit f5dc2f7624
5 changed files with 311 additions and 84 deletions
+15 -10
View File
@@ -16,6 +16,10 @@ SITE 常量(字面量,直接文本解析)。
- 正文复刻 wp-text.ts 的 cleanWpHtml/wrapInlineInP/htmlToExcerpt 清洗
WordPress 残留(Gutenberg 注释、caption 短码、表单区块、H1 降级等),
SiteForge 侧 bleach 白名单再剥离 aligncenter 等类名/内联样式;
- 产品详情区块(CMS wppop_sections 迁移,每产品自定义标题)以
[{title, content}] JSON 存 fields.sections,不追加进 body——主题层渲染
为锚点 Tab 区块(theme_pro.product_detail_sections;旧同步产物追加进
body 的 H2 段由主题层识别拆出);
- 站点配置:site.ts 的 name/tagline + locale=en(PUT /api/i18n/.../locale)。
用法:
@@ -341,14 +345,11 @@ def build_product(slug: str, entry: dict, media: MediaSync) -> dict:
if isinstance(c, dict) and _s(c.get("slug"))
]
body_html = raw_desc
for sec in sections:
body_html += f"\n\n<h2>{sec['title']}</h2>\n{sec['content']}"
if features:
body_html += "\n\n<h2>Features</h2>\n"
for f in features:
body_html += f"\n<h3>{f['title']}</h3>\n<p>{f['desc']}</p>\n"
body_html = media.rewrite_body(body_html)
# 详情区块/特性存结构化 fields(Task 15):正文 body 只放 description,
# sections/features 以 [{title, content|desc}] JSON 进 fields——主题层
# (theme_pro.product_detail_sections)据此渲染每产品自定义锚点 Tab 区块,
# 不再把区块标题追加进 body(旧产物由主题层按 body H2 段识别拆出)。
body_html = media.rewrite_body(raw_desc)
fields: dict = {}
if _s(data.get("brand")):
@@ -357,6 +358,8 @@ def build_product(slug: str, entry: dict, media: MediaSync) -> dict:
fields["color"] = _s(data["color"])
if _s(data.get("standard")):
fields["standard"] = _s(data["standard"])
if sections:
fields["sections"] = json.dumps(sections, ensure_ascii=False)
if categories:
fields["category"] = categories[0]["name"]
fields["categories"] = json.dumps(categories, ensure_ascii=False)
@@ -720,7 +723,7 @@ def main() -> int:
final = sf.entries(args.site)
per_type: dict[str, int] = {}
cover_media = gallery_fields = spec_fields = 0
cover_media = gallery_fields = spec_fields = sections_fields = 0
for e in final:
per_type[e["type"]] = per_type.get(e["type"], 0) + 1
f = (e.get("meta") or {}).get("fields") or {}
@@ -730,6 +733,8 @@ def main() -> int:
gallery_fields += 1
if f.get("specs"):
spec_fields += 1
if f.get("sections"):
sections_fields += 1
print("\n===== 同步统计 =====")
for t in types:
@@ -738,7 +743,7 @@ def main() -> int:
f"updated={s['updated']} failed={s['failed']} site_total={per_type.get(t, 0)}")
print(f"媒体上传:{media.uploads} 张(缓存命中 {media.hits})")
print(f"全站 media 封面:{cover_media} | 含 gallery 字段:{gallery_fields} "
f"| 含 specs 字段:{spec_fields}")
f"| 含 specs 字段:{spec_fields} | 含 sections 字段:{sections_fields}")
if media.failures:
print(f"媒体失败 {len(media.failures)} 项(前 10):")
for f in media.failures[:10]: