Next.js 静态博客实战:以 cms-prepr 示例对接 Prepr 头端 CMS 实现静态生成与预览模式
本篇指南基于 Next.js 官方示例仓库中的 cms-prepr 示例,完整讲解如何以 Prepr 作为数据源搭建一个静态生成(Static Generation)博客:包括 Prepr 环境配置、.env.local 环境变量设置、GraphQL 数据获取层的实现原理、getStaticProps / getStaticPaths 的静态生成流程,以及基于 Next.js Draft Mode 的内容预览机制。读完并跟随操作后,你将能够独立部署一个支持 CMS 编辑、支持草稿预览的 Next.js 博客站点。
示例概述:静态生成博客的技术栈
该示例的核心定位是展示 Next.js 的静态生成功能以 Prepr(一个 GraphQL 优先的头端 CMS)为数据源的博客应用。示例代码位于 examples/cms-prepr 目录,整体结构如下:
- pages/index.js:首页,静态生成全部文章列表,首篇作为 Hero 展示;
- pages/posts/[slug].js:文章详情页,通过
getStaticPaths动态收集全部文章 slug; - pages/api/preview.js 与 pages/api/exit-preview.js:预览模式的进入/退出 API 路由;
- lib/api.js:封装 Prepr GraphQL API 的数据获取层;
- components/:布局、文章头部、正文、更多文章列表等 React 组件;
- 依赖极简,见 package.json:仅
classnames、date-fns、next、react、react-dom,样式由 Tailwind CSS(devDependencies 中的tailwindcss、postcss、autoprefixer)驱动。
从源码结构看,该示例完全采用 Pages Router 的静态生成模式:所有页面数据在服务端构建/按需生成时通过 GraphQL 拉取并注入,客户端不直接访问 Prepr,Access Token 只存在于服务端环境变量中。
启动示例:create-next-app 引导
按 README 说明,使用 create-next-app 配合 npm、Yarn 或 pnpm 引导示例项目:
npx create-next-app --example cms-prepr cms-prepr-app
yarn create next-app --example cms-prepr cms-prepr-app
pnpm create next-app --example cms-prepr cms-prepr-app
--example cms-prepr 参数会让 create-next-app 拉取本仓库中 examples/cms-prepr 的代码作为项目脚手架。
配置第一步:创建 Prepr 环境并加载演示数据
- 注册一个 Prepr 账号(通过 README 中的官方注册入口)。
- 在 Prepr 控制台中新建一个 environment(环境)。
- 创建环境时选择 Load demo data(加载演示数据)。这样 Prepr 会自动上传示例用的 models(如 Article、Author)、内容条目及其他数据,使本地示例博客开箱即用。
演示数据中定义了博客用到的模型结构:Article(含 _slug、_publish_on、title、excerpt、content、authors、cover 等字段)以及 Author(含 full_name、profile_pic)。这些字段与 lib/api.js 中的 GraphQL 查询一一对应——这解释了为什么本地运行前必须先加载 demo data:查询语句是围绕该数据模型编写的。
配置第二步:环境变量设置
准备好 Prepr 环境后,需要在项目中定义环境变量:
2.1 复制并重命名环境示例文件:
cp .env.local.example .env.local
.env.local 文件会被 Git 忽略,其中的密钥不会进入版本库。
2.2 在 Prepr 环境中进入 Settings > Access Tokens,可以看到自动生成的访问令牌。将 GraphQL Production 令牌复制为 .env.local 中的 PREPRIO_PRODUCTION_TOKEN,将 GraphQL Preview 令牌复制为 PREPRIO_PREVIEW_TOKEN。也可以通过 Add access token 自行创建令牌,此时需要为令牌选择正确的 GraphQL 权限。
2.3 为 PREPRIO_PREVIEW_SECRET 设置一个不含空格的自定义值(例如 UUID)。该值用于启用 Next.js 的预览模式(Draft Mode)。
完成后的 .env.local 应如下所示:
PREPRIO_API=https://graphql.prepr.io/graphql
PREPRIO_PRODUCTION_TOKEN='your Production access token'
PREPRIO_PREVIEW_TOKEN='your Preview access token'
PREPRIO_PREVIEW_SECRET='your secret id'
从源码可以确认这四个变量的用途与分工:
| 变量 | 用途 | 消费位置 |
|---|---|---|
PREPRIO_API |
Prepr GraphQL 端点地址 | lib/api.js 中 fetchAPI 的请求 URL |
PREPRIO_PRODUCTION_TOKEN |
读取已发布内容的 Bearer 令牌 | fetchAPI 中非预览场景的 Authorization 头 |
PREPRIO_PREVIEW_TOKEN |
读取含草稿的预览内容 | fetchAPI 中 preview: true 时的 Authorization 头 |
PREPRIO_PREVIEW_SECRET |
进入预览模式的校验密钥 | pages/api/preview.js 中的 req.query.secret 比对 |
这种“双令牌”设计的关键点在于:生产访问令牌只能取到 Review/Published 状态的内容,而 Preview 令牌能取到保存为 Review status 的草稿内容——这正是预览模式能够展示未发布文章的能力来源。
数据获取层:lib/api.js 的 GraphQL 封装
lib/api.js 是整个示例的数据核心,值得逐函数剖析:
fetchAPI(query, { variables, preview }):统一的 GraphQL 请求入口。它以 POST 方式请求 process.env.PREPRIO_API,并在 Authorization 头中按 preview 参数动态选择 PREPRIO_PREVIEW_TOKEN 或 PREPRIO_PRODUCTION_TOKEN。所有上层函数都通过它访问 Prepr,令牌切换逻辑集中在一处。
getAllPostsForHome(preview):查询全部 Articles(按 publish_on_DESC 排序),返回字段包括 _id、_slug、_publish_on、title、excerpt、content(Text 联合类型的 html/text 子字段)、authors(full_name 与 profile_pic.url)以及 cover(url(preset: "square") 图片预设)。首页 pages/index.js 的 getStaticProps 调用它,并将返回列表的第一篇作为 heroPost、其余传入 MoreStories 组件。
getPostAndMorePosts(slug, preview):单篇文章页的数据来源。一次查询同时返回 Article(slug: $slug) 详情与一个别名为 MoreArticles 的相关文章列表(limit: 3)。注意其正文查询使用了 GraphQL 联合类型分支:... on Text 取纯文本内容,... on Assets 取 items { url }——这说明 Prepr 的 content 字段可以是文本或媒体资源列表两种形态。函数末尾还做了两层客户端裁剪:先 filter((item) => item._slug !== slug) 排除当前文章本身,再 slice(0, 2) 只保留两篇“更多文章”。
getPreviewPostBySlug(slug):预览模式专用的轻量查询,仅请求 Article(slug: $slug) { _slug } 并强制 preview: true。它被 pages/api/preview.js 用来校验传入的 slug 在 Prepr 中真实存在。
静态生成流程:getStaticProps 与 getStaticPaths
首页 pages/index.js:
export async function getStaticProps({ preview = null }) {
const allPosts = (await getAllPostsForHome(preview)) || [];
return { props: { allPosts, preview } };
}
首页是固定路径,无需 getStaticPaths;preview 参数由 Next.js 在 Draft Mode 下自动注入,从而让首页在预览时切换为 Preview 令牌拉取数据。
文章详情页 pages/posts/[slug].js:
export async function getStaticProps({ params, preview = false }) {
const { post, morePosts } = await getPostAndMorePosts(params.slug, preview);
return { props: { preview, post, morePosts } };
}
export async function getStaticPaths() {
const allPosts = await getAllPostsWithSlug();
const paths = allPosts.map((post) => ({ params: { slug: post._slug } }));
return { paths, fallback: true };
}
这里有两个值得注意的实现细节:
getStaticPaths调用 lib/api.js 中的getAllPostsWithSlug(),该查询固定以preview: true发起——即构建阶段把 Prepr 中所有 slug(含草稿)都登记为合法路径,保证 CMS 中新建的文章无需重新构建即可在预览中被访问;fallback: true表示对于构建时未预生成的 slug,先展示占位内容、再在请求时按需生成。相应地,组件内部使用useRouter()的router.isFallback判断:处于 fallback 期间渲染<PostTitle>Loading…</PostTitle>占位标题,而当!router.isFallback && !post?._slug时渲染 404 错误页(通过next/error的ErrorPage statusCode={404})。
组件层还处理了 Prepr 返回的复合结构:pages/posts/[slug].js 中 post.cover[0].url 说明 cover 是数组,post.authors[0] 说明一篇可有多个作者;正文由 components/post-body.js 根据 content 的 __typename 渲染 Text 或 Assets 两种形态。
配置第三步:安装依赖并本地运行
3.1 安装 package.json 中列出的依赖:
npm install
yarn install
3.2 运行 package.json 中定义的 dev 脚本("dev": "next"):
npm run dev
yarn dev
完成后,示例博客应运行在 http://localhost:3000 。
配置第四步(可选):验证 Preview 模式
Prepr 的内容工作流支持将文章保存为 Review status(待审状态)。要在网站上预览这类未发布内容:
4.1 在 Prepr 中打开 Article 模型 的某篇内容条目并修改标题(例如在标题前加 [PREVIEW]),然后以 Review status 保存。
4.2 将文章 URL 转换为预览入口格式访问:
http://localhost:3000/api/preview?secret=<PREPRIO_PREVIEW_SECRET>&slug=<SLUG_TO_PREVIEW>
其中:
<PREPRIO_PREVIEW_SECRET>与.env.local中定义的预览密钥一致;<SLUG_TO_PREVIEW>是要预览的内容条目的 slug。
退出预览模式时,需点击页面顶部的 Click here to exit preview mode 链接。
源码层面,该流程由两个 API 路由驱动:
pages/api/preview.js 的处理链为:
if (
req.query.secret !== process.env.PREPRIO_PREVIEW_SECRET ||
!req.query.slug
) {
return res.status(401).json({ message: "Invalid token" });
}
const post = await getPreviewPostBySlug(req.query.slug);
if (!post) {
return res.status(401).json({ message: "Invalid slug" });
}
res.setDraftMode({ enable: true });
res.writeHead(307, { Location: `/posts/${post._slug}` });
res.end();
这里有三个安全细节值得借鉴:
- 双重校验——先比对
secret,再校验slug在 Prepr 中真实存在,两者任一不满足都返回 401,防止预览接口被用于探测任意路径; - 启用 Draft Mode 使用
res.setDraftMode({ enable: true }),由 Next.js 设置专用 cookie,后续请求即进入预览数据链路; - 307 重定向的目标不是用户传入的
req.query.slug,而是回查数据库得到的post._slug——源码注释明确指出这是为了避免开放重定向(open redirect)漏洞。
退出入口 pages/api/exit-preview.js 则是对称操作:res.setDraftMode({ enable: false }) 移除 cookie,再 307 重定向回首页。页面顶部那条提示横幅来自 components/alert.js:当 preview 为真时显示预览状态与退出链接(指向 /api/exit-preview),否则显示示例源码链接。
配置第五步:部署
要让站点上线,可以将项目推送到 GitHub/GitLab/Bitbucket 后导入 Vercel 部署,或使用官方模板一键部署。README 强调了一点:在导入项目到 Vercel 时,务必点击 Environment Variables 设置,使其与 .env.local 文件中的值一一对应——即部署环境同样需要 PREPRIO_API、PREPRIO_PRODUCTION_TOKEN、PREPRIO_PREVIEW_TOKEN、PREPRIO_PREVIEW_SECRET 四个变量。
从 package.json 看,构建命令为 next build,静态生成的全部文章会在构建期完成抓取与渲染;而 getStaticPaths 的 fallback: true 保证了 CMS 后续新增文章在预览模式下立即可访问。
进阶方向与相关示例
README 建议将 Prepr 文档作为进阶参考,方向包括 A/B 测试、个性化(Personalization)与推荐(Recommendations)。
本仓库中同类 CMS 对接示例可横向对比,它们共享同一套博客骨架与 Draft Mode 预览模式,差异主要在 lib/api.js 的数据获取实现:
- AgilityCMS、Builder.io、ButterCMS、Contentful、Cosmic、DatoCMS、DotCMS、Drupal、Enterspeed、Ghost、GraphCMS、Kontent.ai、MakeSwift、Payload、Plasmic、Prismic、Sanity、Sitecore XM Cloud、Sitefinity、Storyblok、TakeShape、Tina、Umbraco、Umbraco heartcore、Webiny、WordPress;
- 通用博客模板:Blog Starter。
此外,本示例的 CMS 元信息(名称 Prepr、示例路径 cms-prepr)集中定义在 lib/constants.js 中,供页面标题等元数据组件复用;构建侧配置见 next.config.js、tailwind.config.js 与 postcss.config.js。
小结
cms-prepr 示例以最小依赖完整呈现了 Next.js 静态生成 + 头端 CMS 的生产级组合:fetchAPI 的双令牌封装决定了生产/预览两条数据链路;getStaticPaths 用 Preview 令牌登记全部 slug 并配合 fallback: true 兼顾构建期与运行期;/api/preview 路由以密钥校验 + slug 存在性校验 + 安全重定向三道防线守护 Draft Mode 入口。掌握这套模式后,将其迁移到任何其他 GraphQL 或 REST 类型的 CMS 都只需替换 lib/api.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 StartedRust0626
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