跳转到内容

内容集合

新增于: astro@2.0.0

内容集合是在任何 Astro 项目中管理内容集的最佳方式。集合有助于组织和查询你的文档,在编辑器中启用智能提示和类型检查,并为你的所有内容提供自动的 TypeScript 类型安全。Astro v5.0 引入了内容层 API,用于定义和查询内容集合。这个高性能、可扩展的 API 为你的本地集合提供了内置的内容加载器。对于远程内容,你可以使用第三方和社区构建的加载器,或者创建自己的自定义加载器,从任何来源拉取数据。

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

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

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

通过适当的集合加载器,你可以从任何外部来源(如 CMS、数据库或无头支付系统)获取远程数据。

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

tsconfig.json
{
// Included with "astro/tsconfigs/strict" or "astro/tsconfigs/strictest"
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"strictNullChecks": true, // add if using `base` template
"allowJs": true // required, and included with all Astro templates
}
}

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

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

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

src/content.config.ts
// 1. Import utilities from `astro:content`
import { defineCollection, z } from 'astro:content';
// 2. Import loader(s)
import { glob, file } from 'astro/loaders';
// 3. Define your collection(s)
const blog = defineCollection({ /* ... */ });
const dogs = defineCollection({ /* ... */ });
// 4. Export a single `collections` object to register your collection(s)
export const collections = { blog, dogs };

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

Astro 为获取本地内容提供了两个内置加载器函数glob()file()),同时还提供了 API 访问权限,以构建你自己的加载器并获取远程数据。

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

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

src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob, file } from 'astro/loaders'; // Not available with legacy 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` can accept an array of multiple patterns as well as string patterns
// Load all markdown files in the space-probes directory, except for those that start with "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 函数。这允许你指定一个自定义解析器(例如 csv-parse)来从文件内容创建一个集合。

file() 加载器将自动检测并解析(根据文件扩展名)来自 JSON 和 YAML 文件的单个对象数组,并将 TOML 文件中的每个顶级表视为一个独立的条目。对这些文件类型的支持是内置的,除非你有嵌套的 JSON 文档,否则不需要 parser。要使用其他文件,如 .csv,你需要创建一个解析器函数。

以下示例展示了如何导入一个 CSV 解析器,然后通过向 file() 加载器传递文件路径和 parser 函数,将一个 cats 集合加载到你的项目中

src/content.config.ts
import { defineCollection } from "astro:content";
import { file } from "astro/loaders";
import { parse as parseCsv } from "csv-parse/sync";
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": [{}]}

你可以通过为每个集合向 file() 加载器传递一个自定义 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();
// Must return an array of entries with an id property, or an object with IDs as keys and entries as values
return data.map((country) => ({
id: country.cca3,
...country,
}));
},
schema: /* ... */
});

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

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

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

查看完整的内容加载器 API以及如何构建自己的加载器的示例。

模式通过 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'; // Not available with legacy 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 };

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

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

// Example: A cheatsheet of many common Zod datatypes
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(),
// In YAML, dates written without quotes around them are interpreted as Date objects
publishDate: z.date(), // e.g. 2024-09-17
// Transform a date string (e.g. "2022-07-08") to a Date object
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';
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({ pattern: '**/[^_]*.md', base: "./src/data/blog" }),
schema: z.object({
title: z.string(),
// Reference a single author from the `authors` collection by `id`
author: reference('authors'),
// Reference an array of related posts from the `blog` collection by `slug`
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`
---

这些引用将被转换为包含 collection 键和 id 键的对象,让您可以轻松地在您的模板中查询它们

当使用 glob() 加载器处理 Markdown、MDX、Markdoc 或 JSON 文件时,每个内容条目的id都会根据内容文件名自动生成为 URL 友好的格式。id 用于直接从集合中查询条目。在从内容创建新页面和 URL 时也很有用。

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

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."
}

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

这些函数返回的条目包含一个唯一的 id、一个包含所有已定义属性的 data 对象,并且对于 Markdown、MDX 或 Markdoc 文档,还会返回一个包含原始、未编译正文的 body

import { getCollection, getEntry } from 'astro:content';
// Get all entries from a collection.
// Requires the name of the collection as an argument.
const allBlogPosts = await getCollection('blog');
// Get a single entry from a collection.
// Requires the name of the collection and `id`
const poodleData = await getEntry('dogs', 'poodle');

生成的集合的排序顺序是不确定的且依赖于平台。这意味着如果您调用 getCollection() 并需要按特定顺序返回条目(例如,按日期排序的博客文章),您必须自己对集合条目进行排序。

src/pages/blog.astro
---
import { getCollection } from 'astro:content';
const posts = (await getCollection('blog')).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
---
查看 CollectionEntry 类型返回的属性的完整列表。

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

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');
if (!entry) {
// Handle Error, for example:
throw new Error('Could not find blog post 1');
}
const { Content, headings } = await render(entry);
---
<p>Published on: {entry.data.published.toDateString()}</p>
<Content />

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

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

src/components/BlogCard.astro
---
import type { CollectionEntry } from 'astro:content';
interface Props {
post: CollectionEntry<'blog'>;
}
// `post` will match your 'blog' collection schema type
const { post } = Astro.props;
---

getCollection() 接受一个可选的“过滤器”回调函数,允许您根据条目的 iddata 属性来过滤查询。

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

// Example: Filter out content entries with `draft: true`
import { getCollection } from 'astro:content';
const publishedBlogEntries = await getCollection('blog', ({ data }) => {
return data.draft !== true;
});

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

// Example: Filter out content entries with `draft: true` only when building for production
import { getCollection } from 'astro:content';
const blogEntries = await getCollection('blog', ({ data }) => {
return import.meta.env.PROD ? data.draft !== true : true;
});

过滤器参数还支持按集合中的嵌套目录进行过滤。由于 id 包含完整的嵌套路径,因此您可以通过过滤每个 id 的开头来仅返回特定嵌套目录中的项目

// Example: Filter entries by sub-directory in the collection
import { getCollection } from 'astro:content';
const englishDocsEntries = await getCollection('docs', ({ id }) => {
return id.startsWith('en/');
});

在首次查询集合条目后,任何在您的模式中定义的引用都必须单独查询。由于 reference() 函数将引用转换为一个包含 collectionid 键的对象,因此您可以使用 getEntry() 函数返回单个引用的项目,或使用 getEntries() 从返回的 data 对象中检索多个引用的条目。

src/pages/blog/welcome.astro
---
import { getEntry, getEntries } from 'astro:content';
const blogPost = await getEntry('blog', 'welcome');
// Resolve a singular reference (e.g. `{collection: "authors", id: "ben-holmes"}`)
const author = await getEntry(blogPost.data.author);
// Resolve an array of references
// (e.g. `[{collection: "blog", id: "about-me"}, {collection: "blog", id: "my-year-in-review"}]`)
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 页面,您需要手动创建一个新的动态路由。您的动态路由将映射传入的请求参数(例如 src/pages/blog/[...slug].astro 中的 Astro.params.slug)以获取每个页面的正确条目。

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

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

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

src/pages/posts/[id].astro
---
import { getCollection, render } from 'astro:content';
// 1. Generate a new path for every collection entry
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { id: post.id },
props: { post },
}));
}
// 2. For your template, you can get the entry directly from the 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/

如果您正在构建一个动态网站(使用 Astro 的 SSR 支持),则在构建期间不需要预先生成任何路径。相反,您的页面应该检查请求(使用 Astro.requestAstro.params)以按需查找 slug,然后使用 getEntry() 获取它。

src/pages/posts/[id].astro
---
import { getEntry, render } from "astro:content";
// 1. Get the slug from the incoming server request
const { id } = Astro.params;
if (id === undefined) {
return Astro.redirect("/404");
}
// 2. Query for the entry directly using the request slug
const post = await getEntry("blog", id);
// 3. Redirect if the entry does not exist
if (post === undefined) {
return Astro.redirect("/404");
}
// 4. Render the entry to HTML in the template
const { Content } = await render(post);
---
<h1>{post.data.title}</h1>
<Content />

任何时候,当你有一组共享共同结构的相关数据或内容时,你都可以创建一个集合

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

  • 定义一个通用的数据结构,以验证单个条目是否“正确”或“完整”,从而避免生产环境中的错误。
  • 以内容为中心的 API,旨在使查询更直观(例如,使用 getCollection() 而不是 import.meta.glob()),以便在页面上导入和渲染内容。
  • 一个内容加载器 API,用于检索您的内容,它提供了内置加载器和对低级 API 的访问。有几个第三方和社区构建的加载器可用,您也可以构建自己的自定义加载器从任何地方获取数据。
  • 性能和可扩展性。内容层 API 允许数据在构建之间被缓存,适用于数以万计的内容条目。

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

  • 您有多个文件或数据需要组织,它们共享相同的整体结构(例如,用 Markdown 写的博客文章,都有相同的前置元数据属性)。
  • 您有现有的内容存储在远程,例如在 CMS 中,并且希望利用集合辅助函数和内容层 API,而不是使用 fetch() 或 SDK。
  • 你需要获取(数以万计的)相关数据,并且需要一种能够处理大规模数据的查询和缓存方法。

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

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

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