Next.js 结合 Agility CMS 构建 SSG 博客:Page Management 模型与 Preview Mode 实战指南
2026/9/7 19:00:17 网站建设 项目流程

Next.js 结合 Agility CMS 构建 SSG 博客:Page Management 模型与 Preview Mode 实战指南

【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js

本文基于 Next.js 官方仓库中的examples/cms-agilitycms示例,完整讲解如何用 Agility CMS 的 Page Management 能力驱动一个静态生成(Static Generation)博客:从 Content Definition、Module、Page Template 的 CMS 侧建模,到 Next.js 侧基于 sitemap 的 catch-all 路由渲染,再到 Preview Mode(草稿模式)的密钥校验实现,最终给出可复制的 18 步落地流程和源码级原理剖析。

一、这个示例与常规 CMS 博客的本质区别

绝大多数 CMS 博客示例(如 WordPress、Ghost、Contentful 等)只解决"内容从哪来"的问题:CMS 提供文章数据,前端页面结构写死在代码里。而 cms-agilitycms 示例 采用 Agility CMS 的Page Management特性,让 CMS 最终决定网站有哪些页面以及每个页面上放什么内容

其核心设计原则是:Editors(编辑)对页面拥有完全控制权,无需接触代码,而开发者只负责构建供编辑组合页面的 UI 组件。对应到技术模型上,就是示例文档中强调的一句关键映射:

Agility CMS 中的ModulesPage Templates,分别对应你网站中的React Components

整个网站由**一个 Page Template(One Column Template)+ 一组 Modules(IntroHero PostMore StoriesPost Details)**构成。编辑在 CMS 里增删页面、往页面的内容区里拖模块,Next.js 端在构建时从 CMS 拉取 sitemap 和页面数据,把"模块名"动态解析成对应的 React 组件来渲染——页面结构完全由 CMS 侧数据驱动。

这种模式下,新增一个落地页、调整某页模块顺序,都不需要开发者改代码重新发布。

二、快速开始:使用 create-next-app 脚手架

通过 npm、Yarn 或 pnpm 中的任意一种执行create-next-app并指定cms-agilitycms示例,即可初始化项目:

npx create-next-app --example cms-agilitycms cms-agilitycms-app
yarn create next-app --example cms-agilitycms cms-agilitycms-app
pnpm create next-app --example cms-agilitycms cms-agilitycms-app

生成的项目中与本文原理剖析强相关的文件布局如下(以仓库内 examples/cms-agilitycms 目录为准):

  • pages/[...slug].tsx:唯一的 catch-all 页面路由,承载所有 CMS 页面
  • pages/api/preview.ts 与 pages/api/exit-preview.ts:Preview Mode 的进入/退出 API 路由
  • lib/api.ts:CMS 客户端封装、sitemap 拉取、页面 props 组装
  • lib/preview.ts:预览密钥生成与校验
  • lib/components/:Page Template 与 Content Zone 的渲染骨架
  • components/:各 Module 对应的 React 组件(hero-post.tsxintro.tsxmore-stories.tsxpost-details.tsx等)
  • lib/constants.ts:CMS 语言与频道常量(en-us/website

依赖方面,package.json 中数据抓取依赖@agility/content-fetch(Agility 官方 Node SDK),UI 基于 React 18 + Tailwind CSS,next使用latest版本,脚本为标准三件套:dev/build/start

三、CMS 侧建模(Step 1 ~ Step 6):内容定义与 Shared Content

以下全部在 Agility CMS 的 Content Manager 界面中完成。

Step 1:创建账号与项目

在 Agility CMS 注册账号后创建新Project,模板选择Blank (advanced users)得到空白实例。

Step 2:创建 Author 内容定义

  1. 进入Settings>Content Definitions
  2. 点击New创建新的 Content Definition。
  3. TitleAuthor(Reference Name 会自动填充)。
  4. Form Builder页签添加两个字段:
    • Name:Field Label 为 "Name",Field Type 为Text
    • Picture:Field Label 为 "Picture",Field Type 为Image
  5. 点击Save & Close保存。

Step 3:基于 Author 创建List

进入Shared Content,点击+ (New),填写:

  • TypeContent List
  • Content DefinitionAuthor
  • Display NameAuthors(Reference Name 会随之自动填充)

Step 4:创建 Post 内容定义

进入Settings>Content Definitions,点击NewTitlePost,然后在Form Builder中按下表添加字段(其余设置无需改动):

字段Field Type附加配置
TitleText
SlugText
DateDate/Time
AuthorIDNumber勾选Hide field from input form
AuthorLinked ContentContent DefinitionAuthorContent ViewShared ContentShared ContentAuthorsRender AsDropdown ListSave Value To Field设为AuthorID
ExcerptText
ContentHTML
Cover ImageImage

完成后点击Save & Close。注意Author字段是Linked Content类型:编辑在表单里从Authors列表下拉选择作者,而实际保存的值写入隐藏的AuthorID数字字段——这是 Agility CMS 关联内容的数据存储方式。

Step 5:创建Dynamic Page List

进入Shared Content,点击+ (New),填写:

  • TypeDynamic Page List
  • Content DefinitionPost
  • Display NamePosts(Reference Name 自动填充)

Dynamic Page List 与普通 List 的区别在于:它后续可以驱动 Dynamic Page,即"每个 Post 内容项自动变成一个 URL 页面"。

Step 6:填充内容

  • Authors列表:点击+ New创建 1 个作者内容项,文本可填虚拟数据,头像图片可自备,完成后点击SavePublish
  • Posts列表:建议至少创建 2 个文章内容项;Content字段支持写 markdown;Pick 之前创建好的 Author;每项保存后必须点击Publish,否则文章停留在Staging(暂存)状态,Live API 抓取不到。

这一点与后文 Preview Mode 直接相关:Staging 内容只有预览密钥才能读到,普通访问读不到。

四、UI 组件层建模(Step 7 ~ Step 11):Modules 与 Page Template

Module 定义对应"页面里可被编辑拖拽的 React 组件"。以下四个 Module 在 CMS 中只需要定义"名字"和少量字段,真正的渲染逻辑在仓库的 components/ 目录中实现。

Step 7:定义IntroModule

Settings>Module Definitions>New

  • TitleIntro
  • DescriptionDisplays an intro message.

不添加任何字段——因为该模块展示的内容直接硬编码在前端模板里。

Step 8:定义Hero PostModule

  • TitleHero Post
  • DescriptionDisplays the latest Post.

同样不加字段:默认取最新一篇文章,数据关联在 Post 自身。

Step 9:定义More StoriesModule

  • TitleMore Stories
  • DescriptionDisplays a listing of Posts.
  • 添加一个字段:Title,Field Type 为Text(用于自定义列表标题)。

Step 10:定义Post DetailsModule

  • TitlePost Details
  • DescriptionDisplays the details of a Post.
  • 不添加字段,数据关联在 Post 自身。

Step 11:定义One ColumnPage Template

Settings>Page Templates>New

  • NameOne Column Template
  • Digital Channel TypeWebsite
  • Module Zones中点击+ (New)
    • Display Name设为Main Content Zone(Reference Name 自动填充为MainContentZone
    • 点击Save应用该内容区

最后Save & Close。Content Zone 是模板内的"插槽",编辑把 Module 放进这个插槽,前端按区名渲染。

五、页面结构建模(Step 12 ~ Step 14):静态页与动态页

Step 12:创建home页面

Pages中点击页面树的+ (New)新建Page

  • TypePage
  • Page TemplateOne Column Template
  • Menu TextHome(Page Title 与 Page Name 自动填充)

点击Save创建/home页面。然后向Main Content Zone依次添加三个模块:

  1. + (New)Intro,模块上点击Save & Close回到页面;
  2. + (New)Hero PostSave & Close
  3. + (New)More StoriesTitle设为More StoriesSave & Close

最后点击页面上的Publish,发布页面及其全部模块。

Step 13:创建posts文件夹

Pages中选中Website频道,在站点根部的页面树点击+ (New)新建Folder

  • TypeFolder
  • Menu TextPosts(Folder Name 自动填充为posts

Save后务必对文件夹点击Publish——文件夹不发版,其下的动态页路径会不出现在 Live sitemap 中。

Step 14:创建 Dynamic Pageposts-dynamic

选中/posts文件夹,在其下新建Dynamic Page

  • TypeDynamic Page
  • Page TemplateOne Column Template
  • Build Pages FromPosts(即 Step 5 的 Dynamic Page List)
  • Sitemap Labelposts-dynamic
  • Page Path Formula##Slug##
  • Page Title FormulaMenu Text Formula##Title##

公式中的##Slug####Title##是 Agility 的字段插值语法:每新增一个 Post 内容项,CMS 就按该 Post 的Slug字段值自动生成/posts/<slug>页面。随后在Main Content Zone中添加Post DetailsMore Stories(Title 填More Stories)两个模块,Save & Close后点击Publish

至此 CMS 侧共三类 URL 来源:/home静态页、/posts文件夹、/posts/<slug>动态页——这三者都会进入 sitemap,也正是 Next.js 端getStaticPaths的路径来源(见下节)。

六、环境变量配置(Step 15):四个密钥的完整说明

把示例目录中的.env.local.example复制为.env.local(已被 Git 忽略):

cp .env.local.example .env.local

.env.local.example(见 .env.local.example)只声明了四个空变量,实际值来自 CMS 后台两处:

  1. 菜单Getting Started页点击API Keys,弹出Content API Details窗口,点击Show API Key(s)
  2. Settings>Global Security中获取 Security Key。

填入.env.local

AGILITY_CMS_GUID=... AGILITY_CMS_API_FETCH_KEY=... AGILITY_CMS_API_PREVIEW_KEY=... AGILITY_CMS_SECURITY_KEY=...

结合 lib/api.ts 与 lib/preview.ts 的源码,四个变量的实际用途如下:

变量来源源码中的用途
AGILITY_CMS_GUIDContent API Details 的Instance GUIDagility.getApi()创建客户端时的实例标识,live 与 preview 两个客户端共用
AGILITY_CMS_API_FETCH_KEYLive API Key生产态liveClient的 API Key,只能读已发布内容
AGILITY_CMS_API_PREVIEW_KEYPreview API KeypreviewClient的 Key,客户端初始化时带isPreview: true,可读取未发布的 Staging 内容
AGILITY_CMS_SECURITY_KEYSettings>Global Security服务端用于生成/校验agilitypreviewkey,防止任何人伪造预览请求

从源码结构看,lib/api.ts 中同时构建了两个@agility/content-fetch客户端并通过getClient(preview)按 Draft Mode 状态切换:

const liveClient = agility.getApi({ guid: process.env.AGILITY_CMS_GUID, apiKey: process.env.AGILITY_CMS_API_FETCH_KEY, }); const previewClient = agility.getApi({ guid: process.env.AGILITY_CMS_GUID, apiKey: process.env.AGILITY_CMS_API_PREVIEW_KEY, isPreview: true, });

这意味着 SSG 构建时用 live 客户端,getStaticPropspreview === true(即 Preview Mode 请求)时切换为 preview 客户端,从而拉取草稿数据。

七、本地运行(Step 16)与页面路由原理

npm install npm run dev # or yarn install yarn dev

博客运行在http://localhost:3000

为什么一个 catch-all 路由能渲染整个网站

从源码结构看,整个前端只有一个页面文件 pages/[...slug].tsx。它的 SSG 三要素:

getStaticPaths——路径完全来自 CMS sitemap:调用 lib/api.ts 中的getAgilityPaths(),该方法请求getSitemapFlat(频道website、语言en-us),过滤掉isFolder === true的节点,返回形如['/home', '/posts/posts-dynamic/xxx', ...]的路径数组,并返回fallback: true

export async function getStaticPaths() { const paths = await getAgilityPaths(); return { paths: paths, fallback: true, }; }

这正是 Page Management 模型的落点:Next.js 不维护任何硬编码路由列表,URL 空间由 CMS 的页面树决定fallback: true保证编辑在 CMS 里新发布的页面,即使尚未重新构建,也能触发按需生成。

getStaticProps——按路径取页面并解析模块:核心是getAgilityPageProps,其流程为:

  1. params.slug拼出请求路径(空参数时为/,此时取 sitemap 第一个节点);
  2. 拉取sitemapFlat,用路径查到pageID,再调client.getPage取整页数据(含模板名与各 Content Zone 内的模块);
  3. 遍历page.zones,对每个模块调用requireComponentDependencyByName(moduleItem.module)模块名字符串解析为 React 组件
  4. 若组件挂载了getCustomInitialProps静态方法,则在 SSG 阶段就调用它拉取该模块所需的附加数据,保证数据在构建时固化而非客户端请求。

模块名到组件文件的解析规则在 lib/dependencies.ts 中实现:先尝试 kebab-case(如HeroPosthero-post),再尝试 PascalCase;查找顺序为先 components/ 目录(业务组件),找不到再回退 lib/components/。也就是说,CMS 里 Module 的 Reference Name 直接决定 require 哪个.tsx文件——编辑在 CMS 改名模块,前端就必须有同名文件,否则构建时报Could not find a component with the name ...

模板的二次解析:页面拿到pageTemplateName(去掉非字母数字字符后的模板名,如OneColumnTemplate)后,交由 lib/components/page-template.tsx 用同样的requireComponentDependencyByName解析出模板组件;lib/components/one-column-template.tsx 只渲染一个MainContentZone

export default function OneColumnTemplate(props) { return ( <> <ContentZone name="MainContentZone" {...props} /> </> ); }

而 lib/components/content-zone.tsx 读取page.zones[zoneName]中的模块数组,逐个解析组件并把 SSG 阶段准备好的数据作为 props 展开传入:

return modules.map((m, i) => { const AgilityModule = requireComponentDependencyByName(m.moduleName); return <AgilityModule key={i} {...m.item} />; });

带自有数据的模块:以 components/hero-post.tsx 为例,Hero Post模块不接收 CMS 字段,而是挂载静态方法getCustomInitialProps,在构建时调用client.getLatestPost()取最新文章。APIClient(lib/api.ts)封装了三个常用查询:getAllPosts(Content ListpostscontentLinkDepth: 1以便联表拉出作者信息)、getLatestPost(取 1 篇后归一化)、getPostsForMoreStories(取 5 篇并排除当前文章)。lib/normalize.ts 把 Agility 的fields结构统一成{ title, slug, excerpt, date, content, author{name, picture}, coverImage{responsiveImage} }的前端友好形状,并为图片 URL 追加?w=2000&h=1000&q=70这类 CDN 尺寸参数。

八、Preview Mode 实战(Step 18):从编辑点击"Preview"到看到草稿

这是文档最核心也最容易配置出错的部分,下面结合仓库源码完整拆解。

8.1 在 CMS 中配置域名

部署后记录你的站点 URL(形如https://<your-vercel-domain>.vercel.app),然后:

  1. Settings>Domain Configuration
  2. 点击列表中名为Website的频道;
  3. 点击+ (New)添加域名:
    • NameProduction
    • Domain URL:你的生产部署 URL
    • 勾选Preview Domain
    • 点击Save

8.2 制造一条 Staging 内容并进入预览

打开任一 Post,修改标题(例如在标题前加[Staging]),点击Save不要Publish——此时文章处于 Staging 状态。点击该 Post 详情页的Preview按钮,CMS 会带着agilitypreviewkey(以及动态页的contentid)重定向到你的站点;进入Preview Mode后即可导航到目标 Post,看到未发布的标题。页面顶部会出现Click here to exit preview mode退出入口。

若要预览具体的某条 Post(而不是落在/页),需进入Shared ContentPosts列表的Settings页签,将Item Preview Page设为~/posts/posts-dynamicItem Preview Query String Parameter设为contentid

8.3 源码级校验链路

整条 Preview Mode 链路横跨"浏览器端 + 两个 API 路由 + SSG 数据源",全部可在仓库中验证:

第一环:查询参数捕获。lib/use-preview-redirect.ts 是一个客户端 hook,被[...slug].tsx页面调用:一旦 URL 上出现agilitypreviewkey(可能还带contentid),立即跳转/api/preview?slug=...&agilitypreviewkey=...。这解释了为什么 CMS 的 Preview 重定向可以落在任意页面:预览入口被统一收敛到 API 路由。

第二环:密钥生成与比对。lib/preview.ts 的generatePreviewKeyAGILITY_CMS_SECURITY_KEY派生预期密钥——取字符串-1_${SECURITY_KEY}_Preview,逐字符转 UTF-16 字节,做SHA-512哈希后 Base64 编码。Agility CMS 后台用同一算法生成agilitypreviewkey并放进 Preview 链接,因此校验本质是:

const correctPreviewKey = generatePreviewKey(); if (agilityPreviewKey !== correctPreviewKey) { return { error: true, message: `Invalid agilitypreviewkey.` }; }

没有共享密钥就无法伪造合法预览链接。校验还包含validateSlugForPreview:对普通页直接查 sitemap 是否含该 slug;对动态页则用contentID在 sitemap 里反查对应节点并解析出真实路径,查不到即返回 401(错误提示会明确指向"请检查 List Preview Page / Item Preview Page / Item Preview Query String Parameter (contentid) 配置")。

第三环:开启 Draft Mode。pages/api/preview.ts 校验通过后执行 Next.js 的res.setDraftMode({ enable: true })写入草稿 cookie,并 307 重定向到校验解析出的 slug;pages/api/exit-preview.ts 则setDraftMode({ enable: false })删除 cookie 并跳回/。Draft Mode 开启后,Next.js 对该路径跳过静态缓存、以preview: true执行getStaticPropsgetClient(true)切换到 preview 客户端,于是读到 Staging 内容——这就是"编辑改标题不发布、前端就能预览"的完整闭环。

九、部署(Step 17)

  • 部署本地项目:把项目推送到 GitHub/GitLab/Bitbucket 后导入 Vercel。务必在导入时打开Environment Variables,把上面四个变量配置成与.env.local一致,否则 Live/Preview 客户端初始化会因缺失 Guid 或 Key 而失败。
  • 从模板一键部署:也可使用示例 README 提供的 Vercel 模板按钮,部署时按提示填写AGILITY_CMS_GUIDAGILITY_CMS_API_FETCH_KEYAGILITY_CMS_API_PREVIEW_KEYAGILITY_CMS_SECURITY_KEY四个环境变量。

十、小结:编辑与开发者的职责边界

角色在 CMS 中的操作对应前端产物
编辑新建/发布 Page、Dynamic Page Item、Post、调整 Zone 内模块顺序sitemap 变化 → 新 URL 与页面 HTML
开发新增 Module Definition 之外的 React 组件、Page Template 组件components/ 与 lib/components/ 下的.tsx文件
双方环境变量、Domain ConfigurationPreview Mode 可用

这套示例的完整价值在于演示了一个可落地的分工范式:URL 空间、页面结构、模块编排全部上移到 CMS 的 Page Management 层;Next.js 端用"sitemap 驱动getStaticPaths+ 模块名动态解析组件 +getCustomInitialProps构建期取数"三件套承接渲染;Preview Mode 则用共享密钥哈希 + Draft Mode 把 Staging 内容安全地暴露给编辑。相关仓库中同系列的 CMS 示例可参考 cms-wordpress、cms-ghost、cms-contentful、cms-sanity、blog-starter 等,结构上均为"Content Definition/List → SSG 取数 → 页面组件"的模式,区别在于是否引入了本文这种页面级编排能力。

【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询