这个教程要做什么
用 Astro 的内容集合(Content Collections)搭一个最小可用的内容站骨架:定义 Schema、写两篇内容、让字段校验生效、最后根据内容自动生成详情页路由。
跑完之后你会有一个能构建、能校验、能出页面的项目。
前置条件
- Node.js
22.23.4或更高(Astro 7 的硬性要求) - 包管理器:npm / pnpm 均可
先确认版本:
node -v
# 应该输出 v22.23.4 或更高
第一步:创建项目
npm create astro@latest my-content-site
cd my-content-site
安装过程中选择:
- 模板:Empty project(不要选 blog 模板,我们从零建)
- TypeScript:Yes, strict
- 安装依赖:Yes
- 初始化 Git:可选
启动开发服务器确认环境正常:
npm run dev
浏览器打开 http://localhost:4321,看到 Astro 的欢迎页就说明环境没问题。
注意: 如果 Node 版本低于
22.23.4,这一步会直接失败并提示版本不满足。
第二步:建立内容目录
在项目根目录创建内容文件夹:
mkdir -p content/articles
这里有个容易踩的点:内容目录不一定要放在 src/ 下。放在项目根的 content/ 也是可以的,只要在配置里写对 base 路径。这样做的好处是内容与代码分离更彻底。
写第一篇内容 content/articles/hello.md:
---
title: 第一篇文章
description: 这是一篇用于验证字段校验的示例内容,用来确认 Schema 是否按预期工作。
publishedAt: 2026-10-24
status: published
category: 测试
---
## 正文标题
正文内容写在这里。
第三步:定义集合与 Schema
创建 src/content.config.ts:
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const articles = defineCollection({
loader: glob({ base: './content/articles', pattern: '**/*.md' }),
schema: z.object({
title: z.string(),
description: z.string().min(20).max(200),
publishedAt: z.coerce.date(),
status: z.enum(['draft', 'published']),
category: z.string(),
tags: z.array(z.string()).default([]),
}),
});
export const collections = { articles };
几个关键点:
glob()的base是相对项目根的路径,不是相对src/。z.coerce.date()会把 Markdown 里的日期字符串自动转成Date对象,不需要手写转换。.default([])让tags可以省略,取不到时是空数组而不是undefined。
第四步:验证校验真的生效
这是整个教程最重要的部分。不要只是配置完就假设它work了,要主动制造错误看它报不报。
把 hello.md 的 status 改成一个不存在的值:
status: publishedd
运行构建:
npm run build
你应该看到类似这样的错误:
InvalidContentEntryDataError: articles → hello data does not match collection schema.
status: Invalid enum value. Expected 'draft' | 'published', received 'publishedd'
看到这个错误说明校验生效了。 把它改回去,再试一次删掉 description 字段,应该会得到长度不足的错误。
这一步不能跳过。 很多人配置完 Schema 就认为”字段校验已实现”,但从来没验证过它会不会真的拦下来。校验没验证过,等于没有。
第五步:根据内容生成详情路由
创建 src/pages/articles/[slug].astro:
---
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const articles = await getCollection('articles', ({ data }) => {
return data.status === 'published';
});
return articles.map((entry) => ({
params: { slug: entry.id },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<article>
<h1>{entry.data.title}</h1>
<time datetime={entry.data.publishedAt.toISOString()}>
{entry.data.publishedAt.toLocaleDateString('zh-CN')}
</time>
<Content />
</article>
两个要点:
getCollection的第二个参数是过滤器。 这里过滤掉了draft,从路由层面保证草稿不会被生成页面——这比在模板里写if (draft) return null更可靠,因为草稿根本没有被构建出来。entry.id来自文件名。hello.md生成的id是hello,所以页面路径是/articles/hello/。如果想自定义,在 frontmatter 里加slug字段。
访问 http://localhost:4321/articles/hello/ 验证页面正常渲染。
常见问题
构建报 Cannot find module 'astro:content'
检查 tsconfig.json 是否 extends 了 astro/tsconfigs/strict 或 strictest。内容集合依赖 TypeScript 的严格配置。
日期显示时区偏移一天
z.coerce.date() 解析 2026-10-24 这种纯日期字符串时会按 UTC 处理。如果按本地时区显示,可能差一天。建议在展示层统一指定时区,或者干脆在 frontmatter 里写完整时间戳。
改了内容但页面没更新
开发模式下内容集合有缓存。修改 content.config.ts 后需要重启开发服务器。
完整文件结构
my-content-site/
├── content/
│ └── articles/
│ └── hello.md
├── src/
│ ├── content.config.ts
│ └── pages/
│ └── articles/
│ └── [slug].astro
├── astro.config.mjs
├── package.json
└── tsconfig.json
教程基于 astro@7.4.2 验证,日期 2026-10-24。Astro 大版本升级后内容集合 API 可能有变化,升级前请查阅对应版本的迁移指南。