首页
/ Next.js 静态博客实战:以 cms-prepr 示例对接 Prepr 头端 CMS 实现静态生成与预览模式

Next.js 静态博客实战:以 cms-prepr 示例对接 Prepr 头端 CMS 实现静态生成与预览模式

2026-09-06 16:00:15作者:仰钰奇

本篇指南基于 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.jspages/api/exit-preview.js:预览模式的进入/退出 API 路由;
  • lib/api.js:封装 Prepr GraphQL API 的数据获取层;
  • components/:布局、文章头部、正文、更多文章列表等 React 组件;
  • 依赖极简,见 package.json:仅 classnamesdate-fnsnextreactreact-dom,样式由 Tailwind CSS(devDependencies 中的 tailwindcsspostcssautoprefixer)驱动。

从源码结构看,该示例完全采用 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 环境并加载演示数据

  1. 注册一个 Prepr 账号(通过 README 中的官方注册入口)。
  2. 在 Prepr 控制台中新建一个 environment(环境)。
  3. 创建环境时选择 Load demo data(加载演示数据)。这样 Prepr 会自动上传示例用的 models(如 Article、Author)、内容条目及其他数据,使本地示例博客开箱即用。

演示数据中定义了博客用到的模型结构:Article(含 _slug_publish_ontitleexcerptcontentauthorscover 等字段)以及 Author(含 full_nameprofile_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.3PREPRIO_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.jsfetchAPI 的请求 URL
PREPRIO_PRODUCTION_TOKEN 读取已发布内容的 Bearer 令牌 fetchAPI 中非预览场景的 Authorization
PREPRIO_PREVIEW_TOKEN 读取含草稿的预览内容 fetchAPIpreview: 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_TOKENPREPRIO_PRODUCTION_TOKEN。所有上层函数都通过它访问 Prepr,令牌切换逻辑集中在一处。

getAllPostsForHome(preview):查询全部 Articles(按 publish_on_DESC 排序),返回字段包括 _id_slug_publish_ontitleexcerptcontent(Text 联合类型的 html/text 子字段)、authorsfull_nameprofile_pic.url)以及 coverurl(preset: "square") 图片预设)。首页 pages/index.jsgetStaticProps 调用它,并将返回列表的第一篇作为 heroPost、其余传入 MoreStories 组件。

getPostAndMorePosts(slug, preview):单篇文章页的数据来源。一次查询同时返回 Article(slug: $slug) 详情与一个别名为 MoreArticles 的相关文章列表(limit: 3)。注意其正文查询使用了 GraphQL 联合类型分支:... on Text 取纯文本内容,... on Assetsitems { 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 } };
}

首页是固定路径,无需 getStaticPathspreview 参数由 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 };
}

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

  1. getStaticPaths 调用 lib/api.js 中的 getAllPostsWithSlug(),该查询固定以 preview: true 发起——即构建阶段把 Prepr 中所有 slug(含草稿)都登记为合法路径,保证 CMS 中新建的文章无需重新构建即可在预览中被访问;
  2. fallback: true 表示对于构建时未预生成的 slug,先展示占位内容、再在请求时按需生成。相应地,组件内部使用 useRouter()router.isFallback 判断:处于 fallback 期间渲染 <PostTitle>Loading…</PostTitle> 占位标题,而当 !router.isFallback && !post?._slug 时渲染 404 错误页(通过 next/errorErrorPage statusCode={404})。

组件层还处理了 Prepr 返回的复合结构:pages/posts/[slug].jspost.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();

这里有三个安全细节值得借鉴:

  1. 双重校验——先比对 secret,再校验 slug 在 Prepr 中真实存在,两者任一不满足都返回 401,防止预览接口被用于探测任意路径;
  2. 启用 Draft Mode 使用 res.setDraftMode({ enable: true }),由 Next.js 设置专用 cookie,后续请求即进入预览数据链路;
  3. 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_APIPREPRIO_PRODUCTION_TOKENPREPRIO_PREVIEW_TOKENPREPRIO_PREVIEW_SECRET 四个变量。

package.json 看,构建命令为 next build,静态生成的全部文章会在构建期完成抓取与渲染;而 getStaticPathsfallback: true 保证了 CMS 后续新增文章在预览模式下立即可访问。

进阶方向与相关示例

README 建议将 Prepr 文档作为进阶参考,方向包括 A/B 测试、个性化(Personalization)与推荐(Recommendations)。

本仓库中同类 CMS 对接示例可横向对比,它们共享同一套博客骨架与 Draft Mode 预览模式,差异主要在 lib/api.js 的数据获取实现:

此外,本示例的 CMS 元信息(名称 Prepr、示例路径 cms-prepr)集中定义在 lib/constants.js 中,供页面标题等元数据组件复用;构建侧配置见 next.config.jstailwind.config.jspostcss.config.js

小结

cms-prepr 示例以最小依赖完整呈现了 Next.js 静态生成 + 头端 CMS 的生产级组合:fetchAPI 的双令牌封装决定了生产/预览两条数据链路;getStaticPaths 用 Preview 令牌登记全部 slug 并配合 fallback: true 兼顾构建期与运行期;/api/preview 路由以密钥校验 + slug 存在性校验 + 安全重定向三道防线守护 Draft Mode 入口。掌握这套模式后,将其迁移到任何其他 GraphQL 或 REST 类型的 CMS 都只需替换 lib/api.js 中的查询实现,页面层与预览模式代码基本可以原样复用。

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