首页 / Astro 教程 / 内容集合 Schema 与查询

Astro 教程

内容集合 Schema 与查询

本教程共 56 篇 · 第 21 篇 · 更新于 2026-08-07 · 约 10 分钟阅读

AstroAstro 教程内容集合ZodschemagetCollectiongetEntryrender

本节目标:学会用 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({...}):嵌套对象,比如把 authornameemail 包成一个子结构。
  • 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 转成带 collectionid 的对象,查询时就能顺着它把关联数据取出来。

查询整个集合: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 /> 放出来,正文就出现了。

Note

MDX 条目同样用 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() 已经把值转成了带 collectionid 的对象,所以直接把 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.mdidhello-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,你已经具备搭建文档站、博客站所需的内容基本功。后续章节会进入样式、图片、集成与部署等更多话题。