feat(content): 三集合 schema 与 EXIF 构建期补全(T4 车道收编)

- src/content.config.ts(Astro 7 唯一合法路径,旧 config.ts 会抛
  LegacyContentConfigError——车道实测发现,经批复的边界修正)
- posts(category→track 派生单源)/moments(text-image 互斥 refine)/
  photos(json sidecar + jpg,exif 构建期 exifr 补全、frontmatter 可覆盖)
- 样例:书评+技术文各 1、瞬间 3、照片 3(横竖方三比例,EXIF 三路径覆盖)
- 附带 scripts/check-responsive.sh(T9 预置,随 T6 产物生效)
- 车道 30min 超时于过度验证循环,产物完好,主会话收编续跑

验证: pnpm astro build exit 0(1.63s,含三集合加载)

Spec: 2026-10-24_add-blog-skeleton
Co-authored-by: GLM-5.3
This commit is contained in:
wait4xandGLM-5.3 committed 2026-10-24 23:36:59 +08:00
1 parent 725bfde21f
commit 80ff67e8fa
18 files changed
+370 -2

No files matched your search

+94
View File
@@ -0,0 +1,94 @@
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
import { photosWithExifLoader, type PhotoExif } from './lib/exif';
/**
* posts 的 track(首页分轨标签)由 category 派生,不接受 frontmatter 直写,
* 单一事实源在此映射,消费端用 trackOf(entry.data.category)。
*/
export const POST_TRACKS = {
reading: '阅读',
tech: '技术',
photo: '照片',
} as const;
export type PostCategory = keyof typeof POST_TRACKS;
export type PostTrack = (typeof POST_TRACKS)[PostCategory];
export function trackOf(category: PostCategory): PostTrack {
return POST_TRACKS[category];
}
const postsSchema = z.object({
title: z.string(),
pubDate: z.coerce.date(),
category: z.enum(['reading', 'tech', 'photo']),
tags: z.array(z.string()),
/** 情绪色,仅书评轨使用;#RRGGBB */
mood: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
draft: z.boolean().optional(),
});
// 与 src/lib/exif.ts 的 PhotoExif 对齐;loader 已保证构建期补全,frontmatter 只需部分覆盖
const photoExifSchema = z.object({
make: z.string().optional(),
model: z.string().optional(),
lens: z.string().optional(),
fNumber: z.number().optional(),
exposureTime: z.number().optional(),
iso: z.number().optional(),
focalLength: z.number().optional(),
dateTimeOriginal: z.string().optional(),
});
const photosFields = {
title: z.string(),
group: z.enum(['street', 'landscape']),
alt: z.string().optional(),
exif: photoExifSchema.optional(),
};
// 编译期双向断言:photoExifSchema 与 PhotoExif 漂移时在此报错,防止两处形状各自演化
type _PhotoExifOut = z.infer<typeof photoExifSchema>;
const _exifShapeOk: _PhotoExifOut extends PhotoExif
? PhotoExif extends _PhotoExifOut
? true
: never
: never = true;
export const collections = {
// slug 即文件名(glob loader 默认按文件名生成 id)
posts: defineCollection({
loader: glob({ pattern: '**/*.md', base: './content/posts' }),
schema: postsSchema,
}),
// image 字段必须走函数式 schema 的 image() helper:zod/v4 的 z 上没有 .image(),
// 相对路径由 Astro 传入的 helper 按 entry 文件位置解析
moments: defineCollection({
loader: glob({ pattern: '**/*.md', base: './content/moments' }),
schema: ({ image }) =>
z
.object({
pubDate: z.coerce.date(),
tags: z.array(z.string()),
text: z.string().optional(),
image: image().optional(),
})
// 瞬间的存在性约束:一句话、一张图至少有其一
.refine((moment) => moment.text !== undefined || moment.image !== undefined, {
message: 'text 与 image 至少提供一项',
}),
}),
// photos 的 exif 字段在加载期由 exif 车道补全,schema 只声明形状
photos: defineCollection({
loader: photosWithExifLoader(),
schema: ({ image }) =>
z.object({
...photosFields,
/** 相对本条 JSON 文件的图片路径 */
image: image(),
}),
}),
};
export type PostData = z.infer<typeof postsSchema>;