feat(about): 块式页面构建器——九类区块 + 三级回落零回归(5g T1/T2)

- schema:siteAbout 增 layout{showIdCard} + blocks(z.discriminatedUnion
  九类:prose/tracks/cards/timeline/quote/figure/linklist/cta/divider,
  图片字段走 Astro image());对未知块型用 z.union([knownUnion,
  z.looseObject({type})]) 复合——已知块严格校验,未知块落宽松分支再由
  访问器 WARN+丢弃(容错与严格校验的张力取舍)
- page-blocks.ts:enabled:false 过滤、未知 type WARN+skip、pickBi 回落
  zh 并 WARN、上限(块≤24/单块文本≤4000,超限截断并 WARN)
- BlockRenderer 统一外壳 `<section class="blk" data-block={type}>`(E2E
  抓手)+ 九个块组件(样式自 AboutPage 原 <style> 搬迁,只吃既有 token)
- AboutPage 三级回落:blocks → intro markdown → 内置默认;legacy
  tracks/build 段整体挪入回退分支(文案逐字一致,零回归)
- 图标白名单 src/lib/icons.ts(12 键,未登记回退首字占位)
- 后台 about 单例四段结构(blocks/layout/intro/typography),块为
  list+types+typeKey,每块带「显示」开关与中文标签

协调报备:车道修了一处跨车道插入事故(T3 把 typography 插进了 intro
的 widget 内导致字段错位),重写为四段正确结构;并发构建擦 dist 的
假失败改用「build→快照 dist→快照起服务跑 E2E」规避(37/37 稳定)

验证: 构建 48 页 exit 0;E2E 37/37(快照口径);九类块顺序与配置一致、
      figure 走 /_astro/*.webp 管线、未知块与外链图被拦并 WARN、
      终版 3 块零回归、en 同构、admin about 四段字段齐全

Spec: 2026-11-02_blog-5g-pages (T1/T2)

Co-authored-by: GLM-5.3
This commit is contained in:
wait4xandGLM-5.3 committed 2026-11-02 18:21:33 +08:00
1 parent 6096a9dafe
commit 2f7cbcf7bd
16 files changed
+1289 -72

No files matched your search

+122 -2
View File
@@ -18,6 +18,92 @@ export type PostTrack = (typeof POST_TRACKS)[PostCategory];
/** 双语文案字段:两语言各自可选,缺一回落一(回退逻辑在 src/lib/site-content.ts) */
const bilingual = z.object({ zh: z.string().optional(), en: z.string().optional() });
/** 块型清单(5g 页面构建器 v1 九类):访问器据此判未知 type */
export const BLOCK_TYPES = [
'prose',
'tracks',
'cards',
'timeline',
'quote',
'figure',
'linklist',
'cta',
'divider',
] as const;
type ImageHelper = (opts?: unknown) => z.ZodTypeAny;
/** 关于页块型 union:图片字段强制 image()(外链字符串在 schema 层即失败,永不渲染) */
const aboutBlockUnion = (image: ImageHelper) =>
z.discriminatedUnion('type', [
z.object({
type: z.literal('prose'),
enabled: z.boolean().optional(),
title: bilingual.optional(),
body: bilingual,
width: z.enum(['narrow', 'full']).optional(),
}),
z.object({
type: z.literal('tracks'),
enabled: z.boolean().optional(),
title: bilingual,
items: z.array(
z.object({ icon: z.string().optional(), name: bilingual, desc: bilingual.optional() }),
),
}),
z.object({
type: z.literal('cards'),
enabled: z.boolean().optional(),
title: bilingual.optional(),
columns: z.union([z.literal(2), z.literal(3)]).optional(),
items: z.array(
z.object({
title: bilingual,
desc: bilingual.optional(),
href: z.string().optional(),
tag: bilingual.optional(),
}),
),
}),
z.object({
type: z.literal('timeline'),
enabled: z.boolean().optional(),
title: bilingual.optional(),
items: z.array(z.object({ time: z.string(), title: bilingual, desc: bilingual.optional() })),
}),
z.object({
type: z.literal('quote'),
enabled: z.boolean().optional(),
text: bilingual,
cite: bilingual.optional(),
}),
z.object({
type: z.literal('figure'),
enabled: z.boolean().optional(),
image: image(),
alt: z.string().optional(),
caption: bilingual.optional(),
}),
z.object({
type: z.literal('linklist'),
enabled: z.boolean().optional(),
title: bilingual.optional(),
items: z.array(z.object({ label: bilingual, href: z.string(), note: bilingual.optional() })),
}),
z.object({
type: z.literal('cta'),
enabled: z.boolean().optional(),
text: bilingual,
href: z.string(),
label: bilingual,
}),
z.object({
type: z.literal('divider'),
enabled: z.boolean().optional(),
style: z.enum(['line', 'space']).optional(),
}),
]);
export function trackOf(category: PostCategory): PostTrack {
return POST_TRACKS[category];
}
@@ -82,6 +168,14 @@ export const collections = {
z.object({
tagline: bilingual.optional(),
avatar: image().optional(),
/** 页面级排版覆盖(层 2):正文默认字体类 / 强调色类 / 字号缩放;不配置即全站默认 */
typography: z
.object({
font: z.string().optional(),
accent: z.string().optional(),
scale: z.number().optional(),
})
.optional(),
/** 站点图标:public 下的路径(.svg/.ico 直出,不经图片管线);缺省回退 /favicon.svg */
favicon: z.string().optional(),
/** 副标题(markdown 短文本,渲染在身份语下);空 = 不渲染 */
@@ -113,14 +207,40 @@ export const collections = {
}),
siteAbout: defineCollection({
loader: glob({ pattern: 'about.yaml', base: './content/site' }),
// intro = 多段 markdown 正文(页面用 createMarkdownProcessor 渲染),字段级空值回落默认
schema: z.object({ intro: bilingual.optional() }),
// intro = 多段 markdown 正文(二级回退:blocks 为空时才用)
// blocks = 块式页面构建器(5g):文本一律 {zh,en},图片字段必须 image();未知 type 落宽松分支
// → src/lib/page-blocks.ts warn + skip,保证后台填错块型不炸构建
schema: ({ image }) =>
z.object({
intro: bilingual.optional(),
/** 页面级排版覆盖(层 2,T3 车道):正文默认字体类 / 强调色类 / 字号缩放 */
typography: z
.object({
font: z.string().optional(),
accent: z.string().optional(),
scale: z.number().optional(),
})
.optional(),
/** 版式:是否渲染名片区(头像/名字/社交),默认 true */
layout: z.object({ showIdCard: z.boolean().optional() }).optional(),
blocks: z
.array(z.union([aboutBlockUnion(image), z.looseObject({ type: z.string() })]))
.optional(),
}),
}),
siteOpenSource: defineCollection({
loader: glob({ pattern: 'opensource.yaml', base: './content/site' }),
schema: z.object({
blurb: bilingual.optional(),
blurb_after: bilingual.optional(),
/** 页面级排版覆盖(层 2):正文默认字体类 / 强调色类 / 字号缩放;不配置即全站默认 */
typography: z
.object({
font: z.string().optional(),
accent: z.string().optional(),
scale: z.number().optional(),
})
.optional(),
repo_url: z.string().optional(),
repo_label: z.string().optional(),
/** 置顶仓短名,顺序即展示顺序(原 projects.ts 硬编码名单迁于此) */