Content Collections 把一个 Markdown 文件夹变成有类型、可查询的数据库。但 2026 年有个坑:网上几乎所有教程用的还是 Astro 4 的旧 API,而这套 API 在 Astro 6 里已经被移除。本文从头到尾都用当前的 Content Layer API,代码能直接编译过。
一句话总结
- Astro 6(2026 年 3 月 10 日转正,当前是 6.4 线)里,collection 用 loader 定义、写在
src/content.config.ts——不再是旧的src/content/config.ts加type: 'content'。 - 本地 Markdown/MDX 用
glob()loader,JSON/YAML 用file(),都从astro/loaders引入。 entry.slug没了,改用entry.id;entry.render()没了,要从astro:content引入render再调render(entry)。- Zod 现在从
astro/zod引入(Astro 6 带的是 Zod 4),不过从astro:content的再导出仍然能用。 - 配置一次大约半小时,之后每次改字段名、改 slug、排查 frontmatter 脏数据都省心。
哪里变了,旧教程为什么会崩
把 2024 年某篇博客里的 content-collections 代码原样贴进新的 Astro 6 项目,必崩。Content Layer API 在 Astro 5.0 落地,旧的 collections API 在 6.0 被彻底移除。三处具体差异:
| Astro 4(已废弃) | Astro 5/6(当前) |
|---|---|
src/content/config.ts | src/content.config.ts(项目根目录,和 astro.config 同级) |
defineCollection({ type: 'content', ... }) | defineCollection({ loader: glob({ ... }), ... }) |
entry.slug | entry.id |
await entry.render() | await render(entry)(从 astro:content 引入) |
import { z } from 'astro:content' | import { z } from 'astro/zod'(再导出仍可用) |
重写也确实划算:按 Astro 官方数据,新管线下 Markdown 构建最快提速 5 倍、MDX 最快提速 2 倍,内存占用降低 25%–50%——站点过几百页之后,这个差别就很明显了。
什么时候值得上 Content Collections
- 已经或预计超过 30 篇左右的 Markdown / MDX 文件。
- 希望 frontmatter 写错时是构建失败、而不是页面坏掉。
- 多个模板要读同一份内容:首页、hub 页、sitemap、RSS。
- 站点要维护数年,而不是数周。
2026 年起 Astro 内容站,第一天就把它接上。等到 200 篇的文件夹再回头补,比现在学 schema 痛苦得多。
实操步骤
1. 放几个带 frontmatter 的 MDX 文件
建 src/content/blog/ 目录,加一篇:
---
title: "Hello world"
description: "一篇测试文章,端到端验证 content collections 是否正常工作。"
publishedAt: 2026-05-22
tags: ["intro", "test"]
author: "alice"
---
这是正文。
glob loader 能读 .md、.mdx、.markdoc、.json、.yaml、.toml,所以同一个 base 路径下数据和正文可以混着放。
2. 在 src/content.config.ts 里写 schema
这个文件放在项目根目录,和 astro.config.mjs 同级——不是放在 src/content/ 里面。Schema 就是契约:任何文件不满足都会让构建失败,并给出精确报错。
import { defineCollection, reference } from 'astro:content';
import { glob, file } from 'astro/loaders';
import { z } from 'astro/zod';
const blog = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
schema: ({ image }) => z.object({
title: z.string().min(10).max(80),
description: z.string().min(120).max(160),
publishedAt: z.coerce.date(),
updatedAt: z.coerce.date().optional(),
tags: z.array(z.string()).min(1),
cover: image().optional(),
author: reference('authors'),
draft: z.boolean().default(false),
featured: z.boolean().default(false),
}),
});
const authors = defineCollection({
loader: file('src/content/authors.json'),
schema: z.object({
id: z.string(),
name: z.string(),
bio: z.string(),
twitter: z.string().optional(),
}),
});
export const collections = { blog, authors };
两个细节值得注意:z.coerce.date() 会帮你把 YAML 里的日期字符串解析成 Date;image() 仍然只能在 schema 上下文回调里用(({ image }) => ...),它会校验文件真实存在、并送进 Astro 的图片优化管线——直接存字符串路径就跳过了这一步。
3. 起 dev server,让它逐个校验
npm run dev
# [content] Could not parse frontmatter in blog → old-post.mdx
# description: String must contain at most 160 character(s)
Astro 会拿每一条 entry 去对 schema,并指出具体哪个文件、哪个字段出错。红字全清掉再往下走——这种”大声失败”正是它的价值。
4. 加作者数据
因为 authors collection 用的是 file() loader,所以它是一个 JSON 数组,而不是每个作者一个文件:
[
{ "id": "alice", "name": "Alice Chen", "bio": "独立开发者,写内容站相关。", "twitter": "alicewrites" }
]
5. 渲染一个带类型的列表页
getCollection 返回的 entry 是完整带类型的,编辑器会补全 entry.data.title,拼错字段在构建期就会被标出来:
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
const posts = (await getCollection('blog', ({ data }) => !data.draft))
.sort((a, b) => +b.data.publishedAt - +a.data.publishedAt);
---
<ul>
{posts.map((p) => (
<li>
<a href={`/blog/${p.id}/`}>{p.data.title}</a>
<time datetime={p.data.publishedAt.toISOString()}>
{p.data.publishedAt.toLocaleDateString()}
</time>
</li>
))}
</ul>
Astro 5/6 上有个坑:getCollection 不再保证顺序稳定,所以只要顺序重要,就一定自己 .sort()。
6. 写单篇文章路由
路由文件用 [...id](rest 参数),这样带斜杠的 id 也能正确解析。注意 render(entry) 现在是顶层引入,作者引用用 getEntry 解析:
---
// src/pages/blog/[...id].astro
import { getCollection, getEntry, render } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => !data.draft);
return posts.map((post) => ({ params: { id: post.id }, props: { post } }));
}
const { post } = Astro.props;
const author = await getEntry(post.data.author);
const { Content } = await render(post);
---
<article>
<h1>{post.data.title}</h1>
<p>作者:{author?.data.name}</p>
<Content />
</article>
7. 跑一遍生产构建确认
npm run build
# 09:42:11 [build] 1209 page(s) built in 78.40s
# 09:42:11 [build] Complete!
构建干净,就说明没有畸形 frontmatter 漏网——schema 替你把住了关。
容易踩的坑
- 配置文件还放在
src/content/config.ts。 Astro 6 直接忽略它,collection 会悄悄变空。挪到项目根目录的src/content.config.ts。 - 从旧教程抄了
type: 'content'。 这个键已经不存在了,每个 collection 都要有loader。 - 还在用
entry.slug或entry.render()。 改成entry.id和await render(entry)。 - 为了”过构建”把必填字段写成
.optional()。 这只会把脏数据放进生产环境。 - 改了 schema 没重启
dev。 生成的类型有缓存,重启才会重新生成.astro/。 - 指望 Live Content Collections 包打天下。 Astro 6 里 live 版本对请求时数据已转正,但它不支持
image()也不能渲染 MDX——正文类内容还是放构建期 collection。
什么时候可以不用
10 页以内的小站,schema 的成本回不来。要是只手写几张静态页,纯 .astro 文件更简单。
FAQ
- Astro 6 用哪个配置文件: 项目根目录下的
src/content.config.ts(和astro.config.mjs同级)。旧的src/content/config.ts是 Astro 4 的路径,现在会被忽略。 - 还支持 MDX 吗: 支持。
glob()loader 能处理.md和.mdx(还有 Markdoc、JSON、YAML、TOML),MDX 里可以内联渲染组件。唯一例外是 live collection,它不能渲染 MDX。 entry.render()和entry.slug被什么取代了: 从astro:content引入render并调用render(entry);entry 现在是普通对象,用entry.id而非entry.slug标识。- 作者、标签这类 JSON/YAML 数据放哪: 用
astro/loaders里的file()loader 指向一个含数组的 JSON/YAML 文件;其它 collection 里用reference('authors')引用某一行。 - frontmatter 写错会怎样: 构建直接失败,报错点名具体文件和字段。这正是它的价值——脏数据进不了生产。
- 升级一定要重写 Astro 4 的 collection 吗: 凡是还在旧 API 上的都要重写,因为 6.0 把它移除了。换成
loader、挪配置文件、把slug/render()的调用处都改掉。