首页
/ Next.js 静态博客实战:基于 Builder.io CMS 的静态生成与预览模式实现

Next.js 静态博客实战:基于 Builder.io CMS 的静态生成与预览模式实现

2026-09-06 15:06:16作者:舒璇辛Bertina

本篇以 Next.js 官方仓库中的 Builder.io CMS 博客示例为核心,讲解如何基于静态生成(Static Generation)构建一个数据来源于 Builder.io 的完整博客站点:从项目初始化、Builder Space 创建与 API Key 配置,到 getStaticProps / getStaticPaths 的数据获取、增量再生成,以及通过 Draft Mode 实现的实时预览(Preview Mode)全流程。读完本文,你可以完整复现示例的每一步操作,并从源码层面理解静态页数据流、预览 Cookie 机制与 Builder.io 渲染器的集成方式。

示例定位

该示例展示的是 Next.js 的静态生成功能,以 Builder.io 作为内容数据源,构建一个静态生成的博客。它位于仓库的 cms-builder-io 示例目录,采用 Pages Router 架构,主要目录结构如下:

如何运行示例

使用 create-next-app 配合示例模板快速启动项目,支持 npm、Yarn、pnpm 三种方式:

npx create-next-app --example cms-builder-io cms-builder-io-app
yarn create next-app --example cms-builder-io cms-builder-io-app
pnpm create next-app --example cms-builder-io cms-builder-io-app

配置步骤

第一步:安装 Builder.io CLI

npm install @builder.io/cli -g

第二步:创建 Space 并配置 API Key

  1. 注册 Builder.io 账号,进入组织的设置页面,创建一个私有密钥(private key);
  2. 在示例项目目录下执行:
cd cms-builder-io-app
builder create -k [private-key] -n [space-name] -d

其中 [space-name] 可取名如 "Blog",-d 表示同时初始化示例数据。命令完成后会输出新 Space 的公共 API Key,将其作为 NEXT_PUBLIC_BUILDER_API_KEY 的值写入 .env.production.env.development 文件。

源码中该环境变量被集中读取于 lib/constants.js

export const BUILDER_CONFIG = {
  apiKey: process.env.NEXT_PUBLIC_BUILDER_API_KEY,
  postsModel: "post",
  previewSecret: "micky-mouse",
};

三个配置项的作用分别为:

  • apiKey:Builder.io 的公共 API Key,所有请求均基于它鉴权;
  • postsModel:内容模型名,示例使用 post 模型,对应 builder/post/ 下的模型 Schema(schema.model.json 定义了 title、slug、intro、image、author、richtext 等字段);
  • previewSecret:预览模式的共享密钥,用于校验来自 Builder.io 的预览回调请求,避免任意人伪造预览请求。

第三步:以开发模式运行

npm install
npm run dev

# 或

yarn install
yarn dev

启动后博客将运行在 http://localhost:3000 。package.json 中脚本即 next / next build / next start,核心依赖为 @builder.io/react(Builder 渲染器)与 @builder.io/widgets(Rich Text 组件集),UI 层使用 Tailwind CSS。

静态生成数据流:从 API 层到页面

统一的数据访问层

lib/api.js 封装了所有对 Builder.io 的访问,关键实现如下:

builder.init(BUILDER_CONFIG.apiKey);
Builder.isStatic = true; // 静态模式下使用 Builder 的静态渲染

export function getAllPostsWithSlug() {
  return builder.getAll(BUILDER_CONFIG.postsModel, {
    options: { noTargeting: true },
    apiKey: BUILDER_CONFIG.apiKey,
  });
}
  • getAllPostsWithSlug:拉取所有带 slug 的文章,供 getStaticPaths 生成构建期路径;
  • searchPosts(query, preview, limit, offset):通用的分页查询,支持 MongoDB 风格的查询语法(如 { "data.slug": { $exists: true } }),预览模式下会将 staleCacheSeconds 从 200 秒压缩到 1 秒,并通过 includeUnpublished: true 纳入未发布内容;
  • getDraftPost(id):预览模式下按 ID 直接请求 Builder 的 v2 内容接口(附带 preview=true&noCache=true),用于拉取草稿全文;
  • getPostAndMorePosts(slug, preview, previewData):详情页的聚合数据函数,返回当前文章 + 2 篇"更多文章"推荐。

首页:getStaticProps 与增量再生成

pages/index.js 的静态属性函数为:

export async function getStaticProps({ preview = null }) {
  const allPosts = (await getAllPostsForHome(preview)) || [];
  return {
    props: { allPosts, preview },
    revalidate: 20, // 每 20 秒增量再生成一次
  };
}

revalidate: 20 表示页面在构建时生成一次静态 HTML 后,每 20 秒接受一次增量再生成(ISR),当 Builder 中有内容更新时,最多 20 秒后线上页面即反映最新内容——这正是"静态性能 + 动态更新"的典型静态生成策略。页面组件中,第一篇数据作为 HeroPost 精选展示,其余通过 MoreStories 渲染为卡片列表。

文章详情页:getStaticPaths 与 fallback

pages/posts/[slug].js 中:

export async function getStaticPaths() {
  const allPosts = await getAllPostsWithSlug();
  return {
    paths: allPosts?.map((post) => `/posts/${post.data.slug}`) || [],
    fallback: true, // 允许构建期未生成的 slug 在首次访问时按需生成
  };
}

export async function getStaticProps({ params, preview = false, previewData }) {
  let { post, morePosts } = await getPostAndMorePosts(
    params.slug, preview, previewData,
  );
  return {
    props: {
      key: post?.id + post?.data.slug + params.slug,
      preview,
      post,
      morePosts,
    },
  };
}

fallback: true 意味着构建时未知路径的访问会先展示 fallback UI(组件中 router.isFallback 时渲染 "Loading…" 标题),随后按需生成静态页并缓存。组件内同时处理了 404 场景:非预览、非编辑状态下取不到文章时返回 next/error 的 404 页面。

渲染层上,详情页使用 BuilderContent 渲染器接管 Rich Text 内容:

<BuilderContent
  {...(!Builder.isEditing && { content: post })}
  modelName={BUILDER_CONFIG.postsModel}
  options={{ includeRefs: true, model: BUILDER_CONFIG.postsModel }}
  isStatic
>

非编辑态使用构建期获取的静态数据渲染,编辑态则交由 Builder 实时接管;文末再拼接 2 篇 morePosts 推荐。

预览模式(Draft Mode)实现原理

这是示例中最值得拆解的部分。编辑者在 Builder.io 中点击 "View current draft" 时,Builder 会回调站点的 /api/preview?secret=... 接口(回调地址需在 Builder 的模型设置中配置;若修改了 lib/constants.js 中的 previewSecret,也需同步更新 Builder 的模型设置)。

pages/api/preview.js 的完整逻辑:

const postId = req.query[`builder.overrides.${BUILDER_CONFIG.postsModel}`];
// 1. 校验 secret 与 postId,缺失则 401
if (req.query.secret !== BUILDER_CONFIG.previewSecret || !postId) {
  return res.status(401).json({ message: "Invalid request" });
}
// 2. 校验该草稿确实存在
const post = await getDraftPost(postId);
if (!post) {
  return res.status(401).json({ message: "Invalid post" });
}
// 3. 设置预览 Cookie,开启 Next.js Draft Mode
res.setPreviewData({ postDraftId: postId });
// 4. 重定向到该文章的静态路径(不直接使用 query 中的 slug,避免开放重定向漏洞)
res.writeHead(307, {
  Location: `/posts/${post.data.slug}?${querystring.stringify(req.query)}`,
});

其安全设计有两点值得注意:一是 secret 双向校验,防止任意访客通过预览接口读取未发布内容;二是重定向目标取自服务端拉取的 post.data.slug 而非用户传入参数,规避开放重定向(open redirect)风险。

进入预览态后,页面组件通过 Layout 显示顶部的 "Click here to exit preview mode" 提示条,其背后调用 pages/api/exit-preview.js

export default async function exit(_, res) {
  res.setDraftMode({ enable: false }); // 移除预览 Cookie
  res.writeHead(307, { Location: "/" });
  res.end();
}

setDraftMode({ enable: false }) 清除 Cookie 后,下次请求即回到纯静态渲染路径。

构建配置:CSP 与远程图片

examples/cms-builder-io/next.config.js 中还有两处与 Builder.io 深度集成的关键配置:

module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "cdn.builder.io",
        pathname: "/my-account/**",
      },
    ],
  },
  async headers() {
    return [
      {
        source: "/:path*",
        headers: [
          {
            key: "Content-Security-Policy",
            value:
              "frame-ancestors https://*.builder.io https://builder.io http://localhost:1234",
          },
        ],
      },
    ];
  },
};
  • images.remotePatterns 允许 next/image 加载 Builder.io CDN 上的文章图片(限 https://cdn.builder.io/my-account/**);
  • frame-ancestors CSP 头将站点允许嵌入的父页面限定为 Builder.io 域名与本地 1234 端口——这正是 Builder 在线编辑器 iframe 预览时能够正常加载本站点的前提。

图片渲染还封装了自定义 loader,见 components/builder-image.js

const builderLoader = ({ src, width, quality }) => {
  return `${src}?width=${width}&quality=${quality || 75}`;
};

即把 next/image 的尺寸/质量参数直接拼到 Builder 图片源 URL 上,由 Builder 的图像服务完成按需缩放,保证响应式图片性能。

部署与注意事项

  • 部署时(如推送到 Git 托管仓库并导入到部署平台)需在部署平台的环境变量中配置与本地 .env 一致的 NEXT_PUBLIC_BUILDER_API_KEY
  • 预览模式的 previewSecret 与 Builder 模型设置中的回调 secret 必须保持一致,二者不同步时预览请求会被 401 拒绝;
  • 由于 apiKeyNEXT_PUBLIC_ 前缀暴露,它属于公共只读 Key,仅用于读取已发布/授权内容,切勿替换为私有密钥。

相关 CMS 示例

Next.js 仓库提供了大量同类 CMS 集成的静态博客示例,可作为对照学习,位于 examples/ 目录下:cms-agilitycmscms-buttercmscms-contentfulcms-cosmiccms-datocmscms-dotcmscms-drupalcms-enterspeedcms-ghostcms-graphcmscms-kontent-aicms-makeswiftcms-payloadcms-plasmiccms-preprcms-prismiccms-sanitycms-sitecore-xmcloudcms-sitefinitycms-storyblokcms-takeshapecms-tinacms-umbracocms-umbraco-heartcorecms-webinycms-wordpress,以及更轻量的 blog-starter

小结

Builder.io 示例完整呈现了 Next.js 静态生成博客的三种核心能力:构建期 getStaticPaths + getStaticProps 预生成全部文章页;revalidate 驱动的增量再生成保证内容时效;以及基于预览 Cookie 的 Draft Mode 让编辑者在 Builder 中点击 "View current draft" 即可看到未发布内容的真实站点效果。配合 CSP 头与 Builder 图片 loader 的细节配置,该示例可作为"Headless CMS + Next.js 静态站点"的参考实现直接落地。

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