Astro Content Collections 入门:半小时上手(Astro 6)

面向 Astro 6 的 Content Collections 上手指南:glob() loader、content.config.ts、Zod schema、render(entry),让上千篇规模的站点依然好维护。

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.tstype: 'content'
  • 本地 Markdown/MDX 用 glob() loader,JSON/YAML 用 file(),都从 astro/loaders 引入。
  • entry.slug 没了,改用 entry.identry.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.tssrc/content.config.ts(项目根目录,和 astro.config 同级)
defineCollection({ type: 'content', ... })defineCollection({ loader: glob({ ... }), ... })
entry.slugentry.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.slugentry.render() 改成 entry.idawait 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() 的调用处都改掉。

相关阅读

外部参考:Astro Content Collections 官方指南Astro 6.0 发布说明

标签: #独立开发 #Astro #Content Collections #MDX #入门