跳转到内容

Contentful & Astro

Contentful 是一个无头 CMS,它允许你管理内容、与其他服务集成以及发布到多个平台。

在本节中,我们将使用 Contentful SDK 将你的 Contentful 空间连接到 Astro,无需任何客户端 JavaScript。

要开始,你需要具备以下条件

  1. 一个 Astro 项目 - 如果你还没有 Astro 项目,我们的安装指南将帮助你快速启动并运行。

  2. 一个 Contentful 帐户和一个 Contentful 空间。如果你没有帐户,可以注册一个免费帐户并创建一个新的 Contentful 空间。如果你已有空间,也可以使用现有的。

  3. Contentful 凭证 - 你可以在 Contentful 仪表板的 Settings > API keys 中找到以下凭证。如果你没有任何 API 密钥,请选择 Add API key 来创建一个。

    • Contentful 空间 ID - 你的 Contentful 空间的 ID。
    • Contentful delivery access token - 用于从你的 Contentful 空间消费已发布内容的访问令牌。
    • Contentful preview access token - 用于从你的 Contentful 空间消费未发布内容的访问令牌。

要将 Contentful 空间的凭证添加到 Astro,请在项目根目录下创建一个 .env 文件,并包含以下变量:

.env
CONTENTFUL_SPACE_ID=YOUR_SPACE_ID
CONTENTFUL_DELIVERY_TOKEN=YOUR_DELIVERY_TOKEN
CONTENTFUL_PREVIEW_TOKEN=YOUR_PREVIEW_TOKEN

现在,你就可以在你的项目中使用这些环境变量了。

如果你想为你的 Contentful 环境变量提供 IntelliSense,可以在 src/ 目录下创建一个 env.d.ts 文件,并像这样配置 ImportMetaEnv

src/env.d.ts
interface ImportMetaEnv {
readonly CONTENTFUL_SPACE_ID: string;
readonly CONTENTFUL_DELIVERY_TOKEN: string;
readonly CONTENTFUL_PREVIEW_TOKEN: string;
}

你的根目录现在应该包含这些新文件:

  • Directorysrc/
    • env.d.ts
  • .env
  • astro.config.mjs
  • package.json

要连接到你的 Contentful 空间,请使用下面适用于你偏好的包管理器的单条命令,安装以下两个包:

终端窗口
npm install contentful @contentful/rich-text-html-renderer

接下来,在你的项目的 src/lib/ 目录下创建一个名为 contentful.ts 的新文件。

src/lib/contentful.ts
import * as contentful from "contentful";
export const contentfulClient = contentful.createClient({
space: import.meta.env.CONTENTFUL_SPACE_ID,
accessToken: import.meta.env.DEV
? import.meta.env.CONTENTFUL_PREVIEW_TOKEN
: import.meta.env.CONTENTFUL_DELIVERY_TOKEN,
host: import.meta.env.DEV ? "preview.contentful.com" : "cdn.contentful.com",
});

上面的代码片段创建了一个新的 Contentful 客户端,并传入了 .env 文件中的凭证。

最后,你的根目录现在应该包含这些新文件:

  • Directorysrc/
    • env.d.ts
    • Directorylib/
      • contentful.ts
  • .env
  • astro.config.mjs
  • package.json

Astro 组件可以通过使用 contentfulClient 并指定 content_type 来从你的 Contentful 帐户获取数据。

例如,如果你有一个“blogPost”内容类型,其中包含一个用于标题的文本字段和一个用于内容的富文本字段,你的组件可能看起来像这样:

---
import { contentfulClient } from "../lib/contentful";
import { documentToHtmlString } from "@contentful/rich-text-html-renderer";
import type { EntryFieldTypes } from "contentful";
interface BlogPost {
contentTypeId: "blogPost",
fields: {
title: EntryFieldTypes.Text
content: EntryFieldTypes.RichText,
}
}
const entries = await contentfulClient.getEntries<BlogPost>({
content_type: "blogPost",
});
---
<body>
{entries.items.map((item) => (
<section>
<h2>{item.fields.title}</h2>
<article set:html={documentToHtmlString(item.fields.content)}></article>
</section>
))}
</body>

你可以在 Contentful 文档中找到更多查询选项。

通过上面的设置,你现在可以创建一个使用 Contentful 作为 CMS 的博客了。

  1. 一个 Contentful 空间 - 对于本教程,我们建议从一个空空间开始。如果你已经有了一个内容模型,可以随意使用它,但你需要修改我们的代码片段以匹配你的内容模型。
  2. 一个集成了 Contentful SDK 的 Astro 项目 - 有关如何使用 Contentful 设置 Astro 项目的更多详细信息,请参阅与 Astro 集成

在你的 Contentful 空间内,在 Content model 部分,创建一个具有以下字段和值的新内容模型:

  • 名称: Blog Post
  • API 标识符: blogPost
  • 描述: 此内容类型用于博客文章

在你新创建的内容类型中,使用 Add Field 按钮添加 5 个新字段,参数如下:

  1. 文本字段
    • 名称: title
    • API 标识符: title(其他参数保留默认值)
  2. 日期和时间字段
    • 名称: date
    • API 标识符: date
  3. 文本字段
    • 名称: slug
    • API 标识符: slug(其他参数保留默认值)
  4. 文本字段
    • 名称: description
    • API 标识符: description
  5. 富文本字段
    • 名称: content
    • API 标识符: content

点击 Save 保存你的更改。

在你的 Contentful 空间的 Content 部分,通过点击 Add Entry 按钮创建一个新条目。然后,填写字段:

  • Title: Astro is amazing!
  • Slug: astro-is-amazing
  • Description: Astro is a new static site generator that is blazing fast and easy to use.
  • Date: 2022-10-05
  • Content: This is my first blog post!

点击 Publish 保存你的条目。你刚刚创建了你的第一篇博客文章。

你可以随意添加任意数量的博客文章,然后切换到你喜欢的代码编辑器,开始用 Astro 编程吧!

创建一个名为 BlogPost 的新接口,并将其添加到 src/lib/ 目录下的 contentful.ts 文件中。此接口将与你在 Contentful 中的博客文章内容类型的字段相匹配。你将用它来为你的博客文章条目响应提供类型支持。

src/lib/contentful.ts
import * as contentful from "contentful";
import type { EntryFieldTypes } from "contentful";
export interface BlogPost {
contentTypeId: "blogPost",
fields: {
title: EntryFieldTypes.Text
content: EntryFieldTypes.RichText,
date: EntryFieldTypes.Date,
description: EntryFieldTypes.Text,
slug: EntryFieldTypes.Text
}
}
export const contentfulClient = contentful.createClient({
space: import.meta.env.CONTENTFUL_SPACE_ID,
accessToken: import.meta.env.DEV
? import.meta.env.CONTENTFUL_PREVIEW_TOKEN
: import.meta.env.CONTENTFUL_DELIVERY_TOKEN,
host: import.meta.env.DEV ? "preview.contentful.com" : "cdn.contentful.com",
});

接下来,转到你将从 Contentful 获取数据的 Astro 页面。在本例中,我们将使用 src/pages/ 下的主页 index.astro

src/lib/contentful.ts 导入 BlogPost 接口和 contentfulClient

从 Contentful 获取所有内容类型为 blogPost 的条目,同时传递 BlogPost 接口来为你的响应提供类型。

src/pages/index.astro
---
import { contentfulClient } from "../lib/contentful";
import type { BlogPost } from "../lib/contentful";
const entries = await contentfulClient.getEntries<BlogPost>({
content_type: "blogPost",
});
---

这个获取调用将在 entries.items 处返回一个你的博客文章数组。你可以使用 map() 来创建一个新的数组(posts),用于格式化你返回的数据。

下面的例子返回了我们内容模型中的 items.fields 属性来创建一个博客文章预览,同时将日期重新格式化为更易读的格式。

src/pages/index.astro
---
import { contentfulClient } from "../lib/contentful";
import type { BlogPost } from "../lib/contentful";
const entries = await contentfulClient.getEntries<BlogPost>({
content_type: "blogPost",
});
const posts = entries.items.map((item) => {
const { title, date, description, slug } = item.fields;
return {
title,
slug,
description,
date: new Date(date).toLocaleDateString()
};
});
---

最后,你可以在你的模板中使用 posts 来显示每篇博客文章的预览。

src/pages/index.astro
---
import { contentfulClient } from "../lib/contentful";
import type { BlogPost } from "../lib/contentful";
const entries = await contentfulClient.getEntries<BlogPost>({
content_type: "blogPost",
});
const posts = entries.items.map((item) => {
const { title, date, description, slug } = item.fields;
return {
title,
slug,
description,
date: new Date(date).toLocaleDateString()
};
});
---
<html lang="en">
<head>
<title>My Blog</title>
</head>
<body>
<h1>My Blog</h1>
<ul>
{posts.map((post) => (
<li>
<a href={`/posts/${post.slug}/`}>
<h2>{post.title}</h2>
</a>
<time>{post.date}</time>
<p>{post.description}</p>
</li>
))}
</ul>
</body>
</html>

使用与上面相同的方法从 Contentful 获取数据,但这次是在一个将为每篇博客文章创建唯一页面路由的页面上。

如果你正在使用 Astro 的默认静态模式,你将使用动态路由getStaticPaths() 函数。这个函数将在构建时被调用,以生成将成为页面的路径列表。

src/pages/posts/ 目录下创建一个名为 [slug].astro 的新文件。

就像你在 index.astro 中所做的那样,从 src/lib/contentful.ts 导入 BlogPost 接口和 contentfulClient

这一次,在 getStaticPaths() 函数内部获取你的数据。

src/pages/posts/[slug].astro
---
import { contentfulClient } from "../../lib/contentful";
import type { BlogPost } from "../../lib/contentful";
export async function getStaticPaths() {
const entries = await contentfulClient.getEntries<BlogPost>({
content_type: "blogPost",
});
}
---

然后,将每个项目映射到一个包含 paramsprops 属性的对象。 params 属性将用于生成页面的 URL,而 props 属性将作为 props 传递给页面组件。

src/pages/posts/[slug].astro
---
import { contentfulClient } from "../../lib/contentful";
import { documentToHtmlString } from "@contentful/rich-text-html-renderer";
import type { BlogPost } from "../../lib/contentful";
export async function getStaticPaths() {
const entries = await contentfulClient.getEntries<BlogPost>({
content_type: "blogPost",
});
const pages = entries.items.map((item) => ({
params: { slug: item.fields.slug },
props: {
title: item.fields.title,
content: documentToHtmlString(item.fields.content),
date: new Date(item.fields.date).toLocaleDateString(),
},
}));
return pages;
}
---

params 内部的属性必须与动态路由的名称匹配。由于我们的文件名是 [slug].astro,我们使用 slug

在我们的示例中,props 对象向页面传递了三个属性:

  • title(一个字符串)
  • content(一个使用 documentToHtmlString 转换为 HTML 的富文本 Document)
  • date(使用 Date 构造函数格式化)

最后,你可以使用页面的 props 来显示你的博客文章。

src/pages/posts/[slug].astro
---
import { contentfulClient } from "../../lib/contentful";
import { documentToHtmlString } from "@contentful/rich-text-html-renderer";
import type { BlogPost } from "../../lib/contentful";
export async function getStaticPaths() {
const { items } = await contentfulClient.getEntries<BlogPost>({
content_type: "blogPost",
});
const pages = items.map((item) => ({
params: { slug: item.fields.slug },
props: {
title: item.fields.title,
content: documentToHtmlString(item.fields.content),
date: new Date(item.fields.date).toLocaleDateString(),
},
}));
return pages;
}
const { content, title, date } = Astro.props;
---
<html lang="en">
<head>
<title>{title}</title>
</head>
<body>
<h1>{title}</h1>
<time>{date}</time>
<article set:html={content} />
</body>
</html>

导航到 https://:4321/ 并点击你的一篇文章,以确保你的动态路由正常工作!

如果你已经选择使用适配器进行按需渲染,你将使用一个动态路由,该路由使用 slug 参数从 Contentful 获取数据。

src/pages/posts 中创建一个 [slug].astro 页面。使用 Astro.params 从 URL 获取 slug,然后将其传递给 getEntries

src/pages/posts/[slug].astro
---
import { contentfulClient } from "../../lib/contentful";
import type { BlogPost } from "../../lib/contentful";
const { slug } = Astro.params;
const data = await contentfulClient.getEntries<BlogPost>({
content_type: "blogPost",
"fields.slug": slug,
});
---

如果未找到条目,你可以使用 Astro.redirect 将用户重定向到 404 页面。

src/pages/posts/[slug].astro
---
import { contentfulClient } from "../../lib/contentful";
import type { BlogPost } from "../../lib/contentful";
const { slug } = Astro.params;
try {
const data = await contentfulClient.getEntries<BlogPost>({
content_type: "blogPost",
"fields.slug": slug,
});
} catch (error) {
return Astro.redirect("/404");
}
---

要将文章数据传递给模板部分,请在 try/catch 块之外创建一个 post 对象。

使用 documentToHtmlStringcontent 从 Document 转换为 HTML,并使用 Date 构造函数格式化日期。 title 可以保持原样。然后,将这些属性添加到你的 post 对象中。

src/pages/posts/[slug].astro
---
import Layout from "../../layouts/Layout.astro";
import { contentfulClient } from "../../lib/contentful";
import { documentToHtmlString } from "@contentful/rich-text-html-renderer";
import type { BlogPost } from "../../lib/contentful";
let post;
const { slug } = Astro.params;
try {
const data = await contentfulClient.getEntries<BlogPost>({
content_type: "blogPost",
"fields.slug": slug,
});
const { title, date, content } = data.items[0].fields;
post = {
title,
date: new Date(date).toLocaleDateString(),
content: documentToHtmlString(content),
};
} catch (error) {
return Astro.redirect("/404");
}
---

最后,你可以引用 post 在模板部分显示你的博客文章。

src/pages/posts/[slug].astro
---
import Layout from "../../layouts/Layout.astro";
import { contentfulClient } from "../../lib/contentful";
import { documentToHtmlString } from "@contentful/rich-text-html-renderer";
import type { BlogPost } from "../../lib/contentful";
let post;
const { slug } = Astro.params;
try {
const data = await contentfulClient.getEntries<BlogPost>({
content_type: "blogPost",
"fields.slug": slug,
});
const { title, date, content } = data.items[0].fields;
post = {
title,
date: new Date(date).toLocaleDateString(),
content: documentToHtmlString(content),
};
} catch (error) {
return Astro.redirect("/404");
}
---
<html lang="en">
<head>
<title>{post?.title}</title>
</head>
<body>
<h1>{post?.title}</h1>
<time>{post?.date}</time>
<article set:html={post?.content} />
</body>
</html>

要部署你的网站,请访问我们的部署指南并按照你偏好的托管提供商的说明进行操作。

如果你的项目正在使用 Astro 的默认静态模式,当你的内容发生变化时,你需要设置一个 webhook 来触发新的构建。如果你使用 Netlify 或 Vercel 作为你的托管提供商,你可以使用它们的 webhook 功能从 Contentful 事件中触发新的构建。

在 Netlify 中设置 webhook

  1. 转到你的站点仪表盘,然后点击 Build & deploy

  2. Continuous Deployment 选项卡下,找到 Build hooks 部分,然后点击 Add build hook

  3. 为你的 webhook 提供一个名称,并选择你想要触发构建的分支。点击 Save 并复制生成的 URL。

在 Vercel 中设置 webhook

  1. 转到你的项目仪表盘,然后点击 Settings

  2. Git 选项卡下,找到 Deploy Hooks 部分。

  3. 为你的 webhook 提供一个名称,并选择你想要触发构建的分支。点击 Add 并复制生成的 URL。

在你的 Contentful 空间设置中,点击 Webhooks 选项卡,然后点击 Add Webhook 按钮创建一个新的 webhook。为你的 webhook 提供一个名称,并粘贴你在前一节中复制的 webhook URL。最后,点击 Save 创建 webhook。

现在,每当你在 Contentful 中发布一篇新的博客文章,就会触发一次新的构建,你的博客就会被更新。

更多 CMS 指南

贡献 社区 赞助