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:
1 parent
6096a9dafe
commit
2f7cbcf7bd
16 files changed
+1289
-72
No files matched your search
+122
-2
@@ -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 硬编码名单迁于此) */
|
||||
|
||||
Reference in new issue
Block a user