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:
+15
-10
@@ -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]:
|
||||
|
||||
Reference in New Issue
Block a user