Next.js 静态博客实战:基于 Builder.io CMS 的静态生成与预览模式实现
本篇以 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 架构,主要目录结构如下:
- pages/index.js:首页,展示首篇精选文章与其余文章列表;
- pages/posts/[slug].js:文章详情页,支持草稿预览渲染;
- pages/api/preview.js 与 pages/api/exit-preview.js:开启 / 退出预览模式的 API 路由;
- lib/api.js 与 lib/constants.js:Builder.io 数据获取逻辑与全局配置;
- builder/ 目录:Builder.io 的内容模型定义(文章、作者模型 Schema 与示例内容)。
如何运行示例
使用 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
- 注册 Builder.io 账号,进入组织的设置页面,创建一个私有密钥(private key);
- 在示例项目目录下执行:
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
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-ancestorsCSP 头将站点允许嵌入的父页面限定为 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 拒绝; - 由于
apiKey以NEXT_PUBLIC_前缀暴露,它属于公共只读 Key,仅用于读取已发布/授权内容,切勿替换为私有密钥。
相关 CMS 示例
Next.js 仓库提供了大量同类 CMS 集成的静态博客示例,可作为对照学习,位于 examples/ 目录下:cms-agilitycms、cms-buttercms、cms-contentful、cms-cosmic、cms-datocms、cms-dotcms、cms-drupal、cms-enterspeed、cms-ghost、cms-graphcms、cms-kontent-ai、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。
小结
Builder.io 示例完整呈现了 Next.js 静态生成博客的三种核心能力:构建期 getStaticPaths + getStaticProps 预生成全部文章页;revalidate 驱动的增量再生成保证内容时效;以及基于预览 Cookie 的 Draft Mode 让编辑者在 Builder 中点击 "View current draft" 即可看到未发布内容的真实站点效果。配合 CSP 头与 Builder 图片 loader 的细节配置,该示例可作为"Headless CMS + Next.js 静态站点"的参考实现直接落地。
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 StartedRust0625
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