跳转到内容

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:4321/, 然后点击其中一篇文章,以确保你的动态路由正常工作!

如果你已经 选择使用 SSR 模式 (EN),你将使用一个使用 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 中发布新的博客文章时,都会触发新的构建,并更新你的博客。

More CMS guides

Contribute

What’s on your mind?

Create GitHub Issue

Quickest way to alert our team of a problem.

Community