跳转到内容

可选:创建一个内容集合

现在你已经有了一个使用 Astro 内置的基于文件的路由的博客,你将更新它以使用内容集合。内容集合是管理类似内容组(例如博客文章)的强大方式。

准备好去……

  • 将你的博客文章文件夹移动到 src/blog/
  • 创建一个 schema 来定义你的博客文章 frontmatter
  • 使用 getCollection() 获取博客文章内容和元数据

即使在使用内容集合时,你仍然会使用 src/pages/ 文件夹来存放单个页面,例如“关于我”页面。但是,将博客文章移出这个特殊的文件夹,可以让你使用更强大、性能更好的 API 来生成博客文章索引和显示单篇博客文章。

同时,你会在代码编辑器中获得更好的指导和自动补全,因为你将拥有一个schema 来为每篇文章定义一个通用结构,Astro 将通过 Zod(一个用于 TypeScript 的 schema 声明和验证库)帮助你强制执行该结构。在你的 schema 中,你可以指定 frontmatter 属性何时是必需的(例如 description 或 author),以及每个属性必须是哪种数据类型(例如字符串或数组)。这有助于更早地发现许多错误,并提供描述性的错误消息,准确地告诉你问题所在。

在我们的指南中阅读更多关于Astro 的内容集合的信息,或者按照下面的说明开始,将一个基本的博客从 src/pages/posts/ 转换为 src/blog/

  1. 你可能会把哪种类型的页面保留在 src/pages/ 中?

  2. 哪项是将博客文章移至内容集合的好处?

  3. 内容集合使用 TypeScript ……

下面的步骤向你展示了如何通过为博客文章创建一个内容集合来扩展“构建博客”教程的最终产品。

通过在终端中运行以下命令,升级到 Astro 的最新版本,并将所有集成升级到其最新版本

终端窗口
# Upgrade Astro and official integrations together
npx @astrojs/upgrade
  1. 创建一个名为 src/blog/ 的新 内容集合(文件夹)。

  2. 将所有现有的博客文章(.md 文件)从 src/pages/posts/ 移动到这个新的内容集合中。

  3. 创建一个 src/content.config.ts 文件,为你的 postsCollection 定义一个 schema。对于现有的博客教程代码,将以下内容添加到文件中,以定义其博客文章中使用的所有 frontmatter 属性

    src/content.config.ts
    // Import the glob loader
    import { glob } from "astro/loaders";
    // Import utilities from `astro:content`
    import { z, defineCollection } from "astro:content";
    // Define a `loader` and `schema` for each collection
    const blog = defineCollection({
    loader: glob({ pattern: '**/[^_]*.md', base: "./src/blog" }),
    schema: z.object({
    title: z.string(),
    pubDate: z.date(),
    description: z.string(),
    author: z.string(),
    image: z.object({
    url: z.string(),
    alt: z.string()
    }),
    tags: z.array(z.string())
    })
    });
    // Export a single `collections` object to register your collection(s)
    export const collections = { blog };
  4. 为了让 Astro 识别你的 schema,请退出(CTRL + C)并重启开发服务器以继续本教程。这将定义 astro:content 模块。

  1. 创建一个名为 src/pages/posts/[...slug].astro 的页面文件。当你的 Markdown 和 MDX 文件位于内容集合中时,它们不再通过 Astro 的基于文件的路由自动成为页面,因此你必须创建一个负责生成每个独立博客文章的页面。

  2. 添加以下代码来查询你的内容集合,以使每篇博客文章的 slug 和页面内容对它将生成的每个页面都可用

    src/pages/posts/[...slug].astro
    ---
    import { getCollection, render } from 'astro:content';
    export async function getStaticPaths() {
    const posts = await getCollection('blog');
    return posts.map(post => ({
    params: { slug: post.id }, props: { post },
    }));
    }
    const { post } = Astro.props;
    const { Content } = await render(post);
    ---
  3. 在 Markdown 页面的布局中渲染你的文章 <Content />。这允许你为所有文章指定一个通用的布局。

    src/pages/posts/[...slug].astro
    ---
    import { getCollection, render } from 'astro:content';
    import MarkdownPostLayout from '../../layouts/MarkdownPostLayout.astro';
    export async function getStaticPaths() {
    const posts = await getCollection('blog');
    return posts.map(post => ({
    params: { slug: post.id }, props: { post },
    }));
    }
    const { post } = Astro.props;
    const { Content } = await render(post);
    ---
    <MarkdownPostLayout frontmatter={post.data}>
    <Content />
    </MarkdownPostLayout>
  4. 删除每篇文章 frontmatter 中的 layout 定义。你的内容现在在渲染时被布局包裹,因此不再需要此属性。

    src/content/posts/post-1.md
    ---
    layout: ../../layouts/MarkdownPostLayout.astro
    title: 'My First Blog Post'
    pubDate: 2022-07-01
    ...
    ---
  1. 在你所有展示博客文章列表的地方,比如教程的博客页面(src/pages/blog.astro/),你需要将 import.meta.glob() 替换为 getCollection() 来从 Markdown 文件中获取内容和元数据。

    src/pages/blog.astro
    ---
    import { getCollection } from "astro:content";
    import BaseLayout from "../layouts/BaseLayout.astro";
    import BlogPost from "../components/BlogPost.astro";
    const pageTitle = "My Astro Learning Blog";
    const allPosts = Object.values(import.meta.glob("../pages/posts/*.md", { eager: true }));
    const allPosts = await getCollection("blog");
    ---
  2. 你还需要更新对每个 post 返回数据的引用。现在你可以在每个对象的 data 属性上找到你的 frontmatter 值。此外,使用内容集合时,每个 post 对象将有一个页面 slug,而不是一个完整的 URL。

    src/pages/blog.astro
    ---
    import { getCollection } from "astro:content";
    import BaseLayout from "../layouts/BaseLayout.astro";
    import BlogPost from "../components/BlogPost.astro";
    const pageTitle = "My Astro Learning Blog";
    const allPosts = await getCollection("blog");
    ---
    <BaseLayout pageTitle={pageTitle}>
    <p>This is where I will post about my journey learning Astro.</p>
    <ul>
    {
    allPosts.map((post) => (
    <BlogPost url={post.url} title={post.frontmatter.title} />)}
    <BlogPost url={`/posts/${post.id}/`} title={post.data.title} />
    ))
    }
    </ul>
    </BaseLayout>
  3. 该教程博客项目还使用 src/pages/tags/[tag].astro 为每个标签动态生成一个页面,并在 src/pages/tags/index.astro 显示标签列表。

    将上述相同的更改应用于这两个文件

    • 使用 getCollection("blog") 而不是 import.meta.glob() 来获取关于所有博客文章的数据
    • 使用 data 而不是 frontmatter 来访问所有 frontmatter 值
    • 通过将文章的 slug 添加到 /posts/ 路径来创建页面 URL

    生成单个标签页面的页面现在变为

    src/pages/tags/[tag].astro
    ---
    import { getCollection } from "astro:content";
    import BaseLayout from "../../layouts/BaseLayout.astro";
    import BlogPost from "../../components/BlogPost.astro";
    export async function getStaticPaths() {
    const allPosts = await getCollection("blog");
    const uniqueTags = [...new Set(allPosts.map((post) => post.data.tags).flat())];
    return uniqueTags.map((tag) => {
    const filteredPosts = allPosts.filter((post) =>
    post.data.tags.includes(tag)
    );
    return {
    params: { tag },
    props: { posts: filteredPosts },
    };
    });
    }
    const { tag } = Astro.params;
    const { posts } = Astro.props;
    ---
    <BaseLayout pageTitle={tag}>
    <p>Posts tagged with {tag}</p>
    <ul>
    { posts.map((post) => <BlogPost url={`/posts/${post.id}/`} title={post.data.title} />) }
    </ul>
    </BaseLayout>

    自己试试 - 更新标签索引页面中的查询

    标题为“自己试试 - 更新标签索引页面中的查询”的部分

    导入并使用 getCollection 来获取 src/pages/tags/index.astro 页面上博客文章中使用的标签,遵循与上面相同的步骤

    给我看代码。
    src/pages/tags/index.astro
    ---
    import { getCollection } from "astro:content";
    import BaseLayout from "../../layouts/BaseLayout.astro";
    const allPosts = await getCollection("blog");
    const tags = [...new Set(allPosts.map((post) => post.data.tags).flat())];
    const pageTitle = "Tag Index";
    ---
    <!-- ... -->

如有必要,请更新项目中任何与你的内容集合 schema 不匹配的 frontmatter 值,例如在你的布局中。

在博客教程示例中,pubDate 是一个字符串。现在,根据为文章 frontmatter 定义类型的 schema,pubDate 将是一个 Date 对象。你现在可以利用这一点,使用任何 Date 对象可用的方法来格式化日期。

要在博客文章布局中渲染日期,请使用 toLocaleDateString() 方法将其转换为字符串

src/layouts/MarkdownPostLayout.astro
<!-- ... -->
<BaseLayout pageTitle={frontmatter.title}>
<p>{frontmatter.pubDate.toLocaleDateString()}</p>
<p><em>{frontmatter.description}</em></p>
<p>Written by: {frontmatter.author}</p>
<img src={frontmatter.image.url} width="300" alt={frontmatter.image.alt} />
<!-- ... -->

该教程博客项目包含一个 RSS 订阅源。此函数也必须使用 getCollection() 从你的博客文章中返回信息。然后,你将使用返回的 data 对象生成 RSS 项目。

src/pages/rss.xml.js
import rss from '@astrojs/rss';
import { pagesGlobToRssItems } from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const posts = await getCollection("blog");
return rss({
title: 'Astro Learner | Blog',
description: 'My journey learning Astro',
site: context.site,
items: await pagesGlobToRssItems(import.meta.glob('./**/*.md')),
items: posts.map((post) => ({
title: post.data.title,
pubDate: post.data.pubDate,
description: post.data.description,
link: `/posts/${post.id}/`,
})),
customData: `<language>en-us</language>`,
})
}

有关使用内容集合的博客教程的完整示例,请参阅教程仓库的Content Collections 分支

贡献 社区 赞助