feat(build): astro 构建管线与媒体落盘

Plan 4 Task 5:build.service snapshotting 后接真实构建——building 态进
pipeline.ts(spawn `pnpm --filter @sf/theme-pilot-minimal exec astro build`
(SF_BUILD_INPUT env;非零退出+stderr 标记判据)→ dist 定位与 target_file
完整性 → 媒体 copyPath→dist/public_path(sha256 全量校验)→ sitemap.xml/
robots.txt(编码 URL 原样)→ route-manifest.json(§12.1 字段面、哈希链复用)
→ build_artifacts dist/manifest 登记)→ validating(空,Task 6) → 报告+report
行 → succeeded;失败任何一步 failed+error_code/error_summary(stderr 摘要)
+input/dist 清残。

dist 双位置纪律:主题包 dist 是暂存,产物汇齐后拷出 <artifactsRoot>/jobs/
<jobId>/dist(job 级权威),暂存随即清空。无网证明:ASTRO_TELEMETRY_DISABLED=1
(实测 @astrojs/telemetry 构建期真调 fetch)+ NODE_OPTIONS 透传(测试注入
fetch/net stub 构建仍成功)。主题集成补面:ProductList 模板 + /products/
主列表分派特判(DB 物化形状 targetContentId=null,Task 4 夹具曾被占位绕过)。

测试:tests/integration/build-pipeline.test.ts 五用例(全链/夹具直跑/五禁
断言/失败路径);build-snapshot 状态机补 building + 清场补 build_artifacts。
root 359 + theme 26 全绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-22 10:52:13 +08:00
parent 107642c1b3
commit 1ed3ac062a
8 changed files with 1200 additions and 30 deletions
@@ -0,0 +1,47 @@
---
// 产品主列表模板(Plan 4 Task 5 集成补面):route-writer finish() 对 productsPath(默认
// /products/)落的主列表行 targetContentId=null——Task 4 的分派器只覆盖了 news 主列表
// 特判,产品主列表在主题夹具里被"targetContentId 占位"绕过(见 CategoryList 头注记),
// DB 真实物化形状(targetContentId=null)即无处落点。本模板补齐:全部 product 卡片
// 单页(latestEntries 同款排序),无落点内容不出卡(禁虚构路径)。
// 与 news-list 同构(主列表两兄弟);manifest 的 'product-list' 模板键声明性覆盖本页
// (appliesTo 单字符串不承载多前缀——/products/ 主列表由分派器特判,/category/ 别名
// 前缀同先例)。
import BaseLayout from '../layouts/BaseLayout.astro'
import Breadcrumbs from '../components/Breadcrumbs.astro'
import ProductCard from '../components/ProductCard.astro'
import type { RenderData, RouteEntry } from '../lib/load-render-data'
import { contentTitle, coverRef, latestEntries, metaDescription, routeHrefFor } from '../lib/ui'
interface Props {
data: RenderData
route: RouteEntry
}
const { data, route } = Astro.props
const page = { path: route.routePath, title: 'Products' }
const allProducts = latestEntries(data.contents, 'product')
---
<BaseLayout data={data} page={{ ...page, description: metaDescription(data.site, page) }}>
<Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'Products' }]} />
<h1>Products</h1>
<div class="product-grid">
{allProducts.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>