内容集合
添加于:
astro@2.0.0
在任何 Astro 项目中,内容集合是使用 Markdown 和 MDX 的最佳方式。内容集合帮助管理你的文档,校验 frontmatter,并为所有内容提供自动 TypeScript 类型安全。
什么是内容集合?
段落标题 什么是内容集合?一个内容集合是保留在 src/content
目录中的任何顶级目录,比如 src/content/newsletter
和 src/content/authors
。内容集合只允许在 src/content
目录中。此目录不能用于其他用处。
一个内容条目是存储在内容集合目录中的任何内容片段。内容条目存储在 Markdown (.md
) 或 MDX (.mdx
) 文件中。你可以使用任何想用的文件名,但是我们建议使用一致的命名方案 (小写,破折号而不是空格) ,以便更容易地查找和组织内容。条目可以使用内容文件的格式,包括 Markdown (.md
) 和 MDX (.mdx
使用 MDX 集成) 或者被支持的二者之一的数据格式:YAML (.yaml
) 和 JSON (.json
)。我们建议为文件使用一致的命名方案(小写、破折号而不是空格),以便更轻松地查找和组织你的内容,但这不是必须的。你还可以通过在文件名前添加下划线 (_) 来从构建中排除条目。
目录src/content/
目录newsletter/ “newsletter” 集合
- week-1.md 一个集合条目
- week-2.md 一个集合条目
- week-3.md 一个集合条目
一旦有了一个集合,你就可以使用 Astro 的内置内容 API 开始查询集合。
“.astro” 目录
段落标题 “.astro” 目录Astro 将内容集合的重要元数据存储在项目中的 .astro
目录。你不需要采取任何措施来维护或更新此目录。我们希望你在项目中完全忽略它。
只要运行astro dev
,astro build
命令,.astro
目录就会自动更新。你可以随时运行astro sync
来手动更新 .astro
目录。
如果你正在使用 Git 进行版本控制,我们建议通过在 .gitignore
文件中添加 .astro
来忽略 .astro
目录。这告诉 Git 忽略这个目录及其中的任何文件。
echo "\n.astro" >> .gitignore
组织多个集合
段落标题 组织多个集合如果两个文件表示不同类型的内容(例如,一篇博客文章和一个作者描述文件),那么它们很可能应属于不同的集合。这一点非常重要,因为许多特性(frontmatter 验证、自动 TypeScript 类型安全)要求集合中的所有条目共享相似的结构。
如果你发现自己正在处理不同类型的内容,则应该创建多个集合来表示每种类型。你可以在项目中创建任意多个不同的集合。
目录src/content/
目录newsletter/
- week-1.md
- week-2.md
目录blog/
- post-1.md
- post-2.md
目录authors/
- grace-hopper.json
- alan-turing.json
使用子目录进行组织
段落标题 使用子目录进行组织内容集合始终是 src/content/
目录中的顶级文件夹。你不能将一个集合嵌套在另一个集合中。不过,你可以使用子目录来组织集合中的内容。
例如,可以使用以下目录结构在单个 docs
集合中组织 i18n 翻译。当查询这个集合时,你将能够使用文件路径按语言过滤结果。
目录src/content/
目录docs/ 此集合使用子目录按语言进行组织
目录en/
- …
目录es/
- …
目录de/
- …
定义集合
段落标题 定义集合src/content/config.ts
文件是可选的。但是,选择不定义集合将禁用它们的一些最佳特性,如 frontmatter schema 验证或自动 TypeScript 类型。
要充分利用内容集合,请在项目中创建一个 src/content/config.ts
文件(也支持 .js
和 .mjs
)。这是一个特殊的文件,Astro 将自动加载并使用它来配置你的内容集合。
// 1. 从 `astro:content` 导入import { defineCollection } from 'astro:content';// 2. 定义集合const blogCollection = defineCollection({ /* ... */ });// 3. 导出一个 `collections` 对象来注册集合// 这个键应该与 `src/content` 中的集合目录名匹配export const collections = { 'blog': blogCollection,};
设置 TypeScript
段落标题 设置 TypeScript如果你没有已经在 tsconfig.json
文件中扩展 Astro 推荐的 strict
或 strictest
的 TypeScript 设置,你可能需要更新 tsconfig.json
来启用 strictNullChecks
。
{ // 注意:如果使用 `astro/tsconfigs/strictest` 或 `astro/tsconfigs/strictest`,则不需要更改 "extends": "astro/tsconfigs/base", "compilerOptions": { "strictNullChecks": true }}
如果在 Astro 项目中使用 .js
或者 .mjs
文件,你可以启用 IntelliSense,并在编辑器中通过在 tsconfig.json
中启用 allowJs
来进行类型检查:
{ // 注意:如果使用 `astro/tsconfigs/strictest` 或 `astro/tsconfigs/strictest`,则不需要更改 "extends": "astro/tsconfigs/base", "compilerOptions": { "strictNullChecks": true, "allowJs": true }}
定义集合模式
段落标题 定义集合模式schema 在集合中强制执行一致的 frontmatter。schema 保证当你需要引用或查询它时,你的 frontmatter 以可预测的形式存在。如果任何文件违反了它的集合 schema,Astro 将提供一个有用的错误让你知道。
schema 还使 Astro 能为内容自动生成 TypeScript 类型。为集合定义 schema 时,Astro 将自动生成并向其应用 TypeScript 接口。查询集合时,结果是完全支持 TypeScript,包括属性自动完成和类型检查。
要定义你的第一个内容 schema,如果该文件不存在的话,新建一个 src/content/config.ts
文件(.js
和 .mjs
后缀也是支持的)。此文件应该:
- 从
astro:content
导入适当的工具。 - 定义要验证的每个集合。这包括一个
type
(Astro v2.5.0 中引入) 指定集合是否包含像 Markdown (type: 'content'
) 的内容创作格式或是像 JSON 及 YAML (type: 'data'
) 的数据格式。同样包含一个用来定义 frontmatter 形式或条目数据的schema
。 - 导出一个
collections
对象来注册你的集合。
// 1. 从 `astro:content` 导入适当的工具。import { z, defineCollection } from 'astro:content';
// 2. 定义要用 schema 验证的每个集合。const blogCollection = defineCollection({ type: 'content', // v2.5.0 及之后 schema: z.object({ title: z.string(), tags: z.array(z.string()), image: z.string().optional(), }),});
// 3. 导出一个 `collections` 对象来注册你的集合。export const collections = { 'blog': blogCollection,};
定义多个集合
段落标题 定义多个集合你可以多次使用 defineCollection()
创建多个 schema。所有集合必须从这个 collections
对象中导出。
const blogCollection = defineCollection({ type: 'content', schema: z.object({ /* ... */ })});const newsletter = defineCollection({ type: 'content', schema: z.object({ /* ... */ })});const authors = defineCollection({ type: 'data', schema: z.object({ /* ... */ })});
export const collections = { 'blog': blogCollection, 'newsletter': newsletter, 'authors': authors,};
随着项目的增长,你还可以自由地重新组织代码库,并将逻辑移出 src/content/config.ts
文件。分别定义 schema 对于跨多个集合重用 schema 和与项目的其他部分共享 schema 非常有用。
// 1. 导入你的实用工具和 schemaimport { defineCollection } from 'astro:content';import { blogSchema, authorSchema } from '../schemas';
// 2. 定义你的集合const blogCollection = defineCollection({ type: 'content', schema: blogSchema,});const authorCollection = defineCollection({ type: 'data', schema: authorSchema,});
// 3. 导出多个集合以注册它们export const collections = { 'blog': blogCollection, 'authors': authorCollection,};
使用第三方集合 schema
段落标题 使用第三方集合 schema你可以从任何地方导入集合 schema,包括外部 npm 包。这在使用提供了它们自己的集合 schema 的主题和库时非常有用。
import { blogSchema } from 'my-blog-theme';const blogCollection = defineCollection({ type: 'content', schema: blogSchema });
// 导出从 'my-blog-theme' 导入的 schema 为 `blog` 集合export const collections = { 'blog': blogCollection,};
用 Zod 定义数据类型
段落标题 用 Zod 定义数据类型Astro 使用 Zod 驱动其内容 schema。Astro 使用 Zod 能够验证集合中每个文件的 frontmatter,并在从项目内部查询内容时提供自动的 TypeScript 类型。
要在 Astro 中使用 Zod,请从 "Astro:content"
中导入 z
。这是 Zod 库的重新导出,它支持 Zod 的所有特性。查看Zod 的 README 获得关于 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()), // 一个可选的 frontmatter 属性。非常普遍! footnote: z.string().optional(), // 在 frontmatter 中,周围没有引号的日期被解释为 Date 对象 publishDate: z.date(), // 可以将日期字符串 (例如 "2022-07-08") 转换为 Date 对象 // publishDate: z.string().transform((str) => new Date(str)), //Advanced: 验证字符串是否也是电子邮件 authorContact: z.string().email(), // Advanced: 验证该字符串也是一个 URL canonicalURL: z.string().url(), })})
定义集合引用
段落标题 定义集合引用集合条目也可以”引用”其他相关条目。
使用集合 API 中的 reference()
函数,可以将集合 schema 中的属性定义为另一个集合中的条目。例如,你可以要求每个 space-shuttle
条目都包含一个 pilot
属性,该属性使用 pilot
集合自己的 schema 进行类型检查、自动填充和验证。
一个常见的示例是引用以 JSON 形式存储的可重用作者个人资料或存储在同一集合中的相关帖子 URL 的博客文章:
import { defineCollection, reference, z } from 'astro:content';
const blog = defineCollection({ type: 'content', schema: z.object({ title: z.string(), // 通过 `id` 引用 `authors` 集合中的单个作者 author: reference('authors'), // 通过 `slug` 引用 `blog` 集合中的相关帖子数组 relatedPosts: z.array(reference('blog')), })});
const authors = defineCollection({ type: 'data', schema: z.object({ name: z.string(), portfolio: z.string().url(), })});
export const collections = { blog, authors };
此示例博客文章指定相关帖子的 slug
和帖子作者的 id
:
---title: "Welcome to my blog"author: ben-holmes # 引用 `src/content/authors/ben-holmes.json`relatedPosts:- about-me # 引用 `src/content/blog/about-me.md`- my-year-in-review # 引用 `src/content/blog/my-year-in-review.md`---
定义自定义 slugs
段落标题 定义自定义 slugs当使用 type: 'content'
时,每个内容条目从它的文件 id
生成一个 URL 友好的 slug
属性。slug 用于直接从集合中查询条目。在根据内容创建新页面和 URL 时,它也很有用。
你可以通过将自己的 slug
属性添加到文件 frontmatter 来覆盖条目生成的 slug。这类似于其他 Web 框架的 “permalink” 特性。"slug"
是一个特殊的保留属性名称,不允许出现在自定义集合 schema
中,也不会出现在条目的 data
属性中。
---title: 我的博客文章slug: my-custom-slug/supports/slashes---你的博客文章内容在这里。
查询集合
段落标题 查询集合Astro 提供了两个函数来查询一个集合并返回一个(或多个)内容条目:getCollection()
和 getEntry()
。
import { getCollection, getEntry } from 'astro:content';
// 获取集合中的所有条目。// 需要集合的名称作为参数。// 示例:检索 `src/content/blog/**`const allBlogPosts = await getCollection('blog');
// 从集合中获取单个条目。// 需要集合的名称以及// 条目 `slug`(内容集合)或 `id`(数据集合)// 示例:检索 `src/content/authors/grace-hopper.json`const graceHopperProfile = await getEntry('authors', 'grace-hopper');
这两个函数都返回由 CollectionEntry
类型定义的内容条目。
使用引用的数据
段落标题 使用引用的数据首次查询集合条目后,必须再单独查询 schema 中定义的引用。你可以再次使用 getEntry()
函数或 getEntries()
从返回的 data
对象中检索引用的条目。
---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.data.name}</p>
<!-- ... -->
<h2>你可能也喜欢:</h2>{relatedPosts.map(p => ( <a href={p.slug}>{p.data.title}</a>))}
筛选集合查询
段落标题 筛选集合查询getCollection()
接受一个可选的 “filter” 回调,它允许你基于条目的 id
、slug
或 data
(frontmatter) 属性对查询进行过滤。对于 type: content
的集合,你也可以通过 slug
来过滤。
slug
属性特定于内容集合,在过滤 JSON 或 YAML 集合时不可用。
你可以使用它根据你喜欢的任何内容条件进行筛选。例如,你可以通过前端属性(如 draft
)进行过滤,以防止任何博客文章草稿发布到你的博客上:
// 例子:使用 `draft: true` 过滤内容条目import { getCollection } from 'astro:content';const draftBlogEntries = 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;});
filter 参数还支持按集合中的嵌套目录进行筛选。由于 id
包含完整的嵌套路径,因此可以根据每个 id
的开头进行筛选,只返回特定嵌套目录中的项目:
// 例子:按集合中的子目录筛选条目import { getCollection } from 'astro:content';const englishDocsEntries = await getCollection('docs', ({ id }) => { return id.startsWith('en/');});
使用 Astro 模板中的内容
段落标题 使用 Astro 模板中的内容查询完集合条目后,可以直接在 Astro 组件模板内访问每个条目。这使你可以呈现诸如内容链接(使用内容 slug
)或关于内容的信息(使用 data
属性)之类的 HTML。
有关将内容呈现为 HTML 的信息,请参见下面的将内容渲染成 HTML。
---import { getCollection } from 'astro:content';const blogEntries = await getCollection('blog');---<ul> {blogEntries.map(blogPostEntry => ( <li> <a href={`/my-blog-url/${blogPostEntry.slug}`}>{blogPostEntry.data.title}</a> <time datetime={blogPostEntry.data.publishedDate.toISOString()}> {blogPostEntry.data.publishedDate.toDateString()} </time> </li> ))}</ul>
将内容作为属性传递
段落标题 将内容作为属性传递组件还可以将整个内容条目作为属性传递。
如果这样做,你可以使用 CollectionEntry
实用工具使用 TypeScript 正确地输入组件属性。此实用工具接受与集合 schema 名称匹配的字符串参数,并将继承该集合 schema 的所有属性。
---import type { CollectionEntry } from 'astro:content';interface Props { post: CollectionEntry<'blog'>;}
// `post` 将匹配 `blog` 集合 schema 类型const { post } = Astro.props;---
将内容渲染成 HTML
段落标题 将内容渲染成 HTML一旦查询,你可以使用 render()
函数属性将集合条目渲染为 HTML。调用此函数可以访问渲染的内容和元数据,包括 <Content/>
组件和所有呈现标题的列表。
---import { getEntry } from 'astro:content';const entry = await getEntry('blog', 'post-1');const { Content, headings } = await entry.render();---<p>发布于:{entry.data.published.toDateString()}</p><Content />
从内容生成路由
段落标题 从内容生成路由内容集合存储在 src/pages/
目录之外。这意味着默认情况下不为集合项生成路由。你将需要手动创建一个新的动态路由,以便从集合条目生成 HTML 页面。动态路由将映射传入的请求参数(例如:在 src/pages/blog/[...slug].astro
中的 Astro.params.slug
)获取集合中的正确条目。
生成路由的确切方法将取决于你的构建 output
模式:static
(默认值) 或 server
(针对 SSR)。
构建为静态输出(默认)
段落标题 构建为静态输出(默认)如果你正在构建一个静态网站(Astro 的默认行为),你可以使用 getStaticPaths()
函数在构建过程中从一个 src/pages/
组件创建多个页面。
调用 getStaticPaths()
中的 getCollection()
来查询内容。然后,使用每个内容条目的 slug
属性创建新的 URL 路径。
---import { getCollection } from 'astro:content';// 1. 为每个集合条目生成一个新路径export async function getStaticPaths() { const blogEntries = await getCollection('blog'); return blogEntries.map(entry => ({ params: { slug: entry.slug }, props: { entry }, }));}// 2. 当渲染的时候,你可以直接从属性中得到条目const { entry } = Astro.props;const { Content } = await entry.render();---<h1>{entry.data.title}</h1><Content />
这将为 blog
集合中的每个条目生成一个新页面。例如,src/content/blog/hello-world.md
上的条目将有一个 hello-world
的 slug,因此它的最终 URL 将是 post/hello-world/
。
如果你的自定义 slug 包含 /
字符来生成具有多个路径段的 URL,则必须使用 rest 参数 ([...slug]
)在此动态路由页面的 .astro
文件名中。
构建为服务器输出(SSR)
段落标题 构建为服务器输出(SSR)如果你正在构建一个动态网站(使用 Astro 的 SSR 支持),则不需要在构建期间提前生成任何路径。相反,你的页面应该检查请求(使用 Astro.request
或 Astro.params
)以按需找到 slug
,然后使用 getEntry()
获取它。
---import { getEntry } from 'astro:content';// 1. 从传入的服务器请求中获取 slugconst { slug } = Astro.params;if (slug === undefined) { throw new Error("Slug is required");}// 2. 直接使用请求 slug 查询条目const entry = await getEntry('blog', slug);// 3. 如果条目不存在,则重定向到 404if (entry === undefined) { return Astro.redirect("/404");}// 4. (可选)在模板中将条目呈现为 HTMLconst { Content } = await entry.render();---
从基于文件的路由迁移
段落标题 从基于文件的路由迁移如果你已经有了一个现成的 Astro 项目,比如一个博客,它使用 Markdown 或 MDX 文件在 src/pages/
内部的子文件夹中,可以考虑将相关内容或数据文件迁移到内容集合中。
请参考我们的手把手教程中的示例,了解如何将位于 src/pages/posts/
的基本博客示例转换为 src/content/posts
目录下的示例。该教程使用了构建博客教程的完整项目的代码库。
启用构建缓存
段落标题 启用构建缓存
添加于:
astro@3.5.0
实验性
如果你正在处理大量的集合,你可能希望通过 experimental.contentCollectionCache
标志启用缓存的构建。这个实验性的功能优化了 Astro 的构建过程,使得未改变的集合能够在构建之间被存储和复用。
在许多情况下,这可以导致显著的构建性能提升。
虽然这个功能正在稳定中,但你也可能会遇到与存储缓存有关的问题。所以你始终可以通过运行以下命令重置你的构建缓存:
npm run astro build -- --force
用 Remark 修改 Frontmatter
段落标题 用 Remark 修改 Frontmatter不推荐。Remark 和 rehyp 插件访问原始 Markdown 或 MDX 文档的 frontmatter。这就意味着 remarkPluginFrontmatter
的 frontmatter 与类型安全的 schema
是分开处理的,并且不会反映通过 Astro 应用的任何更改或缺省值。使用风险自负!
Astro 支持 remark 或者 rehype 插件直接修改 frontmatter。你可以通过使用从 render()
返回的 remarkPluginFrontmatter
属性在一个内容条目中访问这个修改过的 Frontmatter:
---import { getEntry } from 'astro:content';const blogPost = await getEntry('blog', 'post-1');const { remarkPluginFrontmatter } = await blogPost.render();---<p>{blogPost.data.title} — {remarkPluginFrontmatter.readingTime}</p>
![](/houston_chef.webp)
Remark 和 rehype pipeline 只在渲染内容时运行,这就解释了为什么只有在对内容条目调用 render()
之后才可以使用 remarkPluginFrontmatter
。相比之下,getCollection()
和 getEntry()
无法直接返回这些值,因为它们不会渲染内容。
在 frontmatter 中处理日期
段落标题 在 frontmatter 中处理日期内容集合中可以使用多种日期格式,但你的集合 schema 必须匹配你在 Markdown 或 MDX YAML 中 frontmatter 所使用的格式。
YAML 使用 ISO-8601 标准来表示日期,其以 yyyy-mm-dd
格式(例如 2021-07-28
)表示并配合 z.date()
的 schema 类型来使用:
---title: 我的博客文章pubDate: 2021-07-08---
如果没有提供时区,日期格式将指定为 UTC 格式。如果你需要指定一个时区,你可以使用 ISO 8601 格式。
---title: 我的博客文章pubDate: 2021-07-08T12:00:00-04:00---
想要仅渲染完整 UTC 时间戳中的 YYYY-MM-DD
,可以使用 JavaScript 的 slice
方法来移除时间戳:
---const { frontmatter } = Astro.props;---<h1>{frontmatter.title}</h1><p>{frontmatter.pubDate.toString().slice(0,10)}</p>
要查看使用 toLocaleDateString
格式化日期、月份和年份的例子,请查看官方 Astro 博客模板中的 <FormattedDate />
组件。