首页
/ Next.js 静态生成与 Kontent.ai:搭建支持 Preview Mode 的 Headless CMS 博客实战

Next.js 静态生成与 Kontent.ai:搭建支持 Preview Mode 的 Headless CMS 博客实战

2026-09-06 15:46:29作者:廉彬冶Miranda

本篇基于 Next.js 仓库中的 cms-kontent-ai 示例,完整讲解如何用 Kontent.ai(Kontent)作为数据源构建一个静态生成(Static Generation)博客:从内容模型(Author/Post)的创建与导入、环境变量配置,到 getStaticPaths/getStaticProps 的数据获取链路,再到 CMS 端一键触发 Next.js Preview Mode(Draft Mode)的完整流程。读完你可以独立复现该示例,并理解静态博客如何实时预览尚未发布的内容。

一、示例项目概览

该示例位于 examples/cms-kontent-ai,展示的是 Next.js 的静态生成特性:所有文章页面在构建时通过 CMS 数据预渲染为静态 HTML,运行时无需再查库。从 package.json 可以看到核心依赖:

  • @kontent-ai/delivery-sdk:Kontent.ai 的 Delivery API 官方 SDK,负责读取已发布(或预览态)内容;
  • next / react / react-dom:基于 React 18 与 Pages Router(pages/ 目录组织);
  • remark / remark-html:将 CMS 的富文本内容渲染为 HTML;
  • tailwindcss / postcss:样式方案(见 tailwind.config.jspostcss.config.js)。

目录结构与职责一一对应:

examples/cms-kontent-ai/
├── pages/                  # Pages Router 页面
│   ├── index.tsx           # 博客首页(文章列表)
│   ├── posts/[slug].tsx    # 文章详情页(动态路由)
│   └── api/
│       ├── preview.ts      # 开启 Preview Mode 的 API 路由
│       └── exit-preview.ts # 退出 Preview Mode 的 API 路由
├── lib/
│   ├── api.ts              # Delivery SDK 封装:全部数据获取函数
│   └── constants.ts        # 站点元信息(CMS_NAME 等)
├── models/                 # 由 @kontent-ai/model-generator 生成的内容类型定义
│   └── content-types/      # author.ts / post.ts
├── viewmodels/             # 解析后的纯数据视图模型(供组件消费)
├── components/             # 展示组件(hero-post、post-body 等)
├── kontent-ai-backup.zip   # 一键导入的内容模型 + 示例数据备份包
└── .env.local.example      # 环境变量模板

这套"models(SDK 原始类型)→ api.ts(解析)→ viewmodels(视图模型)→ components(渲染)"的分层,是本示例最值得借鉴的工程结构。

二、获取并运行示例

使用 create-next-app 引导示例(对应仓库中的 packages/create-next-app),支持三种包管理器:

npx create-next-app --example cms-kontent-ai cms-kontent-app
yarn create next-app --example cms-kontent-ai cms-kontent-app
pnpm create next-app --example cms-kontent-ai cms-kontent-app

若直接检出本仓库,则进入目录后安装依赖:

cd examples/cms-kontent-ai
npm install
npm run dev

博客默认运行在 http://localhost:3000。在配置好 Kontent.ai 项目并填入环境变量之前,页面会因为缺少内容而无法正常展示文章。

三、配置 Kontent.ai:账号、内容模型与数据

3.1 创建账号与空项目

在 Kontent.ai 注册账号(官方提供 30 天试用,之后可切换到免费的 developer plan),并创建一个空项目。

3.2 创建内容模型:AuthorPost

内容模型(content model)定义站点的数据结构,本示例需要两个内容类型。有两种方式:

方式 A:自动导入(推荐)

仓库内附带了 kontent-ai-backup.zip 备份包,包含内容模型和示例数据。步骤:

  1. 登录 Kontent.ai 应用;

  2. 进入 "Project Settings" → API keys,开启 Management API

  3. 复制 Project IDManagement API key;

  4. 安装 Kontent.ai Backup Manager 并执行恢复:

    npm i -g @kontent-ai/backup-manager
    kbm --action=restore --apiKey=<Management API key> --projectId=<Project ID> --zipFilename=kontent-ai-backup
    
  5. 回到项目,发布(Publish)所有导入的条目

  6. 导入完成后可停用 Management API key,后续步骤不再需要它。

方式 B:手动创建(熟悉 Kontent.ai 界面)

Author 内容类型包含两个元素:

元素 类型 说明
Name Text 作者名
Picture Asset 限制最多选择 At most 1 个资源,且文件类型仅限 Adjustable images

Post 内容类型包含:

元素 类型 说明
Title Text 标题
Date Date & time 发布日期
Excerpt Text 摘要
Content Rich Text 正文
Cover Image Asset 最多 1 个资源,仅限 Adjustable images
Slug URL slug Title 自动生成
Author Linked items 恰好关联 Exactly 1Author 类型条目

这些元素与仓库中由模型生成器产出的 TypeScript 类型严格对应。例如 models/content-types/post.ts 声明了 Post 类型的 elementsauthorLinkedItemsElement<Author>contentRichTextElementcover_imageAssetsElementslugUrlSlugElementmodels/content-types/author.ts 则声明了 name(Text)与 picture(Asset)。这些类型注释标注由 @kontent-ai/model-generator 生成,保证编译期类型安全。

填入数据:在 Content & Assets 的 Content 标签页中新建条目——至少 1 个 Author2 个 Post(图片可从 Unsplash 下载),Post 需关联前面创建的 Author。关键步骤:每个条目都要点击 Publish,否则条目停留在 draft 工作流,Delivery API(默认模式)读取不到。

已发布的 Post 条目概览

3.3 设置环境变量

复制模板文件(.env.local 已在 Git 中忽略):

cp .env.local.example .env.local

模板 .env.local.example 包含三个空值变量,需在 Kontent.ai 的 Project settings > API keys 中取值填入:

变量 取值来源 作用
KONTENT_PROJECT_ID Project ID 标识 Kontent.ai 项目,Delivery SDK 请求的基础
KONTENT_PREVIEW_API_KEY 其中一个 Preview API key 预览模式下读取 draft 内容时使用
KONTENT_PREVIEW_SECRET 任意随机字符串(避免空格),如 MY_SECRET 校验 Preview Mode 激活请求的共享密钥

四、源码解析:数据获取层(lib/api.ts)

所有 CMS 访问集中在 lib/api.ts。客户端初始化时有两个值得注意的细节:

const client = new DeliveryClient({
  projectId: process.env.KONTENT_PROJECT_ID ?? "",
  previewApiKey: process.env.KONTENT_PREVIEW_API_KEY,
  globalHeaders: (_queryConfig) => [
    {
      header: "X-KC-SOURCE",
      value: `@vercel/next.js/example/${pkg.name};${pkg.version}`,
    },
  ],
});
  • previewApiKey 让同一个客户端支持双模式:默认读取已发布内容;当查询显式开启预览时,改用 Preview key 读取 draft 版本——这正是 Preview Mode 的数据基础;
  • globalHeaders 注入的 X-KC-SOURCE 是来源追踪头,便于 CMS 侧统计该 SDK 调用来自哪个集成(从源码结构看,这是 Kontent.ai 的 SDK 归因约定)。

CMS 返回的条目是结构化的元素容器,组件不便直接消费,因此 api.tsparsePost/parseAuthorL20-L37)将模型扁平化为 viewmodels/post.ts 定义的纯数据:{ title, slug, date, content, excerpt, coverImage, author }。注意 parsePost 中通过 post.elements.author.linkedItems[0] 展开关联的 Author 元素——对应内容模型里 Exactly 1 的 Linked items 约束。

四个数据函数构成完整取数能力:

函数 用途 关键 API
getAllPostSlugs() 构建期枚举全部文章 slug .type(post.codename) + .elementsParameter(["slug"])
getPostBySlug(slug, preview) 按 slug 取单篇 .equalsFilter("elements.slug", slug)
getMorePostsForSlug(slug, preview) 相关文章(取 2 篇) .orderByDescending("elements.date") + .notEqualsFilter(...) + .limitParameter(2)
getAllPosts(preview) 首页全部文章 .orderByDescending("elements.date")

其中 preview 参数直接映射到 SDK 的 queryConfig({ usePreviewMode }),同一组函数即可服务于普通构建与预览态页面。

五、静态生成链路:getStaticPaths 与 getStaticProps

文章详情页 pages/posts/[slug].tsx 是标准的静态生成三件套:

export async function getStaticProps({ params, preview = null }: StaticProps) {
  return await Promise.all([
    getPostBySlug(params.slug, preview ?? false),
    getMorePostsForSlug(params.slug, preview ?? false),
  ]).then((values) => ({
    props: { post: values[0], morePosts: values[1], preview },
  }));
}

export async function getStaticPaths() {
  const slugs = await getAllPostSlugs();
  return {
    paths: slugs.map((slug) => ({ params: { slug } })),
    fallback: false,
  };
}

要点:

  • 构建时 getAllPostSlugs() 一次性拉取全部已发布文章的 slug,生成完整路由集合;
  • fallback: false 表示未预渲染的 slug 直接 404,与组件内 if (!router.isFallback && !post?.slug) return <ErrorPage statusCode={404} /> 的兜底逻辑配合(L26-L30);
  • preview ?? false 将 Preview Mode 状态透传给取数函数,使详情页在预览态下渲染 draft 内容。

首页 pages/index.tsxgetStaticProps 逻辑更简单:getAllPosts(preview) 取回按日期倒序排列的全部文章,组件内以第一篇作为 HeroPost 大图展示、其余交给 MoreStories 列表。

六、Preview Mode:从 CMS 的 Preview 按钮到 Draft 页面

这是本示例最有实战价值的部分。目标:编辑在 Kontent.ai 中修改了尚未发布的草稿后,点击 Preview 按钮即可在本地站点看到草稿效果。

6.1 在 Kontent.ai 配置 Preview URL

进入项目的 Project Settings > Preview URLs,为 Post 内容类型添加预览 URL:

http://localhost:3000/api/preview?secret=<KONTENT_PREVIEW_SECRET>&slug={URLslug}

其中 <KONTENT_PREVIEW_SECRET> 需替换为 .env.local 中的实际值,{URLslug} 是 Kontent.ai 提供的占位符,会自动填入当前条目的 slug。

Kontent.ai 中配置 Post 类型的 Preview URL

6.2 preview API 路由:开启 Draft Mode

pages/api/preview.ts 是整个预览机制的安全核心:

// 1. 校验共享密钥与 slug 参数
if (
  req.query.secret !== process.env.KONTENT_PREVIEW_SECRET ||
  !req.query.slug
) {
  return res.status(401).json({ message: "Invalid token or slug not specified" });
}

// 2. 用预览模式回查 CMS,确认该 slug 真实存在
const post = await getPostBySlug(req.query.slug as string, true);
if (!post) {
  return res.status(401).json({ message: "Invalid slug" });
}

// 3. 设置 Draft Mode cookie,并按"查回来的" slug 重定向
res.setDraftMode({ enable: true });
res.writeHead(307, { Location: `/posts/${post.slug}` });
res.end();

三层防护值得逐条理解:

  1. 密钥比对:请求方必须携带与 KONTENT_PREVIEW_SECRET 完全一致的 secret,防止任意访客激活预览;
  2. slug 存在性校验:以预览模式真实调用 CMS 查询,slug 无效则 401,避免为不存在的草稿建立 Draft 会话;
  3. 防开放重定向:第 3 步重定向到的是从 CMS 查回的 post.slug,而非用户传入的 req.query.slug——源码注释明确说明这是为了防止 open redirect 漏洞。

res.setDraftMode({ enable: true }) 会写入 Next.js 的 __prerender_bypass cookie,此后请求走 getStaticProps 但带 preview 参数(即走草稿数据),实现"静态架构 + 草稿内容"的实时预览。退出预览由 pages/api/exit-preview.ts 完成:res.setDraftMode({ enable: false }) 移除 cookie 后 307 重定向回首页;预览页顶部的 "Click here to exit preview mode" 提示条(由 Layout preview={preview} 驱动)即调用该路由。

6.3 端到端演练

  1. 启动 npm run dev
  2. 在 Kontent.ai 打开某篇文章,新建版本并修改标题(例如前缀 [Draft]——注意标题变更会联动重新生成 URL slug,只想改其他字段也可以);
  3. 不要点击 Publish,让条目停留在 draft 工作流;
  4. 点击界面上的 Preview 按钮,即触发 6.1 配置的 URL,进入 preview.ts 流程;
  5. 页面此时展示修改后的标题,顶部出现退出预览的提示;退出后页面恢复为已发布版本。

七、部署与后续

将项目推送到 Git 仓库并导入 Vercel 即可部署(文档中的示例提供一键部署按钮)。关键注意事项:导入 Vercel 时必须在 Environment Variables 中配置与 .env.local 一致的 KONTENT_PROJECT_IDKONTENT_PREVIEW_API_KEYKONTENT_PREVIEW_SECRET 三个变量,且 Kontent.ai 中的 Preview URL 需要相应改为线上域名。

仓库内还有一组同构的 CMS 集成示例可供横向参考,结构均可类比:cms-agilitycmscms-builder-iocms-buttercmscms-contentfulcms-cosmiccms-datocmscms-dotcmscms-drupalcms-enterspeedcms-ghostcms-graphcmscms-makeswiftcms-payloadcms-plasmiccms-preprcms-prismiccms-sanitycms-sitecore-xmcloudcms-sitefinitycms-storyblokcms-takeshapecms-tinacms-umbracocms-umbraco-heartcorecms-webinycms-wordpress,以及自带内容的 blog-starter

小结

这个示例浓缩了 Headless CMS 静态博客的完整工程范式:内容模型(Author/Post)→ 类型安全的数据访问层(Delivery SDK + 生成的 models)→ 静态生成(getStaticPaths/getStaticProps)→ 受保护的 Draft Mode 预览。其中 preview.ts 的密钥校验、slug 回查与防开放重定向三点,是把 CMS 工作流真正接入生产站点前必须照做的安全细节。

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