TL;DR
Content Collections 不只是读 Markdown 文件。掌握动态路由、分页和 schema 校验后,你可以构建比传统博客更灵活内容架构—系列文章、关联推荐、自动分页列表,全部类型安全。
超越基础:Content Collections 能做什么
基础用法:定义 schema → 写 Markdown → getCollection() 获取。
高级用法:
- 自定义 slug 映射(文件名和 URL 路径不一致)
- 分页(每页 N 篇,自动生成多个页面)
- 跨 collection 引用(文章关联项目、作者关联文章)
- 条件内容校验(series 文章必须有 seriesOrder)
- 自定义渲染管线
动态路由:slug 映射
默认行为
文件 src/content/blog/my-post.md → slug my-post → URL /blog/my-post。
自定义 slug
Astro 6.x 允许在 frontmatter 中指定 slug:
// content.config.ts
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
slug: z.string().optional(), // 自定义 slug
// ...
}),
});
---
title: 'My Post'
slug: 'custom-url-path'
---
然后在动态路由页面中读取:
---
// src/pages/blog/[...slug].astro
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: {
slug: post.data.slug || post.id.replace(/\.md$/, '')
},
props: { post },
}));
}
---
嵌套目录路由
文件 src/content/blog/tutorials/astro-setup.md 默认 slug 是 tutorials/astro-setup。
如果你想去掉目录层级:
// content.config.ts
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
slug: z.string().optional(),
}),
});
然后在 frontmatter 中设置 slug: 'astro-setup'。
分页:每页 N 篇
Astro 内置 paginate() 函数:
---
// src/pages/blog/[page].astro
import { getCollection } from 'astro:content';
export async function getStaticPaths({ paginate }) {
const posts = await getCollection('blog');
const sorted = posts.sort((a, b) =>
b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
return paginate(sorted, { pageSize: 10 });
}
const { page } = Astro.props;
---
<!-- 文章列表 -->
{page.data.map(post => (
<article>
<a href={`/blog/${post.slug}`}>
<h2>{post.data.title}</h2>
<time>{post.data.pubDate.toLocaleDateString()}</time>
</a>
</article>
))}
<!-- 分页导航 -->
<nav class="pagination">
{page.url.prev && <a href={page.url.prev}>上一页</a>}
<span>第 {page.currentPage} / {page.lastPage} 页</span>
{page.url.next && <a href={page.url.next}>下一页</a>}
</nav>
分页对象属性
| 属性 | 类型 | 说明 |
|---|---|---|
page.data | T[] | 当前页文章 |
page.currentPage | number | 当前页码 |
page.lastPage | number | 总页数 |
page.url.prev | string | undefined | 上一页 URL |
page.url.next | string | undefined | 下一页 URL |
page.total | number | 总文章数 |
分页和 slug 路由冲突
/blog/[page].astro 和 /blog/[...slug].astro 可能冲突。解决方案:
把分页放在不同路径:
/blog/[...slug].astro:文章页/page/[page].astro:分页列表
或者用条件判断:
export async function getStaticPaths({ paginate }) {
const posts = await getCollection('blog');
// 文章页
const postPaths = posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
// 分页路径
const paginatedPaths = paginate(posts, { pageSize: 10 });
return [...postPaths, ...paginatedPaths];
}
Schema 高级校验
条件校验
系列文章必须有 seriesOrder:
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
category: z.string().optional(),
tags: z.array(z.string()).optional(),
series: z.string().optional(),
seriesOrder: z.number().optional(),
}).refine(
data => {
// 如果有 series,必须有 seriesOrder
if (data.series && !data.seriesOrder) return false;
return true;
},
{ message: '系列文章必须指定 seriesOrder' }
),
});
联合类型
category 可以是预定义值或自由文本:
const CATEGORIES = ['教程', '技术', '随笔'] as const;
const blog = defineCollection({
type: 'content',
schema: z.object({
category: z.enum(CATEGORIES).or(z.string()), // 预定义值或任意字符串
// ...
}),
});
日期校验
确保 pubDate 不是未来日期:
z.date().max(new Date(), '发布日期不能是未来')
内容关系
关联推荐
基于标签重叠文章推荐:
---
const currentPost = Astro.props.entry;
const allPosts = await getCollection('blog');
// 计算标签重叠度
const related = allPosts
.filter(p => p.slug !== currentPost.slug)
.map(p => ({
post: p,
overlap: (p.data.tags || []).filter(t =>
(currentPost.data.tags || []).includes(t)
).length,
}))
.sort((a, b) => b.overlap - a.overlap)
.slice(0, 5);
---
<aside>
<h3>相关文章</h3>
{related.map(({ post, overlap }) => (
<a href={`/blog/${post.slug}`}>
{post.data.title} ({overlap} 个共同标签)
</a>
))}
</aside>
系列导航
同系列文章按 seriesOrder 排序:
---
const seriesPosts = allPosts
.filter(p => p.data.series === currentPost.data.series)
.sort((a, b) => a.data.seriesOrder - b.data.seriesOrder);
const currentIndex = seriesPosts.findIndex(p => p.slug === currentPost.slug);
const prev = seriesPosts[currentIndex - 1];
const next = seriesPosts[currentIndex + 1];
---
<nav class="series-nav">
{prev && <a href={`/blog/${prev.slug}`}>← {prev.data.title}</a>}
<span>{currentIndex + 1} / {seriesPosts.length}</span>
{next && <a href={`/blog/${next.slug}`}>{next.data.title} →</a>}
</nav>
渲染管线定制
自定义 remark 插件
给所有代码块添加复制按钮:
// astro.config.mjs
import { defineConfig } from 'astro/config';
function remarkCodeCopy() {
return (tree) => {
// 遍历所有 code 节点,添加复制按钮 HTML
};
}
export default defineConfig({
markdown: {
remarkPlugins: [remarkCodeCopy],
},
});
自定义渲染输出
获取渲染后 HTML 和元数据:
---
const { Content, headings } = await entry.render();
// headings: [{ depth: 2, slug: '标题', text: '标题' }, ...]
// Content: 渲染后的 HTML 组件
---
<Content />
写在
Content Collections 是 Astro 博客的核心数据层。掌握动态路由、分页、schema 校验和内容关系后,你可以构建比 WordPress 更灵活、比 Hugo 更类型安全内容架构。
种下你的想法
在花园里留下一条评论,和这篇文章一起生长。