From 8e08fc5094aab4ea9a3d6e1d8a08765d37ef8dfd Mon Sep 17 00:00:00 2001 From: dsh Date: Thu, 17 Sep 2026 20:35:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=85=E5=AE=B9=20schema=20=E5=85=9C?= =?UTF-8?q?=E4=BD=8F=20YAML=20=E7=A9=BA=E5=80=BC=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E6=8E=89=E6=89=8B=E5=86=99=E5=81=B6=E5=BE=97=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E7=9A=84=E6=9E=84=E5=BB=BA=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题 新增的 src/content/thoughts/20260622-夏至已至.md 让 build 直接失败: InvalidContentEntryDataError: thoughts → 20260622-夏至已至 tags: Expected type "array", received "object" images: Expected type "array", received "object" 报错里那个 "object" 很误导 —— 它其实是 null(JS 里 typeof null === "object")。 根因 该文件的 frontmatter 写了两个空字段: images: tags: YAML 把「只写 key、后面什么都不跟」解析成 null,而不是 undefined。 而 schema 用的是 z.array(z.string()).optional().default([]) —— .optional() 只放行 undefined,.default() 也只对 undefined 生效, 所以 null 会直接被判为类型不符。 这不是那一个文件的问题,而是一类写法的问题:手写 frontmatter 时把不要的字段 留空太自然了。而且这类报错信息完全指不出真正的原因,很浪费时间。 改法 在 content.config.ts 顶部加三个 helper,把 null 当作「没填」处理: text(fallback = "") 文本,留空 → fallback optionalText() 可选文本,留空 → undefined(保持原类型) textList() 字符串数组,留空 → [] 然后套用到 posts / thoughts / studies 里所有「留空应该算默认值」的字段 (description / image / tags / category / lang / password / prev* / next*、 thoughts 的 title / tags / location / weather / images、studies 的三个可选文本)。 注意:只改了「留空该走默认」的字段。像 posts.title、published、 studies.title / status / started 这些必填项保持原样 —— 它们留空本来就该报错。 验证 - 清内容缓存重建:pnpm build 退出码 0,不再报 InvalidContentEntryDataError - 那条偶得正常产出(tags / images 都成了空数组,location / weather 保留) - 抽查其他条目:树莓派那篇的标签与任务列表、首页的偶得列表均正常 --- src/content.config.ts | 58 ++++++++++++++++------- src/content/thoughts/20260622-夏至已至.md | 14 ++++++ src/data/reading.json | 2 +- 3 files changed, 55 insertions(+), 19 deletions(-) create mode 100644 src/content/thoughts/20260622-夏至已至.md diff --git a/src/content.config.ts b/src/content.config.ts index 7435b44..891e439 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -2,6 +2,28 @@ 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({ @@ -9,36 +31,36 @@ const posts = defineCollection({ published: z.date(), updated: z.date().optional(), draft: z.boolean().optional().default(false), - description: z.string().optional().default(""), - image: z.string().optional().default(""), - tags: z.array(z.string()).optional().default([]), - category: z.string().optional().nullable().default(""), - lang: z.string().optional().default(""), + 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: z.string().optional().default(""), + password: text(), disclaimer: z.union([z.string(), z.array(z.string())]).optional(), - prevTitle: z.string().default(""), - prevSlug: z.string().default(""), - nextTitle: z.string().default(""), - nextSlug: z.string().default(""), + prevTitle: text(), + prevSlug: text(), + nextTitle: text(), + nextSlug: text(), }), }) const thoughts = defineCollection({ loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/thoughts" }), schema: z.object({ - title: z.string().optional(), + title: optionalText(), published: z.date(), - tags: z.array(z.string()).optional().default([]), + tags: textList(), /** 地点,显示在日期旁 */ - location: z.string().optional(), + location: optionalText(), /** 天气,与地点一起显示 */ - weather: z.string().optional(), + weather: optionalText(), /** 配图,public 下的绝对路径 */ - images: z.array(z.string()).optional().default([]), + images: textList(), /** 置顶 */ pinned: z.boolean().optional().default(false), }), @@ -52,9 +74,9 @@ const studies = defineCollection({ title: z.string(), status: z.enum(["在读", "沉淀中", "暂搁", "已结"]), started: z.string(), // "YYYY-MM" 或 "YYYY-MM-DD" - subtitle: z.string().optional(), - epigraph: z.string().optional(), - field: z.string().optional(), + subtitle: optionalText(), + epigraph: optionalText(), + field: optionalText(), }), }) diff --git a/src/content/thoughts/20260622-夏至已至.md b/src/content/thoughts/20260622-夏至已至.md new file mode 100644 index 0000000..7c1698c --- /dev/null +++ b/src/content/thoughts/20260622-夏至已至.md @@ -0,0 +1,14 @@ +--- +published: 2026-06-21 +title: 夏至已至 +location: 张掖·甘州区 +weather: 晴 +images: +tags: +pinned: false +--- + +> 随笔,只是想写一些自己的想法 +转眼之间已是夏至,太阳再次来到北回归线,时间就是这样,总是过得如此之快。突然之间就回想起十年前,也就是2016年那个夏天了,那时我刚参加完高考,面对自己的未来不知所措,踏上了前往军检的火车。人生关键的选择往往并不多,只不过当时是无法感受到其强劲的影响。 + +当时自己也是一个懵懂无知的少年,缺少自己的想法和主见。有的时候可能这就是命吧,谁也不知道自己的未来会怎样,但是依然要积极面对生活。 diff --git a/src/data/reading.json b/src/data/reading.json index dc6a2a7..bde1db0 100644 --- a/src/data/reading.json +++ b/src/data/reading.json @@ -1,5 +1,5 @@ { - "updatedAt": "2026-09-17T12:16:21.056Z", + "updatedAt": "2026-09-17T12:35:36.207Z", "source": "weread", "books": [ {