Next.js 静态生成与 Kontent.ai:搭建支持 Preview Mode 的 Headless CMS 博客实战
本篇基于 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.js 与 postcss.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 创建内容模型:Author 与 Post
内容模型(content model)定义站点的数据结构,本示例需要两个内容类型。有两种方式:
方式 A:自动导入(推荐)
仓库内附带了 kontent-ai-backup.zip 备份包,包含内容模型和示例数据。步骤:
-
登录 Kontent.ai 应用;
-
进入 "Project Settings" → API keys,开启 Management API;
-
复制
Project ID和Management APIkey; -
安装 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 -
回到项目,发布(Publish)所有导入的条目;
-
导入完成后可停用 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 1 个 Author 类型条目 |
这些元素与仓库中由模型生成器产出的 TypeScript 类型严格对应。例如 models/content-types/post.ts 声明了 Post 类型的 elements:author 为 LinkedItemsElement<Author>,content 为 RichTextElement,cover_image 为 AssetsElement,slug 为 UrlSlugElement;models/content-types/author.ts 则声明了 name(Text)与 picture(Asset)。这些类型注释标注由 @kontent-ai/model-generator 生成,保证编译期类型安全。
填入数据:在 Content & Assets 的 Content 标签页中新建条目——至少 1 个 Author 和 2 个 Post(图片可从 Unsplash 下载),Post 需关联前面创建的 Author。关键步骤:每个条目都要点击 Publish,否则条目停留在 draft 工作流,Delivery API(默认模式)读取不到。
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.ts 用 parsePost/parseAuthor(L20-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.tsx 的 getStaticProps 逻辑更简单: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。
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();
三层防护值得逐条理解:
- 密钥比对:请求方必须携带与
KONTENT_PREVIEW_SECRET完全一致的secret,防止任意访客激活预览; - slug 存在性校验:以预览模式真实调用 CMS 查询,slug 无效则 401,避免为不存在的草稿建立 Draft 会话;
- 防开放重定向:第 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 端到端演练
- 启动
npm run dev; - 在 Kontent.ai 打开某篇文章,新建版本并修改标题(例如前缀
[Draft]——注意标题变更会联动重新生成 URL slug,只想改其他字段也可以); - 不要点击 Publish,让条目停留在 draft 工作流;
- 点击界面上的 Preview 按钮,即触发 6.1 配置的 URL,进入
preview.ts流程; - 页面此时展示修改后的标题,顶部出现退出预览的提示;退出后页面恢复为已发布版本。
七、部署与后续
将项目推送到 Git 仓库并导入 Vercel 即可部署(文档中的示例提供一键部署按钮)。关键注意事项:导入 Vercel 时必须在 Environment Variables 中配置与 .env.local 一致的 KONTENT_PROJECT_ID、KONTENT_PREVIEW_API_KEY、KONTENT_PREVIEW_SECRET 三个变量,且 Kontent.ai 中的 Preview URL 需要相应改为线上域名。
仓库内还有一组同构的 CMS 集成示例可供横向参考,结构均可类比:cms-agilitycms、cms-builder-io、cms-buttercms、cms-contentful、cms-cosmic、cms-datocms、cms-dotcms、cms-drupal、cms-enterspeed、cms-ghost、cms-graphcms、cms-makeswift、cms-payload、cms-plasmic、cms-prepr、cms-prismic、cms-sanity、cms-sitecore-xmcloud、cms-sitefinity、cms-storyblok、cms-takeshape、cms-tina、cms-umbraco、cms-umbraco-heartcore、cms-webiny、cms-wordpress,以及自带内容的 blog-starter。
小结
这个示例浓缩了 Headless CMS 静态博客的完整工程范式:内容模型(Author/Post)→ 类型安全的数据访问层(Delivery SDK + 生成的 models)→ 静态生成(getStaticPaths/getStaticProps)→ 受保护的 Draft Mode 预览。其中 preview.ts 的密钥校验、slug 回查与防开放重定向三点,是把 CMS 工作流真正接入生产站点前必须照做的安全细节。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

