import { defineCollection } from "astro:content" import { glob } from "astro/loaders" import { z } from "astro/zod" /* * YAML 里只写 `tags:`(后面什么都不跟)会解析成 **null**,而不是 undefined。 * * Zod 的 `.optional()` 只放行 undefined,`.default(x)` 也只对 undefined 生效, * 所以 `z.array(z.string()).optional().default([])` 遇到 `tags:` 会直接报 * Expected type "array", received "object" * —— 那个 "object" 是 JS 里 `typeof null === "object"` 造成的,很误导, * 光看报错完全想不到是「字段留空」。 * * 手写 frontmatter 时把不要的字段留空是很自然的写法,所以这里统一用下面两个 * helper 把 null 一并兜住,当作「没填」处理。 */ /** 文本字段:留空 / 未填 → fallback(默认空字符串) */ const text = (fallback = "") => z.string().nullish().transform((v) => v ?? fallback) /** 可选文本:留空 / 未填 → undefined(保持原来 `.optional()` 的类型) */ const optionalText = () => z.string().nullish().transform((v) => v ?? undefined) /** 字符串数组:留空 / 未填 → 空数组 */ const textList = () => z.array(z.string()).nullish().transform((v) => v ?? []) const posts = defineCollection({ loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/posts" }), schema: z.object({ title: z.string(), published: z.date(), updated: z.date().optional(), draft: z.boolean().optional().default(false), description: text(), image: text(), tags: textList(), category: z.string().nullish().transform((v) => v ?? ""), lang: text(), pinned: z.boolean().optional().default(false), encrypted: z.boolean().optional().default(false), password: text(), disclaimer: z.union([z.string(), z.array(z.string())]).optional(), prevTitle: text(), prevSlug: text(), nextTitle: text(), nextSlug: text(), }), }) const thoughts = defineCollection({ loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/thoughts" }), schema: z.object({ title: optionalText(), published: z.date(), tags: textList(), /** 地点,显示在日期旁 */ location: optionalText(), /** 天气,与地点一起显示 */ weather: optionalText(), /** 配图,public 下的绝对路径 */ images: textList(), /** 置顶 */ pinned: z.boolean().optional().default(false), }), }) const studies = defineCollection({ loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/studies" }), // 注意:`id` 由 glob loader 从文件路径生成(Content Layer API), // 所以这里不声明 slug,直接读 entry.id。 schema: z.object({ title: z.string(), status: z.enum(["在读", "沉淀中", "暂搁", "已结"]), started: z.string(), // "YYYY-MM" 或 "YYYY-MM-DD" subtitle: optionalText(), epigraph: optionalText(), field: optionalText(), }), }) export const collections = { posts, thoughts, studies }