Contentful & Astro
Contentful 是一个无头 CMS,它允许你管理内容、与其他服务集成以及发布到多个平台。
与 Astro 集成
标题为“与 Astro 集成”的部分在本节中,我们将使用 Contentful SDK 将你的 Contentful 空间连接到 Astro,无需任何客户端 JavaScript。
先决条件
标题为“先决条件”的部分要开始,你需要具备以下条件
-
一个 Astro 项目 - 如果你还没有 Astro 项目,我们的安装指南将帮助你快速启动并运行。
-
一个 Contentful 帐户和一个 Contentful 空间。如果你没有帐户,可以注册一个免费帐户并创建一个新的 Contentful 空间。如果你已有空间,也可以使用现有的。
-
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
文件,并包含以下变量:
CONTENTFUL_SPACE_ID=YOUR_SPACE_IDCONTENTFUL_DELIVERY_TOKEN=YOUR_DELIVERY_TOKENCONTENTFUL_PREVIEW_TOKEN=YOUR_PREVIEW_TOKEN
现在,你就可以在你的项目中使用这些环境变量了。
如果你想为你的 Contentful 环境变量提供 IntelliSense,可以在 src/
目录下创建一个 env.d.ts
文件,并像这样配置 ImportMetaEnv
:
interface ImportMetaEnv { readonly CONTENTFUL_SPACE_ID: string; readonly CONTENTFUL_DELIVERY_TOKEN: string; readonly CONTENTFUL_PREVIEW_TOKEN: string;}
阅读更多关于在 Astro 中使用环境变量和 .env
文件的信息。
你的根目录现在应该包含这些新文件:
Directorysrc/
- env.d.ts
- .env
- astro.config.mjs
- package.json
安装依赖
标题为“安装依赖”的部分要连接到你的 Contentful 空间,请使用下面适用于你偏好的包管理器的单条命令,安装以下两个包:
contentful.js
,官方的 Contentful JavaScript SDKrich-text-html-renderer
,一个将 Contentful 的富文本字段渲染为 HTML 的包。
npm install contentful @contentful/rich-text-html-renderer
pnpm add contentful @contentful/rich-text-html-renderer
yarn add contentful @contentful/rich-text-html-renderer
接下来,在你的项目的 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
文件中的凭证。
在开发模式下,你的内容将从 Contentful 预览 API 获取。这意味着你将能够看到来自 Contentful 网页应用的未发布内容。
在构建时,你的内容将从 Contentful delivery API 获取。这意味着只有已发布的内容才会在构建时可用。
最后,你的根目录现在应该包含这些新文件:
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 模型来学习如何为你的内容创建一个基本的博客模型。
你可以在 Contentful 文档中找到更多查询选项。
使用 Astro 和 Contentful 制作博客
标题为“使用 Astro 和 Contentful 制作博客”的部分通过上面的设置,你现在可以创建一个使用 Contentful 作为 CMS 的博客了。
先决条件
标题为“先决条件”的部分- 一个 Contentful 空间 - 对于本教程,我们建议从一个空空间开始。如果你已经有了一个内容模型,可以随意使用它,但你需要修改我们的代码片段以匹配你的内容模型。
- 一个集成了 Contentful SDK 的 Astro 项目 - 有关如何使用 Contentful 设置 Astro 项目的更多详细信息,请参阅与 Astro 集成。
设置 Contentful 模型
标题为“设置 Contentful 模型”的部分在你的 Contentful 空间内,在 Content model 部分,创建一个具有以下字段和值的新内容模型:
- 名称: Blog Post
- API 标识符:
blogPost
- 描述: 此内容类型用于博客文章
在你新创建的内容类型中,使用 Add Field 按钮添加 5 个新字段,参数如下:
- 文本字段
- 名称: title
- API 标识符:
title
(其他参数保留默认值)
- 日期和时间字段
- 名称: date
- API 标识符:
date
- 文本字段
- 名称: slug
- API 标识符:
slug
(其他参数保留默认值)
- 文本字段
- 名称: description
- API 标识符:
description
- 富文本字段
- 名称: 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 中的博客文章内容类型的字段相匹配。你将用它来为你的博客文章条目响应提供类型支持。
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
接口来为你的响应提供类型。
---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
属性来创建一个博客文章预览,同时将日期重新格式化为更易读的格式。
---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
来显示每篇博客文章的预览。
---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()
函数内部获取你的数据。
---import { contentfulClient } from "../../lib/contentful";import type { BlogPost } from "../../lib/contentful";
export async function getStaticPaths() { const entries = await contentfulClient.getEntries<BlogPost>({ content_type: "blogPost", });}---
然后,将每个项目映射到一个包含 params
和 props
属性的对象。 params
属性将用于生成页面的 URL,而 props
属性将作为 props 传递给页面组件。
---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
来显示你的博客文章。
---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
:
---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 页面。
---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
对象。
使用 documentToHtmlString
将 content
从 Document 转换为 HTML,并使用 Date 构造函数格式化日期。 title
可以保持原样。然后,将这些属性添加到你的 post
对象中。
---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
在模板部分显示你的博客文章。
---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
标题为“Netlify”的部分在 Netlify 中设置 webhook
-
转到你的站点仪表盘,然后点击 Build & deploy。
-
在 Continuous Deployment 选项卡下,找到 Build hooks 部分,然后点击 Add build hook。
-
为你的 webhook 提供一个名称,并选择你想要触发构建的分支。点击 Save 并复制生成的 URL。
Vercel
标题为“Vercel”的部分在 Vercel 中设置 webhook
-
转到你的项目仪表盘,然后点击 Settings。
-
在 Git 选项卡下,找到 Deploy Hooks 部分。
-
为你的 webhook 提供一个名称,并选择你想要触发构建的分支。点击 Add 并复制生成的 URL。
向 Contentful 添加 webhook
标题为“向 Contentful 添加 webhook”的部分在你的 Contentful 空间设置中,点击 Webhooks 选项卡,然后点击 Add Webhook 按钮创建一个新的 webhook。为你的 webhook 提供一个名称,并粘贴你在前一节中复制的 webhook URL。最后,点击 Save 创建 webhook。
现在,每当你在 Contentful 中发布一篇新的博客文章,就会触发一次新的构建,你的博客就会被更新。