首页
/ Next.js × DatoCMS:用静态生成与 Preview Mode 构建 Headless CMS 博客的完整实践

Next.js × DatoCMS:用静态生成与 Preview Mode 构建 Headless CMS 博客的完整实践

2026-09-06 15:20:11作者:姚月梅Lane

本文基于 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;样式侧使用 tailwindcsspostcssautoprefixer

快速开始:用 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 菜单中:

  1. 选择 Author 创建一条新记录——只需要 1 条即可,文字可用占位数据,头像可从 Unsplash 下载。
  2. 选择 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)

  1. 在 DatoCMS 中打开一篇文章:修改标题(例如在标题前加 [Draft]),点击 Save不要点击 Publish,此时该文章处于草稿状态。
  2. 直接访问文章页不会看到新标题(静态页仍是旧内容);但通过 Preview Mode 即可看到变更。若文章没有变成草稿,需回到 Post 模型设置的 Additional Settings 中确认已打开 Enable draft/published system
  3. 访问以下 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_TOKENDATOCMS_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;
}

两个值得注意的实现细节:

  • 预览端点切换:当 previewtrue 时,请求地址从 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):

  1. 双重校验req.query.secret 必须与 process.env.DATOCMS_PREVIEW_SECRET 完全一致,且必须携带 slug,任一不满足即返回 401 Invalid token
  2. slug 真实性校验:调用 getPreviewPostBySlug(req.query.slug) 走 DatoCMS /preview 端点确认该 slug 确实存在,否则返回 401 Invalid slug——防止任意 slug 换取预览 Cookie。
  3. 开启 Draft Moderes.setDraftMode({ enable: true }) 通过写入 Cookie 标记后续请求处于草稿模式,从而让 getStaticPropspreview 参数变为 true
  4. 安全重定向:使用 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 接法差异),相对仓库根目录的路径如下:

小结

cms-datocms 示例以最小化的代码量完整演示了 headless CMS + 静态生成的标准范式:DatoCMS 侧通过模型与草稿/发布系统管理内容,Next.js 侧用 getStaticPaths/getStaticProps 在构建期预生成页面,用 /preview 端点与 Draft Mode Cookie 打通"编辑中内容"的实时预览,并以 secret + slug 双重校验、307 安全重定向保证预览入口不被滥用。这套模式可以直接迁移到 Contentful、Sanity、Prismic 等其他 CMS 示例中。

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