首页
/ Next.js 静态博客实战:用 Contentful 构建支持 Draft Mode 与 On-Demand Revalidation 的博客

Next.js 静态博客实战:用 Contentful 构建支持 Draft Mode 与 On-Demand Revalidation 的博客

2026-09-06 15:13:53作者:乔或婵

本文以 Next.js 仓库中的 examples/cms-contentful 示例为主体,完整讲解如何用 Contentful 作为数据源搭建一个静态生成的博客:从内容模型(Content Model)定义、批量初始化脚本、环境变量配置,到 Draft Mode 草稿预览与基于 Webhook 的按需重新验证(On-Demand Revalidation)全链路。读完后你将掌握该示例的完整配置流程,并理解其背后 revalidateTagdraftMode()、Contentful GraphQL API 等关键实现。

示例项目总览

该示例展示 Next.js 的静态生成(Static Generation)能力,以 Contentful 作为博客数据源,支持多作者、多文章,并内置草稿预览与按需重新验证两套工作流。项目基于 App Router(app/ 目录)编写,使用 TypeScript 与 Tailwind CSS,核心结构如下:

快速开始:用 create-next-app 初始化项目

按照 README,可以通过 create-next-app 以 npm、Yarn 或 pnpm 引导该示例:

npx create-next-app --example cms-contentful cms-contentful-app
yarn create next-app --example cms-contentful cms-contentful-app
pnpm create next-app --example cms-contentful cms-contentful-app

Contentful 侧配置

第 1 步:创建账号与 Space

在 Contentful 创建账号后,从 Dashboard 新建一个空的 Space,可任意命名。后续所有内容模型与内容条目都归属于这个 Space。

第 2 步:创建内容模型

内容模型定义应用的数据结构。本示例需要两个内容类型:Author(作者)Post(文章)。既可以运行脚本自动创建,也可以手动创建以熟悉 Contentful 界面。

方式一:运行 setup 脚本自动创建

先在 Contentful Dashboard 的 Settings > General Settings 中复制 Space ID;再进入 Settings > CMA tokens,点击 Create personal access token 创建一个个人访问令牌(它与登录用户拥有相同权限,切勿公开分享,仅用于初始化 Space,用后可删除)。

然后执行:

npx cross-env CONTENTFUL_SPACE_ID=YOUR_SPACE_ID CONTENTFUL_MANAGEMENT_TOKEN=XXX npm run setup

对应 package.json 中的脚本定义:

"setup": "node ./lib/setup.js"

lib/setup.js 的实现看,脚本会先校验两个环境变量是否齐全(缺失时直接抛出错误并提示正确用法),然后调用 contentful-importspaceImport,把 lib/export.json 中定义的内容结构推送到目标 Space。执行成功后控制台会打印导入摘要:

┌──────────────────────────────────────────────────┐
│ The following entities are going to be imported: │
├─────────────────────────────────┬────────────────┤
│ Content Types                   │ 2              │
├─────────────────────────────────┼────────────────┤
│ Editor Interfaces               │ 2              │
├─────────────────────────────────┼────────────────┤
│ Locales                         │ 1              │
├─────────────────────────────────┼────────────────┤
│ Webhooks                        │ 0              │
├─────────────────────────────────┼────────────────┤
│ Entries                          │ 0              │
├─────────────────────────────────┼────────────────┤
│ Assets                          │ 0              │
└─────────────────────────────────┴────────────────┘
 ✔ Validating content-file
 ✔ Initialize client (1s)
 ✔ Checking if destination space already has any content and retrieving it (2s)
 ✔ Apply transformations to source data (1s)
 ✔ Push content to destination space

方式二:手动创建内容模型

创建 Author 内容类型:进入 Space 的 Content model,新建内容类型,NameAuthorApi Identifierauthor,然后添加字段:

  • nameText 字段(类型 short text),Field ID 设为 name
  • pictureMedia 字段(类型 one file),Field ID 设为 picture

创建 Post 内容类型NamePostApi Identifierpost,添加以下字段:

  • titleText 字段(类型 short text
  • contentRich text 字段
  • excerptText 字段(类型 Long text, full-text search
  • coverImageMedia 字段(类型 one file
  • dateDate and time 字段
  • slugText 字段。可选:在字段设置的 Appearance 中选择 Slug,将其显示为 title 字段的 slug
  • authorReference 字段(类型 one reference),引用 Author 类型

这些字段名与 lib/api.tsPOST_GRAPHQL_FIELDS 查询的字段一一对应(slugtitlecoverImagedateauthor.name/pictureexcerptcontent)。

第 3 步:验证内容模型

无论通过脚本还是手动完成,最终的模型应包含 AuthorPost 两个内容类型,字段与上文定义一致。

第 4 步:填充内容

进入 Space 的 Content 区域,点击 Add entry

  • 先创建 1 个 Author 条目,文本可用占位数据,图片可下载任意图片素材;
  • 再创建至少 2 个 Post 条目,填写标题、富文本正文、摘要、封面图、日期与 slug,并关联前面创建的 Author。

重要:每个条目和素材都必须点击 Publish。否则条目停留在草稿状态,不会出现在博客中(但可以被 Draft Mode 预览,见下文)。

第 5 步:配置环境变量

进入 Settings > API keys,页面会提供一个默认的 Delivery / Preview token,可直接使用(也可新建)。然后把项目根目录的 .env.local.example(对应 examples/cms-contentful/.env.local.example)复制为 .env.local(该文件会被 Git 忽略):

cp .env.local.example .env.local

各变量含义:

  • CONTENTFUL_SPACE_ID:API Key 中的 Space ID
  • CONTENTFUL_ACCESS_TOKENContent Delivery API 的 access token,用于获取已发布内容;
  • CONTENTFUL_PREVIEW_ACCESS_TOKENContent Preview API 的 access token,用于 Draft Mode 下获取草稿内容;
  • CONTENTFUL_PREVIEW_SECRET:任意 URL 友好的值,Contentful Dashboard 会将其作为查询参数传给站点以启用 Next.js Draft Mode;
  • CONTENTFUL_REVALIDATE_SECRET:任意值,作为 Webhook 请求的 secret 头,用于 On-Demand Revalidation

最终 .env.local 形如:

CONTENTFUL_SPACE_ID=...
CONTENTFUL_ACCESS_TOKEN=...
CONTENTFUL_PREVIEW_ACCESS_TOKEN=...
CONTENTFUL_PREVIEW_SECRET=...
CONTENTFUL_REVALIDATE_SECRET=...

第 6 步:启动开发模式

npm install
npm run dev

# or

yarn install
yarn dev

博客随后运行在 http://localhost:3000。

数据获取层:lib/api.ts 的实现

lib/api.ts 源码看,所有数据访问统一走 Contentful GraphQL API:

async function fetchGraphQL(query: string, preview = false): Promise<any> {
  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 }),
      next: { tags: ["posts"] },
    },
  ).then((response) => response.json());
}

两个值得注意的设计:

  1. 双令牌切换preview 参数决定使用 CONTENTFUL_PREVIEW_ACCESS_TOKEN 还是 CONTENTFUL_ACCESS_TOKEN,并在 GraphQL 查询中通过 preview: true/false 请求草稿数据,这正是 Draft Mode 能展示未发布内容的基础;
  2. next: { tags: ["posts"] }:给 fetch 请求打上 posts 缓存标签,使该请求的缓存结果可以被 revalidateTag("posts") 精确失效——这是按需重新验证能生效的关键。

对外导出三个函数:getAllPosts(首页,按日期降序取全部有 slug 的文章)、getPostAndMorePosts(详情页,取指定文章 + 另外 2 篇推荐)、getPreviewPostBySlug(Draft Mode 下按 slug 取草稿版本)。

页面侧的消费方式:

export async function generateStaticParams() {
  const allPosts = await getAllPosts(false);
  return allPosts.map((post) => ({
    slug: post.slug,
  }));
}
  • 页面组件通过 draftMode()(来自 next/headers)读取当前是否处于草稿模式,再决定拉取已发布内容还是预览内容,见 app/page.tsx 中的 const { isEnabled } = await draftMode()

Draft Mode:预览未发布的草稿内容

示例内置了三个 API 路由:

//@ts-ignore
export { enableDraftHandler as GET } from "@contentful/vercel-nextjs-toolkit/app-router";

enableDraftHandler 会校验请求中的 secret 参数与 CONTENTFUL_PREVIEW_SECRET 是否一致,并携带 slug 参数重定向到对应文章页,同时开启草稿模式 Cookie;

在 Contentful 中接入 Content Preview:进入 Settings > Content preview,为开发环境新建一个内容预览(例如命名 Development),在 Content preview URLs 中勾选 Post 并填写:

http://localhost:3000/api/draft?secret=<CONTENTFUL_PREVIEW_SECRET>&slug={entry.fields.slug}

其中 <CONTENTFUL_PREVIEW_SECRET> 替换为 .env.local 中的对应值。保存后:

  1. 打开某篇文章条目,更新标题(例如在标题前加 [Draft]);
  2. 条目状态会自动变为 CHANGED,此时不要发布,保持草稿状态;
  3. 点击侧边栏的 Open preview 按钮,即可在站点上看到更新后的草稿标题;
  4. 想退出 Draft Mode 时,在浏览器访问 /api/disable-draft 即可。

On-Demand Revalidation:Webhook 触发的按需重新验证

发布内容后无需重新构建,即可让站点反映最新内容。在 Contentful Space 的 Settings > Webhooks 中新建 Webhook:

  • 命名:任意;
  • Activate:勾选激活;
  • POST URL:使用上一步部署(或本地)的站点地址加 /api/revalidate 路径,例如 https://<YOUR_VERCEL_DEPLOYMENT_URL>/api/revalidate
  • Triggers:可对所有事件触发,也可仅选择 Entries / Assets 的 Publishing 与 Unpublishing 事件;
  • Secret Header:添加名为 x-vercel-reval-key 的请求头,值填 CONTENTFUL_REVALIDATE_SECRET
  • Content type:选择 application/json

服务端校验逻辑在 app/api/revalidate/route.ts 中一目了然:

import { NextRequest, NextResponse } from "next/server";
import { revalidateTag } from "next/cache";

export async function POST(request: NextRequest) {
  const requestHeaders = new Headers(request.headers);
  const secret = requestHeaders.get("x-vercel-reval-key");

  if (secret !== process.env.CONTENTFUL_REVALIDATE_SECRET) {
    return NextResponse.json({ message: "Invalid secret" }, { status: 401 });
  }

  revalidateTag("posts");

  return NextResponse.json({ revalidated: true, now: Date.now() });
}

即:先比对 x-vercel-reval-key 头与 CONTENTFUL_REVALIDATE_SECRET 环境变量,不匹配返回 401;匹配则调用 revalidateTag("posts"),使 lib/api.ts 中带 tags: ["posts"] 的缓存失效,下次请求重新拉取数据并重新生成首页与对应文章页面。

验证是否成功:在 Contentful 中编辑一篇帖子并点击 Publish,已部署站点应立即展示新内容而不触发构建;可在 Contentful 的 Webhook 请求日志中确认收到 200 状态码(若调用失败,优先检查部署环境中的 CONTENTFUL_REVALIDATE_SECRET 是否与 Webhook 请求头一致)。

部署

  • 部署本地项目:将代码推送到代码托管平台后导入部署平台,导入时务必在 Environment Variables 中补齐与 .env.local 一致的全部变量。
  • 从模板部署:直接使用该示例的模板一键部署,会自动部署 Next.js 项目并通过 Contentful 集成连接你的 Space;若使用 Draft Mode,还需额外添加 CONTENTFUL_PREVIEW_SECRET 环境变量。

相关示例

仓库 examples/ 目录下还提供了一批同类 CMS 集成示例,可作对比参考:WordPressSanityGhostPrismicDatoCMSPayloadStoryblokCosmicTinaBlog Starter 等(完整列表见 examples/cms-contentful/README.md)。

登录后查看全文
热门项目推荐
相关项目推荐