约 2 分钟

用 Astro 内容集合管理你的博客

从 glob loader 到 schema 校验,一步步理解 Astro 内容集合的运作方式,并学会用它组织文章、标签与归档。

内容集合(Content Collections)是 Astro 组织内容的核心方式。它把”内容”和”渲染”分离:你只管写 Markdown,剩下的类型检查、查询、排序都交给框架。

集合与 Loader

src/content.config.ts 中,一个集合由 loaderschema 构成:

import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const post = defineCollection({
  loader: glob({ base: './src/content/post', pattern: '**/*.{md,mdx}' }),
  schema: z.object({
    title: z.string(),
    description: z.string().default(''),
    pubDate: z.coerce.date(),
    tags: z.array(z.string()).default([]),
    pinned: z.boolean().default(false),
  }),
});

export const collections = { post };

glob loader 会把目录下所有匹配的文件变成集合条目,每个条目的 id 就是相对路径(不含扩展名),这也是文章 URL 的一部分。

为什么需要 Schema

Schema 的价值在写作时就能体现出来:

  • 默认值:description 缺省为空字符串,不必每篇都写
  • 类型推导:pubDateDate,排序时可以直接比较
  • 错误反馈:漏字段、写错类型,构建时立刻报错,而不是上线后才发现
// 查询时过滤草稿
const posts = await getCollection('post', ({ data }) => !data.draft);

// 按日期排序
posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());

渲染与标题提取

拿到集合条目后,用 render() 得到渲染后的内容和标题结构:

const { Content, headings } = await render(post);

headings{ depth, slug, text }[],目录组件就是靠它生成的。注意 slug 由 Astro 根据标题自动生成,所以要保证标题唯一。

常见模式

需求做法
标签页遍历所有文章,countTags() 聚合
归档pubDate 的年份 groupByYear() 分组
上下篇导航排序后取当前文章的前后邻居
搜索索引stripMarkdown(body) 建立关键词索引

这些都是本主题已经实现的功能,可以直接在 src/pages/ 下翻看源码。

性能注意

内容多时,render() 只应在文章页调用;列表页只需 post.datapost.body 的纯文本,避免无谓的 Markdown 渲染开销。这个主题首页的阅读时长就是这样算的:

export function readingTime(md: string, wpm = 300): number {
  const text = stripMarkdown(md);
  const cjk = (text.match(/[\u4e00-\u9fa5\u3040-\u30ff]/g) ?? []).length;
  const words = text.replace(/[\u4e00-\u9fa5\u3040-\u30ff]/g, ' ').split(/\s+/).filter(Boolean).length;
  return Math.max(1, Math.round(cjk / wpm + words / 220));
}

中文按字、英文按词,比单一 WPM 估算更贴近真实阅读时长。