feat(theme): 六类页面渲染与路由分派
单一动态路由 [...slug].astro 读 routes.json html 纯路径行分派六模板 (home/typeKey 详情/分类前缀列表/news 主列表),无法分派即构建明错; 404 独立静态页。夹具扩展为六页集(真实 hesupipe 抽样形状脱敏化: knownFields.postDate/title、media 块 attachmentRefs.* fieldPath、 /products/ 内容页占据、编码路径 + .html 字面路径防御)。 字面 .html 路径对齐钩子(astro.config 内联 integration):Astro 页面 产物在 directory/file/preserve 三态均产不出 dist/news/x.html 字面文件, 构建收尾把包装目录折平为字面文件——target_file 契约对齐。 构建级测试 8 例(render-cases):全量映射/编码字节原样/首页截断/ 产品参数相册/新闻字面 .html/列表两类/SEO 基线。372 → 380。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
---
|
||||
// 分类列表模板(快线 §8.1 第 4 序;前置事实:/products/ 被内容页占据——产品列表演示
|
||||
// 走 /product-category/<term>/)。Breadcrumbs(首页 › 分类)+ term 名标题 + 该 term 关联
|
||||
// 产品卡网格。关联面:taxonomies.json 的 term.contentEntryIds(content_terms 物化)反查
|
||||
// contents——term 的 objectType 决定内容类型(product-category → product;/category/ 新闻
|
||||
// 分类落 NewsCard 的扩展是 Plan 5 域,V1 按产品分类演示)。无落点内容不出卡(禁虚构路径)。
|
||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import Breadcrumbs from '../components/Breadcrumbs.astro'
|
||||
import ProductCard from '../components/ProductCard.astro'
|
||||
import type { ContentEntry, RenderData, RouteEntry, TaxonomyTerm } from '../lib/load-render-data'
|
||||
import { contentTitle, coverRef, metaDescription, routeHrefFor } from '../lib/ui'
|
||||
|
||||
interface Props {
|
||||
data: RenderData
|
||||
route: RouteEntry
|
||||
term: TaxonomyTerm
|
||||
}
|
||||
|
||||
const { data, route, term } = Astro.props
|
||||
const page = { path: route.routePath, title: term.name }
|
||||
const products = term.contentEntryIds
|
||||
.map((id) => data.contents.find((c) => c.contentId === id))
|
||||
.filter((c): c is ContentEntry => c !== undefined && c.typeKey === 'product')
|
||||
---
|
||||
|
||||
<BaseLayout data={data} page={{ ...page, description: metaDescription(data.site, page) }}>
|
||||
<Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: term.name }]} />
|
||||
<h1>{term.name}</h1>
|
||||
<div class="product-grid">
|
||||
{products.map((c) => {
|
||||
const href = routeHrefFor(data.routes, c.contentId)
|
||||
if (href === undefined) return null
|
||||
return (
|
||||
<ProductCard title={contentTitle(c)} href={href} cover={coverRef(c, data.assets)} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.product-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
// 普通内容页模板(快线 §8.1 第 3 序):h1 标题 + RichText 正文。
|
||||
// basic_page(及未知 typeKey 的内容页——通用呈现即忠实呈现,无静默)落此。
|
||||
// 信任链:bodyHtml 已过导入清洗 + URL 重写,set:html 直出不二次清洗(ADR-018,Task 9 落笔)。
|
||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import RichText from '../components/RichText.astro'
|
||||
import type { ContentEntry, RenderData, RouteEntry } from '../lib/load-render-data'
|
||||
import { contentBody, contentDescription, contentTitle, metaDescription } from '../lib/ui'
|
||||
|
||||
interface Props {
|
||||
data: RenderData
|
||||
route: RouteEntry
|
||||
content: ContentEntry
|
||||
}
|
||||
|
||||
const { data, route, content } = Astro.props
|
||||
const page = {
|
||||
path: route.routePath,
|
||||
title: contentTitle(content),
|
||||
description: contentDescription(content),
|
||||
}
|
||||
const body = contentBody(content)
|
||||
---
|
||||
|
||||
<BaseLayout data={data} page={{ ...page, description: metaDescription(data.site, page) }}>
|
||||
<article>
|
||||
<h1>{page.title}</h1>
|
||||
{body !== undefined && <RichText html={body} />}
|
||||
</article>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
// 首页模板(快线 §8.1 第 2 序):hero(site name)+ 最新 3 新闻卡 + 最新 3 产品卡。
|
||||
// 数据经 Props 注入([...slug] 分派层调用 loadRenderData 后下传——单一入口纪律)。
|
||||
// 截断纪律:latestEntries 按 knownFields.postDate 降序取 3(分页 V1 不做,首页恒 3 条——
|
||||
// 路由不登记分页路径会自破门禁①死链,ADR-018 移交 Plan 5)。无落点内容不出卡
|
||||
// (routeHrefFor undefined → 跳过——不得虚构路径)。
|
||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import NewsCard from '../components/NewsCard.astro'
|
||||
import ProductCard from '../components/ProductCard.astro'
|
||||
import type { RenderData, RouteEntry } from '../lib/load-render-data'
|
||||
import {
|
||||
contentDate,
|
||||
contentTitle,
|
||||
coverRef,
|
||||
latestEntries,
|
||||
metaDescription,
|
||||
routeHrefFor,
|
||||
} from '../lib/ui'
|
||||
|
||||
interface Props {
|
||||
data: RenderData
|
||||
route: RouteEntry
|
||||
}
|
||||
|
||||
const { data, route } = Astro.props
|
||||
const page = { path: route.routePath }
|
||||
const latestNews = latestEntries(data.contents, 'news_article', 3)
|
||||
const latestProducts = latestEntries(data.contents, 'product', 3)
|
||||
---
|
||||
|
||||
<BaseLayout data={data} page={{ ...page, description: metaDescription(data.site, page) }}>
|
||||
<section class="hero">
|
||||
<h1>{data.site.name}</h1>
|
||||
</section>
|
||||
|
||||
<section aria-label="最新动态" class="home-section">
|
||||
<h2>News</h2>
|
||||
<div class="news-list">
|
||||
{latestNews.map((c) => {
|
||||
const href = routeHrefFor(data.routes, c.contentId)
|
||||
if (href === undefined) return null
|
||||
return <NewsCard title={contentTitle(c)} date={contentDate(c)} href={href} />
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section aria-label="最新产品" class="home-section">
|
||||
<h2>Products</h2>
|
||||
<div class="product-grid">
|
||||
{latestProducts.map((c) => {
|
||||
const href = routeHrefFor(data.routes, c.contentId)
|
||||
if (href === undefined) return null
|
||||
return (
|
||||
<ProductCard
|
||||
title={contentTitle(c)}
|
||||
href={href}
|
||||
cover={coverRef(c, data.assets)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
padding-block: var(--space-5);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.hero h1 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.home-section {
|
||||
margin-block: var(--space-4);
|
||||
}
|
||||
.home-section h2 {
|
||||
font-size: 1.25rem;
|
||||
margin: 0 0 var(--space-2);
|
||||
}
|
||||
.product-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
// 新闻详情模板(快线 §8.1 第 6 序):h1 标题 + 日期(knownFields.postDate——真实数据日期
|
||||
// 字段探测结论;缺席省略 time 块)+ 正文 RichText(URL 已根相对化重写)。
|
||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import RichText from '../components/RichText.astro'
|
||||
import type { ContentEntry, RenderData, RouteEntry } from '../lib/load-render-data'
|
||||
import { contentBody, contentDate, contentDescription, contentTitle, metaDescription } from '../lib/ui'
|
||||
|
||||
interface Props {
|
||||
data: RenderData
|
||||
route: RouteEntry
|
||||
content: ContentEntry
|
||||
}
|
||||
|
||||
const { data, route, content } = Astro.props
|
||||
const page = {
|
||||
path: route.routePath,
|
||||
title: contentTitle(content),
|
||||
description: contentDescription(content),
|
||||
}
|
||||
const date = contentDate(content)
|
||||
const body = contentBody(content)
|
||||
---
|
||||
|
||||
<BaseLayout data={data} page={{ ...page, description: metaDescription(data.site, page) }}>
|
||||
<article>
|
||||
<h1>{page.title}</h1>
|
||||
{date !== undefined && <time datetime={date}>{date}</time>}
|
||||
{body !== undefined && <RichText html={body} />}
|
||||
</article>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
time {
|
||||
display: block;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.875rem;
|
||||
margin-block: var(--space-1) var(--space-3);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
// 新闻列表模板(快线 §8.1 第 6 序):全部 news_article 卡片单页(分页 V1 一律不做——
|
||||
// 分页路由不在 routes.json 会自破门禁①死链;触发条件与路由登记义务记 ADR-018 移交 Plan 5)。
|
||||
// 排序 latestEntries(postDate 降序);无落点内容不出卡(禁虚构路径)。
|
||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import NewsCard from '../components/NewsCard.astro'
|
||||
import type { RenderData, RouteEntry } from '../lib/load-render-data'
|
||||
import { contentDate, contentTitle, latestEntries, metaDescription, routeHrefFor } from '../lib/ui'
|
||||
|
||||
interface Props {
|
||||
data: RenderData
|
||||
route: RouteEntry
|
||||
}
|
||||
|
||||
const { data, route } = Astro.props
|
||||
const page = { path: route.routePath, title: 'News' }
|
||||
const allNews = latestEntries(data.contents, 'news_article')
|
||||
---
|
||||
|
||||
<BaseLayout data={data} page={{ ...page, description: metaDescription(data.site, page) }}>
|
||||
<h1>News</h1>
|
||||
<div class="news-list">
|
||||
{allNews.map((c) => {
|
||||
const href = routeHrefFor(data.routes, c.contentId)
|
||||
if (href === undefined) return null
|
||||
return <NewsCard title={contentTitle(c)} date={contentDate(c)} href={href} />
|
||||
})}
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
// 产品详情模板(快线 §8.1 第 5 序):h1 标题 + 相册(media 引用列表——featured 首图 +
|
||||
// gallery[N] 按序号)+ 参数表(data.specifications 纯对象转表——pilot 约定键,真实 hesupipe
|
||||
// 产品修订无结构化参数对象,缺省容忍;extras.phpRaw 明确忽略不渲染)+ 正文 RichText。
|
||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import ResponsiveMedia from '../components/ResponsiveMedia.astro'
|
||||
import RichText from '../components/RichText.astro'
|
||||
import type { ContentEntry, RenderData, RouteEntry } from '../lib/load-render-data'
|
||||
import {
|
||||
contentBody,
|
||||
contentDescription,
|
||||
contentTitle,
|
||||
mediaGallery,
|
||||
metaDescription,
|
||||
specificationRows,
|
||||
} from '../lib/ui'
|
||||
|
||||
interface Props {
|
||||
data: RenderData
|
||||
route: RouteEntry
|
||||
content: ContentEntry
|
||||
}
|
||||
|
||||
const { data, route, content } = Astro.props
|
||||
const page = {
|
||||
path: route.routePath,
|
||||
title: contentTitle(content),
|
||||
description: contentDescription(content),
|
||||
}
|
||||
const gallery = mediaGallery(content, data.assets)
|
||||
const specs = specificationRows(content)
|
||||
const body = contentBody(content)
|
||||
---
|
||||
|
||||
<BaseLayout data={data} page={{ ...page, description: metaDescription(data.site, page) }}>
|
||||
<article class="product-detail">
|
||||
<h1>{page.title}</h1>
|
||||
|
||||
{gallery.length > 0 && (
|
||||
<div class="gallery">
|
||||
{gallery.map((m) => (
|
||||
<ResponsiveMedia src={m.src} alt={m.alt} width={m.width} height={m.height} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{specs.length > 0 && (
|
||||
<table class="specifications">
|
||||
<tbody>
|
||||
{specs.map(([k, v]) => (
|
||||
<tr>
|
||||
<th scope="row">{k}</th>
|
||||
<td>{v}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
||||
{body !== undefined && <RichText html={body} />}
|
||||
</article>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: var(--space-2);
|
||||
margin-block: var(--space-3);
|
||||
}
|
||||
.specifications {
|
||||
border-collapse: collapse;
|
||||
margin-block: var(--space-3);
|
||||
}
|
||||
.specifications th,
|
||||
.specifications td {
|
||||
border: 1px solid var(--color-border);
|
||||
padding: var(--space-1) var(--space-2);
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user