feat: port cms article detail layout into trade-pro (hero/body/related)

Task 10d: rewrite trade-pro article.html from the thin pro-article
skeleton to the cms news/[slug].astro design (pixel baseline
dist/hesupipe/en/news/<slug>/):

- hero reuses the 10c sf-tp-page-hero component (cover as bg image +
  brand-soft overlay) with the breadcrumb inside; the platform contract
  class .pro-article-hero stays on the hero section and the pro-base
  centered/giant-h1 skeleton is flattened by specificity in pro.css
- meta row (category pill - publish_date - author), rich-text body via
  body_copy with a cms rich-body typographic layer, divider, author
  card, next-story navigation
- related band below the content: More News side box (120px thumb rows,
  3 items, self and next-story excluded) + dark side CTA + back link,
  1fr+300px grid collapsing at 900/620
- date field is meta.fields.publish_date (blueprints post definition),
  rendered raw as YYYY-MM-DD
- new responsive rules live in the 10d media blocks appended at the end
  of pro.css to avoid the 10c source-order cascade trap; the 620 hero
  h1 compact size is re-declared at the same specificity because the
  10d no-media h1 reset (0,4,0) outranks the 10c 620 rule (0,3,0)
- tests: test_trade_pro_article_layout_structure builds a site with a
  cover/date/rich-body post plus 4 other posts and asserts hero/meta/
  body/author/next/related structure, .pro-article-hero presence, no
  bare arrow characters

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-23 15:04:07 +08:00
parent ea2ac339e0
commit 1eddcf9136
3 changed files with 234 additions and 1 deletions
+65
View File
@@ -336,6 +336,71 @@ def test_trade_pro_listing_layout_structure(factory):
assert 'sf-tp-pagination' not in cases
def test_trade_pro_article_layout_structure(factory):
# Task 10d:article.html 移植 cms 文章详情设计(news/[slug].astro,像素基准
# dist/hesupipe/en/news/<slug>/)后的结构契约。构建流(/preview/ HTTP)+
# 带 cover/publish_date/富文本正文的文章条目 + 4 条其他文章驱动断言:
# hero(.pro-article-hero 平台契约保留 + 封面底图 + 面包屑)、meta 行
# (分类胶囊 · 发布日期 · 作者)、富文本正文容器、作者卡、下一篇导航、
# More News 相关盒(不含自身、不含下一篇的去重列表)+ 侧栏 CTA + 返回;
# 交互箭头一律 SVG,无裸箭头字符。
import re
sid = site(factory)
assert factory.post(f'/api/factory/sites/{sid}/bootstrap').status_code == 200
made = factory.post(f'/api/sites/{sid}/entries', json={
'type': 'post', 'title': 'HDPE Pipe Export Standards Update', 'route': '/news/hdpe-standards/',
'cover': 'https://imgs.example/hdpe-cover.jpg', 'summary': 'Updated inspection rules for HDPE pipe exports.',
'body': '<p>Inspection requirements for <strong>HDPE</strong> pipe exports.</p><h2>Packaging</h2><p>Bundles are wrapped and strapped.</p>',
'meta': {'fields': {'category': 'Company News', 'publish_date': '2026-10-18', 'author': 'Trade Desk'}}})
assert made.status_code in (200, 201), made.text
for i in range(4):
r = factory.post(f'/api/sites/{sid}/entries', json={
'type': 'post', 'title': f'Worksite Report {i}', 'route': f'/news/worksite-{i}/',
'summary': f'Installation report from worksite {i}.'})
assert r.status_code in (200, 201), r.text
assert factory.put(f'/api/factory/sites/{sid}/theme', json={'theme': 'trade-pro', 'version': '1.0.0'}).status_code == 200
bid = factory.post(f'/api/sites/{sid}/builds', json={}).json()['version']
r = factory.get(f'/preview/{sid}/{bid}/news/hdpe-standards/')
assert r.status_code == 200, r.text[:400]
html = r.text
soup = BeautifulSoup(html, 'html.parser')
# hero:.pro-article-hero 契约保留(test_theme_pro_v3 对所有主题断言)+
# 封面作 hero 底图 + 面包屑(Home › News › 标题)+ 唯一 h1 + reveal
hero = soup.select_one('section.sf-tp-article-hero.pro-article-hero')
assert hero is not None
bg = hero.select_one('.sf-tp-page-hero-bg')
assert bg is not None and 'hdpe-cover.jpg' in bg.get('src', '')
assert hero.select_one('.sf-tp-breadcrumb a[href$="/news/"]')
assert hero.select_one('.sf-tp-page-hero-copy[data-reveal] h1')
assert len(soup.find_all('h1')) == 1
# meta 行:分类胶囊 + 发布日期(time)+ 作者字段
meta = soup.select_one('.sf-tp-article-meta')
assert meta is not None and 'Company News' in meta.get_text()
assert meta.find('time') is not None and '2026-10-18' in meta.find('time').get_text()
assert 'Trade Desk' in meta.get_text()
# 正文:body_copy 富文本容器(加粗与小节标题随文渲染)
body = soup.select_one('.sf-tp-article-body .pro-rich')
assert body is not None and 'HDPE' in body.get_text() and 'Packaging' in body.get_text()
# 分隔线 + 作者卡(首字母头像)
assert soup.select_one('.sf-tp-article-divider') is not None
assert soup.select_one('.sf-tp-article-author .sf-tp-article-avatar') is not None
# 下一篇导航:排除自身后的首条其他文章
nxt = soup.select_one('a.sf-tp-next-story')
assert nxt is not None and 'Worksite Report' in nxt.get_text()
# 相关阅读:More News 盒 3 条(其余文章),不含自身、不与下一篇重复
items = soup.select('.sf-tp-article-side-box a.sf-tp-article-side-item')
assert len(items) == 3
texts = [i.get_text() for i in items]
assert all('HDPE Pipe Export Standards' not in t for t in texts)
assert nxt.get('href') not in [i.get('href') for i in items]
assert items[0].select_one('.sf-tp-card-media img, .sf-tp-card-media .pro-art')
# 侧栏 CTA 指向联系页;返回链接回 /news/
assert soup.select_one('.sf-tp-article-side-cta a.sf-tp-btn[href$="/contact/"]')
assert soup.select_one('a.sf-tp-article-back[href$="/news/"]')
# 交互箭头一律 SVG(Task 9 契约),链接文本无裸箭头字符
assert not re.search(r'[←→↗↘⌕▾]', ''.join(a.get_text() for a in soup.find_all('a')))
def test_frontend_entries_include_trade_pro():
from pathlib import Path
base = Path(__file__).resolve().parent.parent