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:
1 parent
725bfde21f
commit
80ff67e8fa
18 files changed
+370
-2
No files matched your search
@@ -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>;
|
||||
Reference in new issue
Block a user