Next.js × DatoCMS:用静态生成与 Preview Mode 构建 Headless CMS 博客的完整实践
本文基于 Next.js 仓库中的 examples/cms-datocms 示例,讲解如何以 DatoCMS 作为数据源,通过 getStaticPaths / getStaticProps 实现一个静态生成的博客站点,并结合 Draft Mode(预览模式)在内容发布前实时预览草稿。读完后,你将完整掌握从 DatoCMS 建模、环境变量配置、数据获取层设计到本地预览与部署的全流程,并能读懂示例中每一处关键源码的实现意图。
示例项目概述
该示例展示的是 Next.js 的**静态生成(Static Generation)**能力:文章数据来自 DatoCMS 的 GraphQL API,在构建/请求时预取并生成静态页面,而草稿内容则通过 Preview Mode 绕过静态产物直接实时渲染。
示例采用 Pages Router 技术栈,核心文件组织如下(相对仓库根目录):
| 文件 | 职责 |
|---|---|
| pages/index.js | 首页,getStaticProps 拉取最新文章列表 |
| pages/posts/[slug].js | 文章详情页,getStaticPaths 预生成所有文章路径 |
| pages/api/preview.js | 启用 Draft Mode 的 API 路由 |
| pages/api/exit-preview.js | 退出 Draft Mode 的 API 路由 |
| lib/api.js | DatoCMS GraphQL 数据获取层(4 个查询函数) |
| lib/markdownToHtml.js | 将正文 Markdown 转成 HTML |
| lib/constants.js | 站点常量(如 CMS 名称) |
| next.config.js | 配置 next/image 允许加载 DatoCMS 图片域名 |
| components/ | 展示层组件(文章头部、封面、正文、预览提示条等) |
依赖方面,package.json 中的关键运行时依赖为:react-datocms(渲染 DatoCMS 图片组件)、remark + remark-html(Markdown 转 HTML)、date-fns(日期格式化)、classnames;样式侧使用 tailwindcss、postcss、autoprefixer。
快速开始:用 create-next-app 引导项目
执行 create-next-app 并指定 cms-datocms 示例即可引导完整项目,支持 npm、Yarn 与 pnpm 三种方式:
npx create-next-app --example cms-datocms cms-datocms-app
yarn create next-app --example cms-datocms cms-datocms-app
pnpm create next-app --example cms-datocms cms-datocms-app
引导完成后,项目目录中会包含示例源码以及一个 .env.local.example 环境变量模板文件(见 examples/cms-datocms/.env.local.example),其中只定义了两个待填写的变量:
DATOCMS_API_TOKEN=
DATOCMS_PREVIEW_SECRET=
DatoCMS 端配置:项目与模型设计
第 1 步:创建 DatoCMS 账号与项目
在 DatoCMS 上注册账号后,从 Dashboard 创建一个新项目(New project),可以选择 Blank Project(空白项目)。
第 2 步:创建 Author(作者)模型
在项目的设置页新建一个 Model,命名为 Author,然后添加以下字段(无需修改默认设置):
| 字段名 | 字段类型 |
|---|---|
Name |
Text 字段(Single-line String) |
Picture |
Media 字段(Single asset) |
第 3 步:创建 Post(文章)模型
同样新建一个 Model:
- 名称为
Post。 - 重点:在 "Additional Settings"(附加设置)页签中打开 Enable draft/published system(启用草稿/发布系统)。这是后续预览模式能区分草稿与已发布内容的前提。
字段清单如下(除特别注明外,不必修改设置):
| 字段名 | 字段类型 | 额外配置 |
|---|---|---|
Title |
Text(Single-line String) | — |
Content |
Text(Multiple-paragraph Text,支持 Markdown) | — |
Excerpt |
Text(Single-line String) | — |
Cover Image |
Media(Single asset) | — |
Date |
Date and time(Date) | — |
Author |
Links(Single link) | 在 "Validations" 页签的 "Accept only specified model" 中选择 Author |
Slug |
SEO(Slug) | 在 "Validations" 页签的 "Reference field" 中选择 Title |
其中 Slug 字段引用 Title 自动生成,正好对应 Next.js 端 pages/posts/[slug].js 的动态路由参数;Author 的单链接校验保证了文章只能关联作者模型,与代码中 author { name picture } 的 GraphQL 查询结构一一对应。
第 4 步:录入内容
在顶部 Content 菜单中:
- 选择 Author 创建一条新记录——只需要 1 条即可,文字可用占位数据,头像可从 Unsplash 下载。
- 选择 Post 创建新记录,建议至少创建 2 条文章;Content 字段支持写 Markdown;封面图可从 Unsplash 下载;并选择之前创建的 Author。
重要:每条 Post 记录保存后必须点击 Publish,否则文章会停留在草稿(draft)状态,不会出现在正式内容中。
环境变量配置
在 DatoCMS 顶部 Settings 菜单中点击 API tokens,然后复制 Read-only API token(只读 API token)。
将示例目录中的 .env.local.example 复制为 .env.local(该文件会被 Git 忽略):
cp .env.local.example .env.local
然后在 .env.local 中填写两个变量:
| 变量 | 取值说明 | 用途 |
|---|---|---|
DATOCMS_API_TOKEN |
上一步复制的只读 API token | 携带为 Authorization: Bearer <token> 请求 DatoCMS GraphQL API |
DATOCMS_PREVIEW_SECRET |
任意随机字符串(避免空格),如 MY_SECRET |
作为开启 Preview Mode 的身份校验密钥 |
填写后的 .env.local 形如:
DATOCMS_API_TOKEN=...
DATOCMS_PREVIEW_SECRET=...
本地运行与预览模式
第 6 步:以开发模式运行 Next.js
npm install
npm run dev
# 或
yarn install
yarn dev
博客运行在 http://localhost:3000。
第 7 步:体验预览模式(Preview Mode)
- 在 DatoCMS 中打开一篇文章:修改标题(例如在标题前加
[Draft]),点击 Save 但不要点击 Publish,此时该文章处于草稿状态。 - 直接访问文章页不会看到新标题(静态页仍是旧内容);但通过 Preview Mode 即可看到变更。若文章没有变成草稿,需回到
Post模型设置的 Additional Settings 中确认已打开 Enable draft/published system。 - 访问以下 URL 开启预览:
http://localhost:3000/api/preview?secret=<secret>&slug=<slug>
<secret>:填入DATOCMS_PREVIEW_SECRET对应的字符串;<slug>:文章的slug属性值(可在 DatoCMS 中查看)。
开启后页面顶部会出现 "Click here to exit preview mode" 提示条,点击即可退出预览(对应 pages/api/exit-preview.js)。
第 8 步:部署
可将项目推送到代码托管平台后导入 Vercel 进行部署。注意:在 Vercel 项目设置中进入 Environment Variables,将 DATOCMS_API_TOKEN 与 DATOCMS_PREVIEW_SECRET 设置为与 .env.local 一致的值;也可以使用本仓库该示例对应的 Vercel 模板一键部署(部署时会要求填写上述两个环境变量)。
源码纵览:DatoCMS 数据获取层(lib/api.js)
lib/api.js 是示例的数据核心,所有页面查询都收敛到这一层。
通用请求函数与预览端点切换
fetchAPI 是所有 GraphQL 查询的统一入口(lib/api.js#L21-L40):
const API_URL = "https://graphql.datocms.com";
const API_TOKEN = process.env.DATOCMS_API_TOKEN;
async function fetchAPI(query, { variables, preview } = {}) {
const res = await fetch(API_URL + (preview ? "/preview" : ""), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_TOKEN}`,
},
body: JSON.stringify({ query, variables }),
});
const json = await res.json();
if (json.errors) {
console.error(json.errors);
throw new Error("Failed to fetch API");
}
return json.data;
}
两个值得注意的实现细节:
- 预览端点切换:当
preview为true时,请求地址从https://graphql.datocms.com变为https://graphql.datocms.com/preview。DatoCMS 的/preview端点会返回含草稿的内容,这正是预览模式能看到未发布文章的根本原因。 - 错误处理:一旦响应中包含
errors,立即打印并抛出Failed to fetch API,让构建或 SSR 阶段尽早失败,而不是渲染出残缺页面。
响应式图片 Fragment
示例定义了一个 GraphQL fragment,用来一次性取齐 DatoCMS 响应式图片的全部信息(lib/api.js#L5-L19):
const responsiveImageFragment = `
fragment responsiveImageFragment on ResponsiveImage {
srcSet webpSrcSet sizes src
width height aspectRatio
alt title bgColor base64
}
`;
其中 webpSrcSet 用于提供 WebP 变体,base64 用于渲染占位(LQIP)效果。封面图查询时通过 responsiveImage(imgixParams: {fm: jpg, fit: crop, w: 2000, h: 1000}) 指定 imgix 图片处理参数——指定格式、裁剪方式与输出尺寸,由 DatoCMS 的图片 CDN 按需产出不同分辨率的 srcSet。
四个查询函数
| 函数 | 作用 | 关键查询点 |
|---|---|---|
getAllPostsWithSlug |
构建期收集所有文章 slug | allPosts { slug },供 getStaticPaths 生成路径列表 |
getAllPostsForHome(preview) |
首页文章列表 | allPosts(orderBy: date_DESC, first: 20),含封面响应式图与作者信息(头像裁剪为 100×100 并 sat: -100 去除饱和度) |
getPostAndMorePosts(slug, preview) |
文章详情 + 推荐文章 | 按 filter: {slug: {eq: $slug}} 取单篇,另附 morePosts: allPosts(first: 2, filter: {slug: {neq: $slug}}) 取两篇最新文章作延伸阅读 |
getPreviewPostBySlug(slug) |
预览校验用 | 固定 preview: true,只取 slug,用于确认该 slug 在 CMS 中真实存在 |
源码纵览:静态生成与 Draft Mode
文章详情页:getStaticPaths + fallback
pages/posts/[slug].js 展示了完整的静态生成模式:
export async function getStaticProps({ params, preview = false }) {
const data = await getPostAndMorePosts(params.slug, preview);
const content = await markdownToHtml(data?.post?.content || "");
return {
props: {
preview,
post: { ...data?.post, content },
morePosts: data?.morePosts ?? [],
},
};
}
export async function getStaticPaths() {
const allPosts = await getAllPostsWithSlug();
return {
paths: allPosts?.map((post) => `/posts/${post.slug}`) || [],
fallback: true,
};
}
getStaticPaths通过getAllPostsWithSlug()在构建期枚举全部文章,生成/posts/<slug>路径列表;fallback: true表示遇到未预生成的 slug 时,先渲染骨架(组件中router.isFallback为真时显示 "Loading…"),再按需生成该页的静态资源。getStaticProps的第二个参数preview由 Next.js 根据当前请求是否处于 Draft Mode 自动注入:处于预览模式时它等价于true,于是getPostAndMorePosts走/preview端点取草稿数据;正文 Markdown 在此处经 lib/markdownToHtml.js(基于 remark 体系)转为 HTML 后作为 prop 传入页面。- 若 slug 查不到文章(
!post?.slug且非 fallback 状态),页面渲染next/error的 404 页。
首页 pages/index.js 更简洁:getStaticProps({ preview = false }) 调用 getAllPostsForHome(preview),取第一条作 Hero 文章、其余进入 "More Stories" 列表。
预览 API:校验、开启与退出
pages/api/preview.js 是 Draft Mode 的入口,其防御逻辑值得逐段学习(pages/api/preview.js#L4-L27):
- 双重校验:
req.query.secret必须与process.env.DATOCMS_PREVIEW_SECRET完全一致,且必须携带slug,任一不满足即返回401 Invalid token。 - slug 真实性校验:调用
getPreviewPostBySlug(req.query.slug)走 DatoCMS/preview端点确认该 slug 确实存在,否则返回401 Invalid slug——防止任意 slug 换取预览 Cookie。 - 开启 Draft Mode:
res.setDraftMode({ enable: true })通过写入 Cookie 标记后续请求处于草稿模式,从而让getStaticProps的preview参数变为true。 - 安全重定向:使用
307跳转到/posts/${post.slug},并且注释明确指出"不直接重定向到req.query.slug",以避免开放重定向(open redirect)漏洞。
退出预览由 pages/api/exit-preview.js 完成:res.setDraftMode({ enable: false }) 移除 Cookie 后同样以 307 跳回首页。页面顶部 "Click here to exit preview mode" 提示条即指向该路由。
其他工程配置要点
- 图片域名白名单:next.config.js 通过
images.remotePatterns放行https://www.datocms-assets.com/my-account/**,这是next/image加载 DatoCMS 图片 CDN 资源的必要前提:
module.exports = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "www.datocms-assets.com",
port: "",
pathname: "/my-account/**",
},
],
},
};
- 路径别名:jsconfig.json 配置了
@/*指向项目根目录,源码中因此可以写import { getAllPostsForHome } from "@/lib/api"。 - 构建脚本:package.json 仅提供
dev(next)、build(next build)、start(next start)三个脚本,静态生成的产物由next build在构建期产出。
相关 CMS 示例
Next.js 仓库为众多 headless CMS 提供了同构的博客示例(数据获取层、静态生成与预览模式结构基本一致,可横向对比 GraphQL/REST 接法差异),相对仓库根目录的路径如下:
- AgilityCMS
- Builder.io
- ButterCMS
- Contentful
- Cosmic
- DatoCMS
- DotCMS
- Drupal
- Enterspeed
- Ghost
- GraphCMS
- Kontent.ai
- MakeSwift
- Payload
- Plasmic
- Prepr
- Prismic
- Sanity
- Sitecore XM Cloud
- Sitefinity
- Storyblok
- TakeShape
- Tina
- Umbraco
- Umbraco heartcore
- Webiny
- WordPress
- Blog Starter(无 CMS 的版本)
小结
cms-datocms 示例以最小化的代码量完整演示了 headless CMS + 静态生成的标准范式:DatoCMS 侧通过模型与草稿/发布系统管理内容,Next.js 侧用 getStaticPaths/getStaticProps 在构建期预生成页面,用 /preview 端点与 Draft Mode Cookie 打通"编辑中内容"的实时预览,并以 secret + slug 双重校验、307 安全重定向保证预览入口不被滥用。这套模式可以直接迁移到 Contentful、Sanity、Prismic 等其他 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