Contentful 与 Astro

Contentful 是一个无头(headless) CMS,允许你管理内容,集成其他服务并发布到多个平台。

在本节中,我们将使用 Contentful SDK 来连接你的 Contentful 空间与 Astro,实现零客户端 JavaScript。

首先,你需要以下内容:

  1. 一个 Astro 项目 - 如果你还没有 Astro 项目,我们的安装指南会帮助你迅速上手。

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

  3. Contentful 凭证 - 你可以在 Contentful 仪表板中的设置 > API 密钥中找到以下凭证。如果你还没有 API 密钥,请选择添加 API 密钥

    • Contentful space 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 环境变量启用智能感知,你可以在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;
}

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

  • 目录src/
    • 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 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文件中的凭证传递进去。

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

  • 目录src/
    • env.d.ts
    • 目录lib/
      • contentful.ts
  • .env
  • astro.config.mjs
  • package.json

从 Contentful 获取数据

标题部分 从 Contentful 获取数据

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 文档 中找到更多的查询选项。

使用 Astro 和 Contentful 制作博客

标题部分 使用 Astro 和 Contentful 制作博客

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

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

在你的 Contentful 空间中,在 内容模型 部分,创建一个新的内容模型,并设置以下字段和值:

  • Name: 博客文章
  • API identifier: blogPost
  • Description: 此内容类型用于博客文章

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

  1. 文本字段
    • Name: title
    • API identifier: title (将其他参数保持默认)
  2. 日期和时间字段
    • Name: date
    • API identifier: date
  3. 文本字段
    • Name: slug
    • API identifier: slug (将其他参数保持默认)
  4. 文本字段
    • Name: description
    • API identifier: description
  5. 富文本字段
    • Name: content
    • API identifier: content

单击保存以保存你的更改。

在你的 Contentful 空间的内容部分,点击添加条目按钮创建一个新条目。然后,填写字段:

  • Title: Astro 真是太棒了!
  • Slug: astro-is-amazing
  • Description: Astro 是一个全新的静态站点生成器,速度快,易于使用。
  • Date: 2022-10-05
  • Content: 这是我的第一篇博客文章!

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

随意添加你想要的博客文章,然后切换到你喜欢的代码编辑器,开始使用 Astro 进行开发!

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

src/lib/contentful.ts
import contentful, { 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

通过传递 BlogPost 接口来从 Contentful 获取所有带有内容类型为 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 属性将作为属性传递给页面组件。

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",
});
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(将文档转换为 HTML 的富文本文档)
  • date(使用 Date 构造函数进行格式化)

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

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",
});
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;
}
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>

在浏览器中导航到 http://localhost:3000/, 然后点击其中一篇文章,以确保你的动态路由正常工作!

如果你已经 选择使用 SSR 模式,你将使用一个使用 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 从文档转换为 HTML,并使用 Date 构造函数格式化日期。title 可以保持原样。然后,将这些属性添加到你的 post 对象中。

src/pages/posts/[slug].astro
---
import { contentfulClient } from "../../lib/contentful";
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>

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

在 Contentful 更改后重新构建

标题部分 在 Contentful 更改后重新构建

如果你的项目使用的是 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。

将 Webhook 添加到 Contentful
标题部分 将 Webhook 添加到 Contentful

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

现在,每当你在 Contentful 中发布新的博客文章时,都会触发新的构建,并更新你的博客。

更多 CMS 指南