Files
NewBlog/src/content.config.ts
T
dsh 8e08fc5094 内容 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 保留)
- 抽查其他条目:树莓派那篇的标签与任务列表、首页的偶得列表均正常
2026-09-17 20:35:56 +08:00

84 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 }