内容集合 Schema 与查询
本教程共 56 篇 · 第 21 篇 · 更新于 2026-08-07 · 约 10 分钟阅读
本节目标:学会用 Zod 给集合定义结构做校验,建立集合间引用,并用查询函数把内容取出来、渲染成页面。
上一章讲了怎么用加载器把内容「搬」进集合。这一步解决「数据从哪来」。这一章解决两件事:数据「长什么样」(schema),以及怎么「取出来用」(查询与渲染)。
以 Astro 7.2.0 为准。所有查询函数都从 astro:content 引入,不要去翻那些旧的 src/content/config.ts 写法。
用 Zod 定义结构
schema 用 Zod 来写。Zod 是个做「数据校验」的库,Astro 直接内置了,从 astro/zod 引入 z 即可。
它的作用有两个:一是校验每条数据是否符合预期,字段写错或类型不对,构建时直接报错;二是自动给集合生成 TypeScript 类型,写代码时有补全、有类型检查。
// src/content.config.ts
import { defineCollection } from 'astro:content';
import { z } from 'astro/zod';
import { glob, file } from 'astro/loaders';
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/data/blog' }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
}),
});
const dogs = defineCollection({
loader: file('src/data/dogs.json'),
schema: z.object({
id: z.string(),
breed: z.string(),
temperament: z.array(z.string()),
}),
});
export const collections = { blog, dogs };
几个常用写法:
z.string():字符串。z.coerce.date():日期,会自动把字符串转成Date对象。.optional():这一项可以没有。z.array(z.string()):字符串数组。
Note一旦写了
schema,集合里每条数据的属性都得在 schema 里声明,没写的会被当成多余字段而报错。这是 Zod 帮你「防漏写」的保护机制。
更多 Zod 写法
除了上面几个,Zod 还有不少常用类型,覆盖绝大多数内容字段:
z.number():数字,比如文章字数、评分。z.boolean():真/假,比如draft(是否草稿)。z.enum(["a", "b"]):只能取列表里的几个固定值,适合「状态」类字段,写错值会直接报错。z.literal("x"):固定必须是某个值。z.union([z.string(), z.number()]):可以是多种类型之一。z.object({...}):嵌套对象,比如把author的name、email包成一个子结构。z.string().optional()与z.string().default("默认值"):前者允许为空,后者在缺省时补一个默认值,省得每条都写。
类型还能组合。比如一个「标签」字段,既想要数组、又想允许为空,可以写 z.array(z.string()).optional()。构建时只要某条数据不符合,终端会指出「哪条、哪个字段、错在哪」,改起来很直接。
还有一点容易忽略:z.coerce.date() 的「强制转换」不是万能的。它只认能被 new Date() 解析的字符串(如 2026-08-07)。如果你写成 2026/13/40 这种非法日期,Zod 会报错而不是默默存进去。这正是 schema 的价值——把脏数据挡在构建阶段,而不是等上线才发现。
集合之间建立引用
真实内容常有关系。一篇博客要关联作者,一个商品要关联分类。schema 里可以用 reference() 指向另一个集合的条目。
// src/content.config.ts
import { defineCollection, reference } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const blog = defineCollection({
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
schema: z.object({
title: z.string(),
author: reference('authors'), // 关联到 authors 集合的一条
relatedPosts: z.array(reference('blog')), // 关联到本集合的若干条
}),
});
const authors = defineCollection({
loader: glob({ pattern: '**/*.json', base: './src/data/authors' }),
schema: z.object({
name: z.string(),
portfolio: z.url(),
}),
});
export const collections = { blog, authors };
写内容时,只填被引用条目的 id:
---
title: "欢迎来到我的博客"
author: ben-holmes
relatedPosts:
- about-me
- my-year-in-review
---
Astro 会把这些 id 转成带 collection 和 id 的对象,查询时就能顺着它把关联数据取出来。
查询整个集合:getCollection
取数据用 getCollection(),它把整个集合读出来,返回一个条目数组。从 astro:content 引入。
---
import { getCollection, getEntry } from 'astro:content';
// 取整个 blog 集合
const allBlogPosts = await getCollection('blog');
// 取单条,需要集合名和 id
const poodleData = await getEntry('dogs', 'poodle');
---
<ul>
{allBlogPosts.map(post => (
<li><a href={`/blog/${post.id}`}>{post.data.title}</a></li>
))}
</ul>
每条条目有三个关键部分:id(唯一标识)、data(你在 schema 里定义的所有属性)、body(Markdown 原始正文,未编译)。注意属性都挂在 data 下,所以写 post.data.title 而不是 post.title。
Tip集合返回的顺序不固定,跟平台有关。要按日期排序,得自己排。比如博客按发布时间倒序:
posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())。
取单条:getEntry
只想要某一条,用 getEntry(),传集合名加 id。它比取整个集合更省。
---
import { getEntry } from 'astro:content';
const poodle = await getEntry('dogs', 'poodle');
---
拿到之后,poodle.data.breed 就是这条数据的品种字段。
把正文渲染成 HTML
集合里的 Markdown 正文不会自动变成 HTML,得用 render() 函数。它返回一个 <Content /> 组件,负责把正文画出来。
---
import { getEntry, render } from 'astro:content';
const entry = await getEntry('blog', 'post-1');
if (!entry) {
throw new Error('找不到这条内容');
}
const { Content } = await render(entry);
---
<h1>{entry.data.title}</h1>
<p>发布于:{entry.data.pubDate.toDateString()}</p>
<Content />
流程是:先 getEntry 取条目,再 render(entry) 拿到 <Content />,最后在模板里把 <Content /> 放出来,正文就出现了。
NoteMDX 条目同样用
render()。你还可以通过<Content components={...} />把自定义组件传进去,替换默认的 HTML 元素,用法在第 18 章讲过。
过滤查询结果
getCollection() 接受第二个参数:一个过滤函数。你想筛掉草稿、按子目录归类,都用它。
比如发博客常有个 draft 字段标记草稿,不希望草稿被发出去:
---
import { getCollection } from 'astro:content';
const publishedBlogEntries = await getCollection('blog', ({ data }) => {
return data.draft !== true;
});
---
想更细一点:开发时能看到草稿,正式构建时隐藏。用 import.meta.env.PROD 判断环境:
---
import { getCollection } from 'astro:content';
const blogEntries = await getCollection('blog', ({ data }) => {
return import.meta.env.PROD ? data.draft !== true : true;
});
---
也能按 id 过滤。因为 id 带着完整嵌套路径,所以能用 startsWith 只取某个子目录的内容:
---
import { getCollection } from 'astro:content';
const englishDocs = await getCollection('docs', ({ id }) => {
return id.startsWith('en/');
});
---
取被引用的数据
前面用 reference() 建了关联,现在取出来。先查主条目,再用 getEntry() 或 getEntries() 顺着引用去取关联数据。
---
import { getEntry, getEntries } from 'astro:content';
const blogPost = await getEntry('blog', 'Adventures in Space');
if (!blogPost) throw new Error('文章找不到');
// 取单条引用:作者
const author = await getEntry(blogPost.data.author);
// 取多条引用:相关文章
const relatedPosts = await getEntries(blogPost.data.relatedPosts);
---
<h1>{blogPost.data.title}</h1>
<p>作者:{author.data.name}</p>
<h2>你可能也喜欢:</h2>
{relatedPosts.map((post) => <a href={post.id}>{post.data.title}</a>)}
reference() 已经把值转成了带 collection 和 id 的对象,所以直接把 blogPost.data.author 丢给 getEntry() 就能查到。
给每条内容生成页面
内容集合默认不生成页面(它们不在 src/pages/ 里)。想给每篇博客一个独立网址,得自己建一个动态路由。
静态输出(Astro 默认)用 getStaticPaths():
---
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { id: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
---
<h1>{post.data.title}</h1>
<Content />
getStaticPaths 给每个条目造一条路径,params.id 就是网址里那段;整条条目通过 props 传给页面。比如文件 src/blog/hello-world.md 的 id 是 hello-world,最终网址就是 /posts/hello-world/。
如果装了适配器做按需渲染,还可以在请求时现取条目,用 getEntry() 配合 Astro.params.id 动态查询,不必预先生成全部静态页。这适合条目极多、不想一次构建上万页面的场景。
编辑器里的类型提示
内容集合依赖 TypeScript 才能给 schema 做校验和补全。新建项目时 Astro 默认用 strict 模板,已经配好。若你改成 base 模板,要在 tsconfig.json 补两行:
{
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"strictNullChecks": true,
"allowJs": true
}
}
另外,Astro 会给每个集合自动生成 JSON Schema 文件,放在 .astro/collections/ 下。在 VS Code 里给数据文件指一下 $schema,写 JSON、YAML 数据时也能有提示。
给组件 props 也带上类型
当你把一条集合条目作为属性传给子组件时,可以用 CollectionEntry 这个工具类型,让子组件的 props 自动获得该集合 schema 的类型。
---
import type { CollectionEntry } from 'astro:content';
interface Props {
post: CollectionEntry<'blog'>;
}
// post 的所有字段都会匹配 blog 集合的 schema 类型
const { post } = Astro.props;
---
<h2>{post.data.title}</h2>
CollectionEntry<'blog'> 里的字符串要跟你在 collections 里注册的名字一致。这样子组件里写 post.data.xxx 就有补全,拼错字段名编辑器立刻标红。
小结
这一章把内容集合的「后半程」讲完了。用 Zod 的 schema 校验数据形状并生成类型;用 reference() 串起集合间关系;用 getCollection / getEntry 查询,用 render() 把 Markdown 正文渲染成 HTML;用过滤函数筛草稿、按目录归类;最后用动态路由给每条内容造页面。
内容集合是 Astro 内容站的核心能力。配合第 17、18 章的 Markdown 与 MDX,你已经具备搭建文档站、博客站所需的内容基本功。后续章节会进入样式、图片、集成与部署等更多话题。