内容 schema 兜住 YAML 空值,修掉手写偶得触发的构建失败
问题 新增的 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 保留) - 抽查其他条目:树莓派那篇的标签与任务列表、首页的偶得列表均正常
This commit is contained in:
+40
-18
@@ -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(),
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user