如何使用Nextjs将Contentful CMS作为无头文件运行
Contentful是一个无头CMS。这意味着它是作为一个内容库而构建的,并通过一个API来提供数据。
在这篇文章中,我们将在CMS上创建简单的博客文章,然后使用Next.js将它们显示给用户。
我们将使用API和内容模型来查询CMS的数据。
内容模型指的是一种模式,它将允许生成类似的结构化数据。
例如,对于每篇博客文章,我们希望有一个标题、一个摘录、一个描述、一个封面图片和一个日期。所有这些字段构成了模式。
什么是无头CMS
一个面向内容的CMS由后台和前台组成。这样,它们都作为一个单一的系统运行。它结合了各种模块和功能,像一个整体一样运行。
因此,当你把CMS作为一个无头系统运行时,内容和表现形式都被解耦。作为一个无头系统,内容表现层是灵活的。
后台现在可以使用API(应用程序编程接口)来驱动原始数据。
将WordPress作为无头CMS运行的原因
这种技术允许你修改你呈现给用户的内容传递渠道。换句话说,它可以让你改变内容的呈现方式,而无需重新授权。
由于数据是作为原始数据交付的,你可以使用任何适合你需要的技术。API驱动的数据促进了全渠道架构。
它通过分离前端和后端组件,简化了开发人员的工作。这意味着你可以创建一个更快的、高效的、可扩展的应用程序。
目标
本文将与作为无头CMS的Contentful互动。我们将通过使用Next.js构建一个简单的帖子应用来展示这一概念。
前提条件
要跟上这篇文章,你应该具备以下条件。
- 在你的电脑上安装了[Node.js]。
- 在你的电脑上安装了[Git]。
- 对JavaScript和Next.js有一定的了解。
- 对Git有一定的了解。
在Contentful上创建一个账户
你需要在Contentful上有一个账户。如果你已经有一个账户,只需登录并继续进行下一步,否则你可以按照下面的步骤来创建一个。
- 访问注册页面。
- 你可以手动注册,或者你也可以使用你的谷歌或GitHub账户。
- 回答突出显示的问题。
- 然后点击
Start With Contentful。
至此,你就可以进行下一步了。
设置内容模型
内容模型指的是模式,它支持生成类似的结构化信息。
例如,对于每篇博客文章,我们希望有一个 标题,一个 摘录, a 描述, a 封面图片,以及a 日期.
要设置上述模式,请按照以下步骤进行。
- 在顶部栏中,点击 内容模式.
- 然后是 设计你的内容模型.
- 在弹出的模式中按下 创建内容类型在弹出的模式中按下按钮。
- 给 内容类型命名为 简单博文,API标识符字段是自动填充的。接下来,在
descriptions字段中提供一个简短的评论。
现在是时候添加字段来组成模式了。我们将逐一添加我们在上面讨论过的字段。我们可以使用add field 按钮来完成。
完成后,记得保存偏好。
在这一点上,我们已经通过创建一个内容类型并向该内容类型添加字段来定义我们的模式。现在,我们继续向我们的content type 添加内容。
向Contentful添加帖子
添加完内容类型后,现在是时候添加我们想在我们的应用程序中看到的数据了。
要做到这一点,我们将遵循以下步骤。
- 在顶部导航栏,点击
Content。 - 点击
Add simple blog post按钮。 - 在由此产生的表格中,输入
title,excerpt, 和description。你也可以选择是否添加虚拟文本。
现在我们来添加封面图片。
要做到这一点,点击Add new media ,给它一个标题,如simple cover image 或其他,点击Open file selector 。
然后你可以从你的电脑或其他来源上传一张图片。一旦你选择了一个文件,点击Publish 按钮。
记住要添加当前日期。最后,点击Publish 按钮。你会看到一个通知信息,说明你的博客文章已经成功添加。
在内容页面,你应该看到你发布的博文。你可以重复这个过程来添加几个帖子。
设置Next.js应用程序
我们将使用create-next-app来设置Next.js环境。
要生成这个Next.js模板应用程序,请打开终端(命令行),从目录中运行以下命令。
npx create-next-app contentful-nextjs-app
上述命令将在contentful-nextjs-app 文件夹内创建Next.js应用程序。
你可以使用cd 命令导航到这个项目文件夹,如下图所示。
cd contentful-nextjs-app
向Next.js应用程序添加Contentful凭证
为了让Next.js与Contentful API通信,我们需要添加用于验证的凭证。
在你的contentful-nextjs-app 项目文件夹中,创建一个.env.local 文件。
该文件将承载连接到Contentful的环境变量。在该文件中添加以下属性。
CONTENTFUL_SPACE_ID=your_contentful_space_id
CONTENTFUL_ACCESS_TOKEN=your_contentful_space_token
CONTENTFUL_PREVIEW_ACCESS_TOKEN=your_contentful_preview_access_token
CONTENTFUL_PREVIEW_SECRET=your_contentful_preview_secrets
为了访问上述变量,我们将使用以下步骤。
- 从你的Contentful仪表板页面,点击
Settings。 - 在
Space settings,点击API keys。
如果你还没有添加API key ,点击Add Api Key 按钮。
在出现的表格中。
- 复制
Space ID,并将其粘贴到CONTENTFUL_SPACE_ID字段。 - 复制
Access token并粘贴到CONTENTFUL_ACCESS_TOKEN栏。 - 复制
Preview access token并粘贴到CONTENTFUL_PREVIEW_ACCESS_TOKEN栏。 - 对于
CONTENTFUL_PREVIEW_SECRET,键入任何随机字符串。
我们的应用程序现在已经完全设置好了,我们准备进行下一步。
查询添加的帖子
在这一点上,我们现在可以获取我们先前添加的帖子了。为了查询这些帖子,我们将首先设置API。
首先,在你项目的根文件夹中创建一个lib 文件夹。在lib 文件夹中,创建一个api.js 文件。
在api.js ,我们将声明我们需要获取的字段,如下所示。
const POST_GRAPHQL_FIELDS = `
sys {
id
}
title
coverImage {
url
}
date
excerpt
description {
json
}`
在上面的代码中,我们正在使用先前创建的模式为每篇博客文章定义我们想要的字段。
还是在api.js 文件中,创建一个自定义的fetchGraphQL 方法,用于与Contentful API建立联系。
async function fetchGraphQL(query, preview = false) {
return fetch(
`https://graphql.contentful.com/content/v1/spaces/${process.env.CONTENTFUL_SPACE_ID}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${
preview
? process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN
: process.env.CONTENTFUL_ACCESS_TOKEN
}`,
},
body: JSON.stringify({ query }),
}
).then((response) => response.json())
}
上述fetchGraphQL 方法将构成查询文章的基础。我们将向Contentful API发送一个POST 请求,并设置URL 和headers 。
注意,在每个请求中,body 是动态的。
接下来,我们需要创建一个方法来从检索到的帖子中提取数据。
function extractPostEntries(fetchResponse) {
return fetchResponse?.data?.simpleBlogPostCollection?.items
}
对于来自Contentful API的每一个响应,数据将与各种元数据一起返回。因此,我们必须将其缩小到我们想要的数据。
现在,创建一个函数来查询来自Contentful API的帖子。
export async function getAllPostsForHome(preview) {
const entries = await fetchGraphQL(
`query {
simpleBlogPostCollection(order: date_DESC, preview: ${preview ? 'true' : 'false'}) {
items {
${POST_GRAPHQL_FIELDS}
}
}
}`,
preview
)
return extractPostEntries(entries)
}
在上面的代码中,我们使用上面定义的fetchGraphQL 方法来从CMS中获取帖子。
请注意,当我们要查询一个以上的博客文章时,我们将使用simpleBlogPostCollection 。如果我们查询的是一篇博客文章,我们将使用simpleBlogPost 。
显示文章
我们现在可以显示帖子了。在pages/index.js ,我们将使用getStaticProps来获取帖子。
这只是意味着数据将在构建时被预先渲染。这有助于Next.js提前加载这些数据。
此外,这使得网络应用快速渲染内容,就像它是一个静态页面一样。
要实现这一点,请从lib/api.js 的顶部指定的pages/index.js ,导入getAllPostsForHome 函数。
import {getAllPostsForHome} from "../lib/api"
同时,在Home 函数后添加以下代码。
export async function getStaticProps({preview = false}){
let allPosts = (await getAllPostsForHome(preview)) ?? [];
return {
props: { preview, allPosts }
}
}
由于我们现在要获取帖子,我们将对Home 函数进行如下修改,以便我们能够接收和显示帖子。
export default function Home({allPosts}) {
return (
<div className={styles.container}>
<Head>
<title>Blog app</title>
<meta name="description" content="Simple blog app with Contentful CMS" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<div className={styles.grid}>
{
allPosts.length > 0 ? (
allPosts.map((post) => (
<div className={styles.card} key={post.sys.id}>
<div className={styles.imageHolder}>
<img src={post.coverImage.url} alt={post.title} />
</div>
<div className={styles.details}>
<Link href={`posts/${post.sys.id}`}>
<a>
{post.title} →
</a>
</Link>
<p>{post.excerpt}</p>
</div>
</div>
))
) : (
<div className={styles.card}>
<p>No posts added!</p>
</div>
)
}
</div>
</main>
<footer className={styles.footer}>
<p>Simple blog app</p>
</footer>
</div>
)
}
在上面的代码中,我们检查我们是否有任何帖子。如果是真的,我们就循环查看结果并显示每一个帖子。否则,如果我们没有帖子,我们就显示一条信息。
对styles/Home.module.css 进行如下修改,以包括我们对Home 函数所作的修改。
.container {
min-height: 100vh;
padding: 0 0.5rem;
}
.main {
padding: 5rem 0;
min-height: 85vh;
}
.footer {
width: 100%;
padding: 1rem;
border-top: 1px solid #eaeaea;
display: flex;
justify-content: center;
align-items: center;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 1rem;
}
.title,
.description {
text-align: left;
padding: 12px;
}
.description {
line-height: 1.5;
font-size: 1.5rem;
}
.grid {
max-width: 800px;
margin: 0px auto;
/* display:flex;
justify-content: left; */
}
.card {
margin: 1rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
width: 90%;
display: flex;
justify-content: space-between;
}
.imageHolder {
width: 15%;
}
.imageHolder img {
width: 100%;
height: 100%;
margin-right: 10px;
border-radius: 10px 0px 0px 10px;
}
.details {
width: 85%;
padding: 1.5rem;
}
.details a {
margin: 0 0 1rem 0;
font-size: 16px;
font-weight: bold;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
.details p {
margin: 0;
font-size: 14px;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.5;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}
做完这些修改后,我们就可以查看帖子了。
测试simpleBlogPostCollection
我们现在准备测试一下,我们是否可以获取所有我们添加到Contentful API中的帖子。
要做到这一点,确保你的开发服务器已经启动,在contentful-nextjs-app 项目根目录下的终端运行以下命令。
npm run dev
打开你的浏览器并导航到http://localhost:3000 。你现在应该可以看到这些帖子了。
如果出现任何错误,请重温上面的步骤。你的页面应该类似于。

解决单个帖子的404错误
当你点击任何一个帖子时,它给你一个404 错误,因为我们还没有在这方面下功夫。
让我们用下面的步骤来了解它。
获取单个帖子的数据
在lib/api.js 文件中,我们将添加一个提取单个帖子的方法,如下所示。
function extractPost(fetchResponse){
return fetchResponse?.data?.simpleBlogPost?.items[0];
}
为了避免元数据,我们将使用上述函数得出我们想要的东西。
在lib/api.js 文件中,我们添加另一个函数来提取单个帖子,以及其他相关的帖子,如下所示。
export async function getPostAndMorePosts(preview,postId){
// Get a singlepost/entry
const entry = await fetchGraphQL(
`query{
title(id:"${postId}",preview:${preview ? true : false}){
${POST_GRAPHQL_FIELDS}
}
}`
);
// Get entries
const entries = await fetchGraphQL(
`query{
titleCollection(preview:${preview ? true : false}, limit:2){
items{
${POST_GRAPHQL_FIELDS}
}
}
}`
);
// Extract a post
const post = extractPost(entry);
// Get the related posts
const relatedPosts = extractPostEntries(entries).filter((_post) => _post.sys.id !== post.sys.id);
return {
post,
relatedPosts
};
}
在上面的函数中,我们获取了一个单一的帖子和相关的帖子。
在pages 文件夹中,创建另一个文件夹,将其称为posts 。
在posts 文件夹中,创建一个[postId].js 文件。[postId] 标志着post id 将是动态的。
我们需要将lib/api.js 中的getPostAndMorePosts 函数导入到[postId].js 文件中。
import {getPostAndMorePosts} from "../../lib/api";
使用getStaticProps 和getStaticPaths 方法来获取帖子,如下所示。
// Fetch for a single post
export async function getStaticProps({ params: { postId } }) {
let { post, relatedPosts } = await getPostAndMorePosts(false, postId);
return {
props: {
post,
relatedPosts
}
}
}
// Fetch the other posts done at build time
export async function getStaticPaths() {
const posts = await getAllPostsForHome(false);
let paths = posts.map((post) => ({
params: {
postId: post.sys.id
}
})
);
return {
paths,
fallback: true
}
}
getStaticProps() 函数将接收来自context params 的postId ,然后获取该特定的帖子。
getStaticPaths 将根据来自Contentful的帖子创建动态页面。因此,下次你在获取一个特定的帖子时,你将得到先前创建的动态页面。
显示单个帖子
创建一个Post 函数来显示检索到的帖子。
我们首先要导入必要的模块,如下图所示。
import Head from "next/head";
import Link from "next/link";
import {useRouter} from "next/router";
import styles from "../../styles/Home.module.css";
对于Post 函数,添加以下代码。
export function Post({posts, relatedPosts}){
const router = useRouter();
return (
<div className={styles.container}>
<Head>
<title>Blog app</title>
<meta name="description" content="Simple blog app with Contentful CMS" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
{
router.isFallBack ? (
<div styles={styles.title}>
Loading
</div>
): (
<>
<div className={styles.content}>
<div styles={styles.title}>
{post.title}
</div>
<p className={styles.meta}>
{new Date(post.date).toDateString()}
</p>
<div className={styles.coverImage}>
<img src={post.coverImage.url} alt={post.title} />
</div>
</div>
<div className={styles.grid}>
{
relatedPosts.length > 0 ? (
<>
<div className={styles.title}>
Related posts
</div>
{
relatedPosts.map((post) => (
<div className={styles.card} key={post.sys.id}>
<div className={styles.imageHolder}>
<img src={post.coverImage.url} alt={post.title} />
</div>
<div className={styles.details}>
<Link href={`posts/${post.sys.id}`}>
<a>
{post.title} →
</a>
</Link>
<p>{post.excerpt}</p>
</div>
</div>
))
}
</>
) : null
}
</div>
</>
)
}
</main>
<footer className={styles.footer}>
<p>Simple blog app</p>
</footer>
</div>
)
}
在上面的代码中,我们正在接收post 和来自getStaticProps 的相关帖子到我们的函数。
为了满足构建过程的需要,我们正在检查来自路由器的isFallBack ,并显示一个加载文本。然后我们显示title 、date 、coverimage 、related posts 。
我们需要在Home.module.css 文件中添加以下样式。
.content{
max-width: 800px;
margin: 0px auto;
padding: 12px;
}
.meta{
font-size:12px;
color: #aaa;
}
.coverImage {
width:100%;
height:300px;
}
.coverImage img{
width: 100%;
height: 100%;
}
测试simpleBlogPost
在这一点上,如果你点击任何一个帖子,你将会被重定向到一个单一的帖子页面。
要测试这一点,请确保你的服务器已经启动并运行。如果没有,从你的项目根文件夹的终端运行以下命令。
npm run dev
打开你的浏览器并导航到http://localhost:3000 。 然后,点击主页上的任何一个帖子。所选的帖子应该显示在你的屏幕上。

虽然看起来不错,但我们还缺少一些东西;描述部分。让我们在下一步处理这个功能。
由于我们将描述添加为富文本,我们将不得不使用@contentful/rich-text-react-renderer来在用户界面中显示它。
在你的终端中打开一个单独的标签,使用下面的命令安装该包。
npm install @contentful/rich-text-react-renderer
安装后,在[postId].js 文件中导入该包。
import { documentToReactComponents } from '@contentful/rich-text-react-renderer'
在cover image 部分之后,添加以下代码。
<div className={styles.contentBody}>
{documentToReactComponents(post.description.json)}
</div>
在上面的代码中,我们已经将富文本嵌入到一个div 。在Home.module.css ,让'给这个div 一些空白,如下所示。
.contentBody{
margin:10px auto;
}
现在,检查你的服务器是否还在运行。如果没有,请使用。
npm run dev
当你刷新你的页面时,你现在应该看到如下的描述。

我们现在只剩下一个步骤了。我们应该能够轻松地过渡到主页。
为此,我们将在主组件内添加一个主页链接,如下图所示。
<div className={styles.homeLink}>
<Link href="/">
<a>Home</a>
</Link>
</div>
将以下样式添加到Home.module.css 。
.homeLink {
max-width: 800px;
margin: 10px auto;
padding: 12px;
}
.homeLink a {
color: #0070f3;
}
在帖子页面上,你应该有一个可见的链接,你可以点击它来进入主页。
你的页面应该类似于下面的样子。

现在,在这一点上,帖子对用户来说是可见的。
预览模式
要查看对你的博客文章所作的未发表的修改,你需要使用预览模式。
要做到这一点,你应该在进行API调用时将预览选项设置为true* 。
托管到Vercel
要把项目托管到Vercel,请使用下面强调的步骤。
- 创建一个GitHub仓库。
- 推送代码到你的GitHub仓库。
- 登录到Vercel。
- 如果你没有账户,只需在任何供应商处注册。
- 在你的仪表板上,点击新项目。
- 在
import Git repository部分,选择你之前创建的仓库,然后点击import。 - 跳过
create team部分,进入Configure project部分。 - 在环境变量标签下,添加你项目中的所有变量
.env.local,每个变量都有它的名字和值。你可以复制-粘贴它们以避免错误。 - 最后,点击
Deploy按钮。 - 等待几秒钟,一切都会被设置好。
- 从你的仪表板,你将能够访问你的项目。
总结
在这篇文章中,我们已经讨论了围绕Next.js和Contentful的各种概念。