内容丰富且 Astro
Contentful 是一个无头 CMS,允许你管理内容、与其他服务集成以及发布到多个平台。
¥Contentful is a headless CMS that allows you to manage content, integrate with other services, and publish to multiple platforms.
与 Astro 集成
标题部分 与 Astro 集成¥Integrating with Astro
在本节中,我们将使用 内容丰富的 SDK 通过零客户端 JavaScript 将你的 Contentful 空间连接到 Astro。
¥In this section, we’ll use the Contentful SDK to connect your Contentful space to Astro with zero client-side JavaScript.
先决条件
标题部分 先决条件¥Prerequisites
首先,你需要具备以下条件:
¥To get started, you will need to have the following:
-
Astro 项目 - 如果你还没有 Astro 项目,我们的 安装指南 将立即帮助你启动并运行。
-
Contentful 账户和一个 Contentful 空间。如果你没有账户,你可以 报名 获取免费账户并创建一个新的 Contentful 空间。如果你有现有空间,也可以使用。
-
内容丰富的凭证 - 你可以在 Contentful 仪表板设置 > API 密钥中找到以下凭据。如果你没有任何 API 密钥,请选择添加 API 密钥创建一个。
-
内容空间 ID - 你的 Contentful 空间的 ID。
-
内容交付访问令牌 - 用于从 Contentful 空间使用已发布内容的访问令牌。
-
内容预览访问令牌 - 用于从 Contentful 空间使用未发布内容的访问令牌。
-
设置凭据
标题部分 设置凭据¥Setting up credentials
要将 Contentful 空间的凭据添加到 Astro,请使用以下变量在项目的根目录中创建 .env
文件:
¥To add your Contentful space’s credentials to Astro, create an .env
file in the root of your project with the following variables:
CONTENTFUL_SPACE_ID=YOUR_SPACE_IDCONTENTFUL_DELIVERY_TOKEN=YOUR_DELIVERY_TOKENCONTENTFUL_PREVIEW_TOKEN=YOUR_PREVIEW_TOKEN
现在,你可以在项目中使用这些环境变量。
¥Now, you can use these environment variables in your project.
如果你希望为你的 Contentful 环境变量提供 IntelliSense,你可以在 src/
目录中创建一个 env.d.ts
文件并配置 ImportMetaEnv
,如下所示:
¥If you would like to have IntelliSense for your Contentful environment variables, you can create a env.d.ts
file in the src/
directory and configure ImportMetaEnv
like this:
interface ImportMetaEnv { readonly CONTENTFUL_SPACE_ID: string; readonly CONTENTFUL_DELIVERY_TOKEN: string; readonly CONTENTFUL_PREVIEW_TOKEN: string;}
你的根目录现在应该包含这些新文件:
¥Your root directory should now include these new files:
Directorysrc/
- env.d.ts
- .env
- astro.config.mjs
- package.json
安装依赖
标题部分 安装依赖¥Installing dependencies
要连接你的 Contentful 空间,请使用以下单个命令为你的首选包管理器安装以下两个内容:
¥To connect with your Contentful space, install both of the following using the single command below for your preferred package manager:
-
contentful.js
,官方 JavaScript Contentful SDK -
rich-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
的新文件。
¥Next, create a new file called contentful.ts
in the src/lib/
directory of your project.
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
文件中的凭据。
¥The above code snippet creates a new Contentful client, passing in credentials from the .env
file.
最后,你的根目录现在应该包含这些新文件:
¥Finally, your root directory should now include these new files:
Directorysrc/
- env.d.ts
Directorylib/
- contentful.ts
- .env
- astro.config.mjs
- package.json
获取数据
标题部分 获取数据¥Fetching data
Astro 组件可以通过使用 contentfulClient
并指定 content_type
从你的 Contentful 账户获取数据。
¥Astro components can fetch data from your Contentful account by using the contentfulClient
and specifying the content_type
.
例如,如果你的 “blogPost” 内容类型具有标题文本字段和内容富文本字段,则你的组件可能如下所示:
¥For example, if you have a “blogPost” content type that has a text field for a title and a rich text field for content, your component might look like this:
---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>
你可以在 内容丰富的文档.txt 中找到更多查询选项。
¥You can find more querying options in the Contentful documentation.
使用 Astro 和 Contentful 制作博客
标题部分 使用 Astro 和 Contentful 制作博客¥Making a blog with Astro and Contentful
通过上述设置,你现在可以创建一个使用 Contentful 作为 CMS 的博客。
¥With the setup above, you are now able to create a blog that uses Contentful as the CMS.
先决条件
标题部分 先决条件¥Prerequisites
- 内容丰富的空间 - 对于本教程,我们建议从空白处开始。如果你已经有了内容模型,请随意使用它,但你需要修改我们的代码片段以匹配你的内容模型。
- 与 内容丰富的 SDK 集成的 Astro 项目 - 请参阅 与 Astro 集成 以了解如何使用 Contentful 设置 Astro 项目的更多详细信息。
建立内容模型
标题部分 建立内容模型¥Setting up a Contentful model
在你的 Contentful 空间内,在内容模型部分,创建一个具有以下字段和值的新内容模型:
¥Inside your Contentful space, in the Content model section, create a new content model with the following fields and values:
-
名称:博客文章
-
API 标识符:
blogPost
-
描述:此内容类型适用于博客文章
在你新创建的内容类型中,使用添加字段按钮添加 5 个具有以下参数的新字段:
¥In your newly created content type, use the Add Field button to add 5 new fields with the following parameters:
-
文本域
-
名称:title
-
API 标识符:
title
(其他参数保留默认值)
-
-
日期和时间字段
-
名称:date
-
API 标识符:
date
-
-
文本域
-
名称:slug
-
API 标识符:
slug
(其他参数保留默认值)
-
-
文本域
-
名称:description
-
API 标识符:
description
-
-
富文本字段
-
名称:content
-
API 标识符:
content
-
单击“保存”以保存你的更改。
¥Click Save to save your changes.
在 Contentful 空间的内容部分,单击添加条目按钮创建一个新条目。然后,填写字段:
¥In the Content section of your Contentful space, create a new entry by clicking the Add Entry button. Then, fill in the fields:
-
标题:
Astro is amazing!
-
蛞蝓:
astro-is-amazing
-
描述:
Astro is a new static site generator that is blazing fast and easy to use.
-
日期:
2022-10-05
-
内容:
This is my first blog post!
单击“发布”以保存你的条目。你刚刚创建了第一篇博客文章。
¥Click Publish to save your entry. You have just created your first blog post.
你可以随意添加任意数量的博客文章,然后切换到你最喜欢的代码编辑器,开始使用 Astro 进行黑客攻击!
¥Feel free to add as many blog posts as you want, then switch to your favorite code editor to start hacking with Astro!
显示博客文章列表
标题部分 显示博客文章列表¥Displaying a list of blog posts
创建一个名为 BlogPost
的新接口并将其添加到 src/lib/
中的 contentful.ts
文件中。此界面将匹配 Contentful 中你的博客文章内容类型的字段。你将使用它来输入你的博客文章条目响应。
¥Create a new interface called BlogPost
and add it to your contentful.ts
file in src/lib/
. This interface will match the fields of your blog post content type in Contentful. You will use it to type your blog post entries response.
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",});
接下来,转到 Astro 页面,你将从 Contentful 获取数据。在此示例中,我们将在 src/pages/
中使用主页 index.astro
。
¥Next, go to the Astro page where you will fetch data from Contentful. We will use the home page index.astro
in src/pages/
in this example.
从 src/lib/contentful.ts
导入 BlogPost
接口和 contentfulClient
。
¥Import BlogPost
interface and contentfulClient
from src/lib/contentful.ts
.
从 Contentful 中获取内容类型为 blogPost
的所有条目,同时传递 BlogPost
接口以键入你的响应。
¥Fetch all the entries from Contentful with a content type of blogPost
while passing the BlogPost
interface to type your response.
---import { contentfulClient } from "../lib/contentful";import type { BlogPost } from "../lib/contentful";
const entries = await contentfulClient.getEntries<BlogPost>({ content_type: "blogPost",});---
此 fetch 调用将返回你在 entries.items
的博客文章的数组。你可以使用 map()
创建一个新数组 (posts
),用于格式化返回的数据。
¥This fetch call will return an array of your blog posts at entries.items
. You can use map()
to create a new array (posts
) that formats your returned data.
下面的示例从我们的内容模型返回 items.fields
属性以创建博客文章预览,同时将日期重新格式化为更易读的格式。
¥The example below returns the items.fields
properties from our Content model to create a blog post preview, and at the same time, reformats the date to a more readable format.
---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
来显示每篇博客文章的预览。
¥Finally, you can use posts
in your template to show a preview of each blog post.
---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>
生成个人博客文章
标题部分 生成个人博客文章¥Generating individual blog posts
使用与上面相同的方法从 Contentful 获取数据,但这一次,在将为每个博客文章创建唯一页面路由的页面上。
¥Use the same method to fetch your data from Contentful as above, but this time, on a page that will create a unique page route for each blog post.
静态站点生成
标题部分 静态站点生成¥Static site generation
如果你使用 Astro 的默认静态模式,你将使用 动态路由 和 getStaticPaths()
函数。该函数将在构建时被调用以生成成为页面的路径列表。
¥If you’re using Astro’s default static mode, you’ll use dynamic routes and the getStaticPaths()
function. This function will be called at build time to generate the list of paths that become pages.
在 src/pages/posts/
中创建一个名为 [slug].astro
的新文件。
¥Create a new file named [slug].astro
in src/pages/posts/
.
正如你在 index.astro
上所做的那样,从 src/lib/contentful.ts
导入 BlogPost
接口和 contentfulClient
。
¥As you did on index.astro
, import the BlogPost
interface and contentfulClient
from src/lib/contentful.ts
.
这次,在 getStaticPaths()
函数中获取数据。
¥This time, fetch your data inside a getStaticPaths()
function.
---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 传递给页面组件。
¥Then, map each item to an object with a params
and props
property. The params
property will be used to generate the URL of the page and the props
property will be passed to the page component as 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
。
¥The property inside params
must match the name of the dynamic route. Since our filename is [slug].astro
, we use slug
.
在我们的示例中,props
对象将三个属性传递给页面:
¥In our example, the props
object passes three properties to the page:
-
标题(字符串)
-
内容(使用
documentToHtmlString
转换为 HTML 的富文本文档) -
日期(使用
Date
构造函数格式化)
最后,你可以使用页面 props
来显示你的博客文章。
¥Finally, you can use the page props
to display your blog post.
---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>
导航到 http://localhost:4321/ 并单击你的其中一篇文章,以确保你的动态路由正常运行!
¥Navigate to http://localhost:4321/ and click on one of your posts to make sure your dynamic route is working!
服务端渲染
标题部分 服务端渲染¥Server side rendering
如果你有 选择 SSR 模式,你将使用动态路由,该路由使用 slug
参数从 Contentful 获取数据。
¥If you’ve opted in to SSR mode, you will use a dynamic route that uses a slug
parameter to fetch the data from Contentful.
在 src/pages/posts
中创建 [slug].astro
页面。使用 Astro.params
从 URL 获取 slug,然后将其传递给 getEntries
:
¥Create a [slug].astro
page in src/pages/posts
. Use Astro.params
to get the slug from the URL, then pass that to 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 页面。
¥If the entry is not found, you can redirect the user to the 404 page using Astro.redirect
.
---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
对象。
¥To pass post data to the template section, create a post
object outside the try/catch
block.
使用 documentToHtmlString
将 content
从 Document 转换为 HTML,并使用 Date 构造函数设置日期格式。title
可以保持原样。然后,将这些属性添加到你的 post
对象中。
¥Use documentToHtmlString
to convert content
from a Document to HTML, and use the Date constructor to format the date. title
can be left as-is. Then, add these properties to your post
object.
---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
在模板部分显示你的博客文章。
¥Finally, you can reference post
to display your blog post in the template section.
---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>
发布你的网站
标题部分 发布你的网站¥Publishing your site
要部署你的网站,请访问我们的 部署指南 并按照你首选托管提供商的说明进行操作。
¥To deploy your website, visit our deployment guides and follow the instructions for your preferred hosting provider.
重建内容丰富的变化
标题部分 重建内容丰富的变化¥Rebuild on Contentful changes
如果你的项目使用 Astro 的默认静态模式,则需要设置一个 Webhook 以在内容更改时触发新的构建。如果你使用 Netlify 或 Vercel 作为托管提供商,则可以使用其 Webhook 功能从 Contentful 事件触发新构建。
¥If your project is using Astro’s default static mode, you will need to set up a webhook to trigger a new build when your content changes. If you are using Netlify or Vercel as your hosting provider, you can use its webhook feature to trigger a new build from Contentful events.
Netlify
标题部分 Netlify要在 Netlify 中设置 Webhook:
¥To set up a webhook in Netlify:
-
Go to your site dashboard and click on Build & deploy.
-
Under the Continuous Deployment tab, find the Build hooks section and click on Add build hook.
-
Provide a name for your webhook and select the branch you want to trigger the build on. Click on Save and copy the generated URL.
Vercel
标题部分 Vercel要在 Vercel 中设置 Webhook:
¥To set up a webhook in Vercel:
-
Go to your project dashboard and click on Settings.
-
Under the Git tab, find the Deploy Hooks section.
-
Provide a name for your webhook and the branch you want to trigger the build on. Click Add and copy the generated URL.
将 webhook 添加到 Contentful
标题部分 将 webhook 添加到 Contentful¥Adding a webhook to Contentful
在你的 Contentful 空间设置中,单击“Webhooks”选项卡,然后单击“添加 Webhook”按钮创建一个新的 webhook。为你的 Webhook 提供名称并粘贴你在上一部分中复制的 Webhook URL。最后,点击保存以创建 webhook。
¥In your Contentful space settings, click on the Webhooks tab and create a new webhook by clicking the Add Webhook button. Provide a name for your webhook and paste the webhook URL you copied in the previous section. Finally, hit Save to create the webhook.
现在,每当你在 Contentful 中发布新博客文章时,都会触发新的构建并更新你的博客。
¥Now, whenever you publish a new blog post in Contentful, a new build will be triggered and your blog will be updated.