跳转到内容

内容集合

添加于: astro@2.0.0

内容集合(Content collections) 是在任何 Astro 项目中管理内容集的最佳方式。集合有助于组织和查询文档,为你的编辑器启用智能提示和类型检查,并为所有内容提供自动的 TypeScript 类型安全。

Astro 5.0 引入了内容层(Content Layer)API,用于定义和查询内容集。这个高性能、可扩展的 API 为本地集合提供了内置的内容加载器(content loaders)。对于远程内容,你可以使用第三方和社区构建的加载器,或者创建自己的自定义加载器,从任何来源拉取数据。

你可以从结构相似的数据集中定义一个集合。这可以是一个博客文章的目录,一个产品项目的 JSON 文件,或者任何代表相同形状的多个项目的数据。

本地存储在项目中或文件系统上的集合可以包含 Markdown、MDX、Markdoc、YAML 或 JSON 文件的条目:

  • 文件夹src/
  • 文件夹newsletter/ “newsletter” 集合
    • week-1.md 一个集合条目
    • week-2.md 一个集合条目
    • week-3.md 一个集合条目
  • 文件夹authors/ “author” 集合
    • authors.json 包含所有集合条目的单个文件

使用对应的集合加载器,你可以从任何外部来源获取远程数据,比如 CMS、数据库或无头支付系统。

集合的 TypeScript 配置

段落标题 集合的 TypeScript 配置

内容集合依靠 TypeScript 为你的编辑器提供 Zod 验证、智能提示和类型检查。如果你没有扩展 Astro 的 strictstrictest TypeScript 设置,你需要确保在 tsconfig.json 中设置以下 compilerOptions

tsconfig.json
{
// 包括在 "astro/tsconfigs/strict" 或 "astro/tsconfigs/strictest" 中
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"strictNullChecks": true, // 使用 `base` 模板需要添加
"allowJs": true // 必需,包含在所有 Astro 模板中
}
}

单个集合使用 defineCollection() 配置:

  • 一个 loader 用于数据源(必需)
  • 一个 schema 用于类型安全(可选,但强烈推荐!)

要定义集合,你必须在项目中创建一个 src/content.config.ts 文件(也支持 .js.mjs 扩展名)。这是一个特殊的文件,Astro 将根据以下结构使用它来配置你的内容集合:

src/content.config.ts
// 1. 从 `astro:content` 导入工具函数
import { defineCollection, z } from 'astro:content';
// 2. 导入加载器
import { glob, file } from 'astro/loaders';
// 3. 定义你的集合
const blog = defineCollection({ /* ... */ });
const dogs = defineCollection({ /* ... */ });
// 4. 导出一个 `collections` 对象来注册你的集合
export const collections = { blog, dogs };

内容层 API 允许你从任何地方获取你的内容(无论是本地存储在项目中还是远程存储),并使用 loader 属性来检索你的数据。

Astro 提供了两个内置的加载器函数(glob()file())用于获取本地内容,也可以通过 API 来构建自己的加载器并请求远程数据。

glob() 加载器可以从文件系统的任何地方创建 Markdown、MDX、Markdoc 或 JSON 文件的目录条目。它基于 micromatch 的 glob 模式支持来接受一个匹配条目文件的 pattern,以及你的文件所在的 base 文件路径。每个条目的 id 将从其文件名自动生成。当每个条目对应一个文件时,请使用此加载器。

file() 加载器从单个本地文件创建多个条目。文件中的每个条目必须有一个唯一的 id 键属性。它接受一个 相对你的文件的 base 文件路径,以及一个可选的 parser 函数 用于它无法自动解析的数据文件。当你的数据文件可以解析为对象数组时,请使用此加载器。

src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob, file } from 'astro/loaders'; // 不适用于旧版 API
const blog = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/data/blog" }),
schema: /* ... */
});
const dogs = defineCollection({
loader: file("src/data/dogs.json"),
schema: /* ... */
}),
});
const probes = defineCollection({
// `loader` 可以接受多个模式的数组以及字符串模式"
// 加载 space-probes 目录中的所有 Markdown 文件,以 "voyager-" 开头的文件除外
loader: glob({ pattern: ['*.md', '!voyager-*'], base: 'src/data/space-probes' }),
schema: z.object({
name: z.string(),
type: z.enum(['Space Probe', 'Mars Rover', 'Comet Lander']),
launch_date: z.date(),
status: z.enum(['Active', 'Inactive', 'Decommissioned']),
destination: z.string(),
operator: z.string(),
notable_discoveries: z.array(z.string()),
}),
});
export const collections = { blog, dogs, probes };

file() 加载器接受一个第二个参数,定义了一个 parser 函数。这允许你指定一个自定义解析器(例如 toml.parsecsv-parse)来从文件内容创建一个集合。

file() 加载器将自动检测和解析 JSON 和 YAML 文件中的单个对象数组(基于它们的文件扩展名),无需 parser,除非你有一个嵌套的 JSON 文件。要使用其他文件,例如 .toml.csv,你需要创建一个解析器函数。

以下示例使用 .toml 文件定义了一个内容集合 dogs

src/data/dogs.toml
[[dogs]]
id = "..."
age = "..."
[[dogs]]
id = "..."
age = "..."

导入 TOML 解析器后,你可以通过将文件路径和 parser 函数传递给 file() 加载器来将 dogs 集合加载到你的项目中。类似的过程可以用来从 .csv 文件定义 cats 集合:

src/content.config.ts
import { defineCollection } from "astro:content";
import { file } from "astro/loaders";
import { parse as parseToml } from "toml";
import { parse as parseCsv } from "csv-parse/sync";
const dogs = defineCollection({
loader: file("src/data/dogs.toml", { parser: (text) => parseToml(text).dogs }),
schema: /* ... */
})
const cats = defineCollection({
loader: file("src/data/cats.csv", { parser: (text) => parseCsv(text, { columns: true, skipEmptyLines: true })})
});

parser 参数还允许你从嵌套的 JSON 文档中加载单个集合。例如,这个 JSON 文件包含了多个集合:

src/data/pets.json
{"dogs": [{}], "cats": [{}]}

你可以通过为每个集合传递一个自定义的 parser 来将这些集合分开:

src/content.config.ts
const dogs = defineCollection({
loader: file("src/data/pets.json", { parser: (text) => JSON.parse(text).dogs })
});
const cats = defineCollection({
loader: file("src/data/pets.json", { parser: (text) => JSON.parse(text).cats })
});

构建一个自定义加载器

段落标题 构建一个自定义加载器

你可以构建一个自定义加载器来从任何数据源(如 CMS、数据库或 API 端点)获取远程内容。

使用加载器请求数据将自动从远程数据创建一个集合。这为你提供了所有本地集合的好处,例如集合特定的 API 助手,如 getCollection()render() 来查询和显示你的数据,以及模式验证。

你可以在集合内部定义一个内联加载器,作为一个返回条目数组的异步函数。

这对不需要手动控制数据如何加载和存储的加载器非常有用。每当调用加载器时,它都会清除存储并重新加载所有条目。

src/content.config.ts
const countries = defineCollection({
loader: async () => {
const response = await fetch("https://restcountries.com/v3.1/all");
const data = await response.json();
// 必须返回具有 id 属性的条目数组,或以 ID 作为键、条目作为值的对象
return data.map((country) => ({
id: country.cca3,
...country,
}));
},
schema: /* ... */
});

返回的条目将存储在集合中,并可以使用 getCollection()getEntry() 函数进行查询。

为了更好的控制加载过程,你可以使用内容加载器 API 来创建一个加载器对象。例如,通过直接访问 load 方法,你可以创建一个加载器,允许条目逐步更新或仅在必要时清除存储。

与创建 Astro 集成或 Vite 插件类似,你可以将你的加载器 作为 NPM 包分发,供他人在他们的项目中使用。

更多关于构建你自己的加载器的例子,请参考完整的 内容加载器 API (EN)

定义集合模式(Schema)

段落标题 定义集合模式(Schema)

模式通过 Zod 验证强制执行集合中的一致的 frontmatter 或条目数据。模式 保证 了当你需要引用或查询数据时,这些数据以可预测的形式存在。如果任何文件违反了其集合模式,Astro 将提供一个有用的错误提示。

模式还为 Astro 的内容自动 TypeScript 类型提供了支持。当你为集合定义模式时,Astro 将自动生成并应用一个 TypeScript 接口。结果是当你查询集合时,完整的 TypeScript 支持,包括属性自动补全和类型检查。

集合条目的每个 frontmatter 或数据属性都必须使用 Zod 数据类型定义:

src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob, file } from 'astro/loaders'; // 不适用于旧版 API
const blog = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/data/blog" }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
})
});
const dogs = defineCollection({
loader: file("src/data/dogs.json"),
schema: z.object({
id: z.string(),
breed: z.string(),
temperament: z.array(z.string()),
}),
});
export const collections = { blog, dogs };

使用 Zod 定义数据类型

段落标题 使用 Zod 定义数据类型

Astro 使用 Zod 来支持其内容模式。使用 Zod,Astro 能够验证集合中每个文件的数据,并在你查询内容时提供自动的 TypeScript 类型。

要在 Astro 中使用 Zod,请从 "astro:content" 导入 z 工具函数。这是 Zod 库的重新导出,支持 Zod 的所有功能。

// 示例:许多常见 Zod 数据类型的备忘单
import { z, defineCollection } from 'astro:content';
defineCollection({
schema: z.object({
isDraft: z.boolean(),
title: z.string(),
sortOrder: z.number(),
image: z.object({
src: z.string(),
alt: z.string(),
}),
author: z.string().default('Anonymous'),
language: z.enum(['en', 'es']),
tags: z.array(z.string()),
footnote: z.string().optional(),
// 在 YAML 中,不带引号的日期被解释为 Date 对象
publishDate: z.date(), // 例如 2024-09-17
// 将日期字符串(例如 “2022-07-08”)转换为 Date 对象
updatedDate: z.string().transform((str) => new Date(str)),
authorContact: z.string().email(),
canonicalURL: z.string().url(),
})
})
有关 Zod 如何工作及其可用功能的完整文档,请参阅 Zod 的 README

所有 Zod 模式方法(例如 .parse().transform())都可用,但有一些限制。特别是,使用 image().refine() 对图像执行自定义验证检查是不支持的。

集合条目还可以“引用”其他相关条目。这对于在集合模式中定义属性引用其他集合的条目非常有用。

使用集合 API 中的 reference() 函数,你可以将集合模式中的属性定义为来自另一个集合的条目。例如,你可以要求每个 space-shuttle 条目都包含一个 pilot 属性,该属性使用 pilot 集合自己的模式进行类型检查、自动补全和验证。

一个常见的例子是一个引用存储为 JSON 的可重用作者配置文件的博客文章,或者存储在同一集合中的相关文章 URL:

src/content.config.ts
import { defineCollection, reference, z } from 'astro:content';
const blog = defineCollection({
loader: glob({ pattern: '**/[^_]*.md', base: "./src/data/blog" }),
schema: z.object({
title: z.string(),
// 通过 `id` 从 `authors` 集合引用单个作者
author: reference('authors'),
// 通过 `slug` 从 `blog` 集合引用相关文章数组
relatedPosts: z.array(reference('blog')),
})
});
const authors = defineCollection({
loader: glob({ pattern: '**/[^_]*.json', base: "./src/data/authors" }),
schema: z.object({
name: z.string(),
portfolio: z.string().url(),
})
});
export const collections = { blog, authors };

这个博客文章示例指定了相关文章的 id 和作者的 id

src/data/blog/welcome.md
---
title: "Welcome to my blog"
author: ben-holmes # references `src/data/authors/ben-holmes.json`
relatedPosts:
- about-me # references `src/data/blog/about-me.md`
- my-year-in-review # references `src/data/blog/my-year-in-review.md`
---

当将 Markdown、MDX、Markdoc 或 JSON 文件与 glob() 加载器一起使用时,每个内容条目的 id 将根据内容文件名自动生成一个 URL 友好的格式。id 用于直接从你的集合中查询条目。它还在从内容创建新页面和 URL 时非常有用。

你可以通过在文件的 frontmatter 或 JSON 文件的数据对象中添加自己的 slug 属性来覆盖条目生成的 id。这类似于其他 Web 框架的 “permalink” 功能。

src/blog/1.md
---
title: My Blog Post
slug: my-custom-id/supports/slashes
---
Your blog post content here.
src/categories/1.json
{
"title": "My Category",
"slug": "my-custom-id/supports/slashes",
"description": "Your category description here."
}

Astor 提供辅助函数来查询集合并返回一个(或多个)内容条目。

它们返回具有唯一 id、包含所有定义属性的 data 对象,并且还将返回一个 body,其中包含 Markdown、MDX 或 Markdoc 文档的原始、未编译的正文。

import { getCollection, getEntry } from 'astro:content';
// 获取集合中的所有条目。
// 需要集合的名称作为参数。
const allBlogPosts = await getCollection('blog');
// 从集合中获取单个条目。
// 需要集合的名称和 `id`
const poodleData = await getEntry('dogs', 'poodle');
请参阅 CollectionEntry 类型 返回的完整属性列表。

在 Astro 模板中使用内容

段落标题 在 Astro 模板中使用内容

查询集合后,你可以直接在 Astro 组件模板中访问每个条目的内容。例如,你可以创建一个链接列表,显示条目的 frontmatter 信息,使用 data 属性。

src/pages/index.astro
---
import { getCollection } from 'astro:content';
const posts = await getCollection('blog');
---
<h1>My posts</h1>
<ul>
{posts.map(post => (
<li><a href={`/blog/${post.id}`}>{post.data.title}</a></li>
))}
</ul>

查询后,你可以使用 render() 函数属性将 Markdown 和 MDX 条目渲染为 HTML。调用此函数将使你可以访问渲染的 HTML 内容,包括 <Content /> 组件和所有已渲染标题的列表。

src/pages/blog/post-1.astro
---
import { getEntry, render } from 'astro:content';
const entry = await getEntry('blog', 'post-1');
const { Content, headings } = await render(entry);
---
<p>Published on: {entry.data.published.toDateString()}</p>
<Content />

将内容作为 props 传递

段落标题 将内容作为 props 传递

组件还可以将整个集合条目作为 prop 传递。

你可以使用 CollectionEntry 工具函数来使用 TypeScript 正确地为组件的 props 添加类型。此函数接受一个字符串参数,该参数与你的集合模式的名称匹配,并将继承该集合模式的所有属性。

src/components/BlogCard.astro
---
import type { CollectionEntry } from 'astro:content';
interface Props {
post: CollectionEntry<'blog'>;
}
// `post` 将匹配你的 'blog' 集合模式类型
const { post } = Astro.props;
---

getCollection() 采用一个可选的 “filter” 回调,允许你根据条目的 iddata 属性过滤查询。

你可以使用它来根据你喜欢的任何内容标准进行过滤。例如,你可以通过 draft 属性来过滤,以防止任何草稿博客文章发布到你的博客:

// 示例:使用 `draft: true` 过滤掉内容条目
import { getCollection } from 'astro:content';
const publishedBlogEntries = await getCollection('blog', ({ data }) => {
return data.draft !== true;
});

你也可以创建在运行开发服务器时可用但不在生产中构建的草稿页面。

// 示例:仅在为生产构建时过滤掉带有 `draft: true` 的内容条目
import { getCollection } from 'astro:content';
const blogEntries = await getCollection('blog', ({ data }) => {
return import.meta.env.PROD ? data.draft !== true : true;
});

过滤参数还支持按照集合中的嵌套目录进行过滤。由于 id 包含完整的嵌套路径,你可以通过每个 id 的开始来过滤,以仅返回特定嵌套目录中的条目:

// 示例:按集合中的子目录过滤条目
import { getCollection } from 'astro:content';
const englishDocsEntries = await getCollection('docs', ({ id }) => {
return id.startsWith('en/');
});

首次查询集合条目后,必须单独查询 模式中定义的任何引用。你可以使用 getEntry() 函数返回单个引用项,或者使用 getEntries() 从返回的 data 对象中检索多个引用条目。

src/pages/blog/welcome.astro
---
import { getEntry, getEntries } from 'astro:content';
const blogPost = await getEntry('blog', 'welcome');
// 解析单一引用
const author = await getEntry(blogPost.data.author);
// 解析引用数组
const relatedPosts = await getEntries(blogPost.data.relatedPosts);
---
<h1>{blogPost.data.title}</h1>
<p>Author: {author.data.name}</p>
<!-- ... -->
<h2>You might also like:</h2>
{relatedPosts.map(post => (
<a href={post.id}>{post.data.title}</a>
))}

内容集合存储在 src/pages/ 目录之外。这意味着默认情况下不会为集合条目生成页面或路由。

如果你想要为每个集合条目生成 HTML 页面或路由,例如单独的博客文章,你需要手动创建一个新的 动态路由。你的动态路由将把传入的请求参数(例如 Astro.params.slugsrc/pages/blog/[...slug].astro 中)映射到每个页面的正确条目。

生成路由的确切方法取决于你的页面是预渲染的(默认)还是由服务器按需渲染的。

构建静态输出(默认)

段落标题 构建静态输出(默认)

如果你正在构建一个静态网站(Astro 的默认行为),请使用 getStaticPaths() (EN) 函数在构建期间从单个页面组件(例如 src/pages/[slug])创建多个页面。

getStaticPaths() 中调用 getCollection(),以便在构建静态路由时为你的集合数据提供支持。然后,使用每个内容条目的 id 属性创建单独的 URL 路径。每个页面都将整个集合条目作为 prop 传递,以便在 页面模板中使用

src/pages/posts/[id].astro
---
import { getCollection, render } from 'astro:content';
// 1. 为每个集合条目生成一个新路径
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { id: post.id },
props: { post },
}));
}
// 2. 对于你的模板,你可以直接从 prop 获取条目
const { post } = Astro.props;
const { Content } = await render(post);
---
<h1>{post.data.title}</h1>
<Content />

这将为 blog 集合中的每个条目生成一个页面路由。例如,src/blog/hello-world.md 中的条目将具有 hello-worldid,因此其最终 URL 将是 /posts/hello-world/

构建服务器输出 (SSR)

段落标题 构建服务器输出 (SSR)

如果你正在构建动态网站(使用 Astro 的 SSR 支持),你不需要在构建期间提前生成任何路径。相反,你的页面应该在请求时检查请求(使用 Astro.requestAstro.params)以找到 slug,然后使用 getEntry() 来按需请求它。

src/pages/posts/[id].astro
---
import { getEntry, render } from "astro:content";
// 1. 从传入的服务器请求中获取 slug
const { id } = Astro.params;
if (id === undefined) {
return Astro.redirect("/404");
}
// 2. 直接使用请求 slug 查询条目
const post = await getEntry("blog", id);
// 3. 如果条目不存在则重定向
if (post === undefined) {
return Astro.redirect("/404");
}
// 4. 将条​​目渲染为模板中的 HTML
const { Content } = await render(post);
---
<h1>{post.data.title}</h1>
<Content />

只要你拥有一组共享相同结构的相关数据或内容,你就可以创建一个集合

使用集合的大部分好处来自于:

  • 定义通用数据形态,以验证单个条目是否 “正确” 或 “完整”,避免在生产中出现错误。
  • 以内容为中心的 API 旨在当在页面上导入和渲染内容时,使查询变得直观(例如 getCollection() 而不是 import.meta.glob())。
  • 用于检索内容的 内容加载器 API (EN),提供了内置加载器和可用的底层 API。这里有一些第三方和社区构建的加载器可用,你可以构建自己的自定义加载器,从任何地方获取数据。
  • 性能和可扩展性。内容层 API 允许数据在构建之间缓存,并适用于数万条内容条目。

在以下情况下,将 数据定义为集合

  • 你有多个文件或数据需要组织,共享相同的整体结构(例如,使用 Markdown 编写的博客文章,这些文章都具有相同的 frontmatter 属性)。
  • 你有远程存储的现有内容,例如在 CMS 中,想要利用集合辅助函数和内容层 API,而不是使用 fetch() 或 SDK。
  • 你需要请求(数万)个相关数据,并且需要一个能够处理大规模查询和缓存的方法。

什么时候不要使用集合

段落标题 什么时候不要使用集合

当你有 多个内容需要共享相同的属性 时,集合提供了出色的结构、安全性和组织。

以下情况,集合 可能不是你的解决方案

  • 你只有一个或少量不同的页面。考虑直接使用你的内容 创建单独的页面组件,例如 src/pages/about.astro
  • 你正在显示不由 Astro 处理的文件,例如 PDF。将这些静态资产放在项目的 public/ 目录中。
  • 你的数据源有自己的 SDK/客户端库用于导入,与内容加载器不兼容或不提供内容加载器,并且你更喜欢直接使用它。
  • 你正在使用需要实时更新的 API。内容集合仅在构建时更新,因此如果你需要实时数据,请使用其他方法 导入文件 或 使用 按需渲染 (EN)请求数据
贡献 社区 赞助