news 2026/9/7 19:44:05

Next.js 结合 ButterCMS 构建静态生成内容站的完整实战:cms-buttercms 示例解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Next.js 结合 ButterCMS 构建静态生成内容站的完整实战:cms-buttercms 示例解析

Next.js 结合 ButterCMS 构建静态生成内容站的完整实战:cms-buttercms 示例解析

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

本文以 Next.js 官方仓库中的 cms-buttercms 示例 为主线,讲解如何用 ButterCMS 作为数据源搭建一个包含落地页、博客、分类/标签页、站内搜索的完整网站,覆盖安装、环境变量配置、预览模式与 Vercel 部署全流程;读完后你将掌握 Next.js Pages Router 的getStaticPaths/getStaticProps/revalidate(ISR)在真实 CMS 集成中的落地方式,并能对照 API 封装层 与 路由配置 理解每个关键参数的作用。

示例定位:一个开箱即用的 CMS 驱动站点

该示例的 README 将其定义为"fully-functional, drop-in proof-of-concept"——一个与 ButterCMS 账号完全打通的 Next.js 起步项目。它包含以下能力:

  • 动态内容全集成:主菜单、落地页、博客文章、分类、标签,全部来自 ButterCMS 账号中的动态数据;
  • 静态生成(Static Generation)为核心渲染策略:页面在构建期/请求期由 CMS 数据预渲染;
  • 内置搜索功能:博客支持按关键词搜索、按分类与标签过滤;
  • 自定义主题:基于 Bootstrap 的完整站点主题,落地页采用"API 驱动的分节组件"模式;
  • 示例内容自动创建:注册 ButterCMS 免费试用后,账号后台会自动生成本示例所需的全部样例内容(落地页、菜单、博客文章等),无需手动造数据。

从源码结构看,示例采用 Pages Router(pages/目录 +getInitialProps/getStaticProps/getServerSideProps),package.json 中声明next: ^12.1.0buttercms: ^1.2.8,即该集成模式验证于 Next.js 12 时代的 Pages Router,其静态生成思想在 App Router 中同样适用。

目录结构总览

examples/cms-buttercms/ ├── components/ │ ├── blog/ # 博客组件:文章列表、文章卡片、分类侧栏、搜索框 │ ├── landing-page-sections/ # 落地页分节组件:hero、features、testimonials 等 │ ├── main-menu/ # 主菜单 │ ├── _app 相关布局组件 # 页头/页脚/加载器/回顶按钮等 ├── lib/api.js # ButterCMS 客户端与全部数据获取函数(核心) ├── pages/ │ ├── blog.js # 博客列表(静态生成) │ ├── blog/[slug].js # 单篇文章(静态生成 + fallback) │ ├── blog/category/[slug].js# 分类页(ISR,revalidate: 1) │ ├── blog/tag/[slug].js # 标签页 │ ├── blog/search.js # 搜索结果页(SSR) │ ├── landing-page/[slug].js # 落地页(静态生成 + fallback) │ ├── _app.js / _document.js │ └── missing-token.js # 未配置 API Key 时的提示页 ├── .env.local.example ├── app.json # Vercel App 清单(一键部署元数据) └── next.config.js

一、获取与安装示例

README 提供了两条安装路径,对应"从零克隆"与"脚手架引导"两种工作流。

方式一:克隆仓库后用 npm / Yarn 安装

git clone https://github.com/ButterCMS/nextjs-starter-buttercms.git cd nextjs-starter-buttercms npm install # 或 yarn install

在本地复现时,等价做法是直接查看本仓库中的 examples/cms-buttercms 目录,其package.json即为完整依赖清单(buttercmscamelcase-keysdate-fnsbootstraptiny-slidersharp等)。

方式二:通过 Create-Next-App 引导

使用npx(npm)、yarnpnpm执行 create-next-app 并指定示例名,即可一键拉取该示例模板:

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

其中--example cms-buttercms对应本仓库的示例目录名,cms-buttercms-app为新建的目标项目目录名。

二、配置:API Key、环境变量与启动

Step 1. 获取 ButterCMS API Token

在 ButterCMS 注册账号后,后台会直接展示一个免费 API Token,后续所有数据请求都依赖它。

Step 2. 设置环境变量

本示例通过 .env.local.example 给出环境变量模板,其内容只有一行:

NEXT_PUBLIC_BUTTER_CMS_API_KEY=your_auth_token

按 README 的操作步骤:

cp .env.local.example .env.local

然后在.env.local中设置各变量:

  • NEXT_PUBLIC_BUTTER_CMS_API_KEY:填入你的 ButterCMS API Key。变量带NEXT_PUBLIC_前缀,意味着客户端代码也能读到(本示例中它同时用于服务端渲染守卫,见下文missing-token重定向)。
  • PREVIEW(可选):控制是否允许 CMS 中的草稿内容被返回。该变量在 lib/api.js 中的解析逻辑值得逐行看清:
const previewSetting = process.env.PREVIEW; // make preview mode by default const preview = previewSetting === "true" || previewSetting === undefined ? 1 : 0; try { butter = Butter(process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY, preview); } catch (e) { console.log(e); }

从源码可以看出:预览(草稿可见)模式默认开启——只有显式设置PREVIEW=falsepreview才会为0PREVIEW=true或未设置都视为开启。第二个参数preview会透传给 Butter SDK 客户端,决定 API 是否返回未发布的草稿内容。

Step 3. 开发模式运行

注册 ButterCMS 后,账号内已自动创建本示例所需的全部示例内容。运行:

npm install npm run dev # 或 yarn install yarn dev

启动后访问http://localhost:3000,即可看到完整的起步站点:基于 API 组件的落地页、基于 API 的主菜单以及博客。

未配置 Token 时的兜底行为

值得注意的细节是,示例做了"缺 Key 不崩"的优雅降级。next.config.js 中的redirects()会根据 Token 是否存在动态决定重定向规则:

redirects() { const sourcesRequiringAuthToken = [ "/", "/landing-page/:slug*", "/blog/:path*", ]; return process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY ? [ { source: "/missing-token", destination: "/", permanent: false, }, ] : sourcesRequiringAuthToken.map((source) => ({ source: source, destination: "/missing-token", permanent: false, })); },
  • 有 Token:仅保留/missing-token → /的回退重定向;
  • 无 Token:首页、所有落地页与博客路径全部 302 到/missing-token,由 pages/missing-token.js 渲染的提示页承接。

同样地,pages/_app.js 的MyApp.getInitialProps中只在authToken存在时才调用getMainMenu()拉取菜单,否则传入空数组[],保证无 Key 时应用仍可渲染布局。

三、API 封装层:lib/api.js 的核心函数

整个示例与 ButterCMS 的通信被集中封装在 examples/cms-buttercms/lib/api.js 中,这是理解数据流的枢纽。除客户端初始化外,关键函数签名如下:

函数作用关键参数与行为
getLandingPage(slug)按 slug 获取单张落地页调用butter.page.retrieve("landing-page", slug)
getLandingPages()获取全部落地页内部以defaultPageSize = 100分页,while 循环翻页直到next_page为空
getPostsData({ page, pageSize, tag, category })文章列表默认pageSize = 10defaultPostCount),支持tag_slug/category_slug过滤
getPost(slug)获取单篇文章调用butter.post.retrieve(slug)
getMainMenu()获取主菜单butter.content.retrieve(["navigation_menu"])中取名为"Main menu"menu_items
getCategories()/getTags()分类/标签列表分别调用butter.category.list()/butter.tag.list()
searchPosts({ query })关键词搜索调用butter.post.search(query)

落地页的分页拉取是一个典型实现,展示了如何处理 CMS API 的分页元数据:

export async function getLandingPages() { let paginatedLandingPages = []; let currentPage = 1; while (!!currentPage) { const landingPagesData = await getLandingPagesData(currentPage); paginatedLandingPages.push(...landingPagesData.pages); currentPage = landingPagesData.nextPage; } return paginatedLandingPages; }

其中getLandingPagesData返回pages / prevPage / nextPage三元组,nextPage来自响应meta.next_page,为null时循环终止。所有函数统一采用try/catch抛出e.response.data.detail,把 CMS 侧错误以可读形式上抛,由页面层的catch决定降级策略(返回notFound或空数组)。

四、静态生成在各页面的具体落地

README 明确说明该示例展示的是 Next.js 的 Static Generation 能力。对照源码,五种页面使用了三种数据获取策略:

1. 博客列表(纯静态):pages/blog.js

pages/blog.js 使用getStaticProps拉取文章与分类,并在 API 失败时降级为空列表而非报错:

export async function getStaticProps() { const butterToken = process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY; if (butterToken) { try { const blogPosts = (await getPostsData()).posts; const categories = await getCategories(); return { props: { posts: camelcaseKeys(blogPosts), categories } }; } catch (e) { console.log("Could not get posts", e); return { props: { posts: [], categories: [] }, }; } } return { props: { posts: [], categories: [] } }; }

注意两处工程细节:camelcaseKeys把 Butter API 返回的 snake_case 字段(如category_slug)统一转成 camelCase,使 React 侧属性访问更符合习惯;if (butterToken)守卫与前述重定向策略呼应。

2. 动态路径 + fallback:落地页与文章详情页

pages/landing-page/[slug].js 的getStaticPaths在构建期列出所有落地页 slug:

export async function getStaticPaths() { const butterToken = process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY; if (butterToken) { try { const landingPages = await getLandingPages(); return { paths: landingPages.map((page) => `/landing-page/${page.slug}`), fallback: true, }; } catch (e) { console.error("Couldn't load content for Landing pages.", e); } return { paths: [], fallback: false }; } }

fallback: true的意义在于:构建期未预渲染的 slug 不会被拒绝,而是先展示router.isFallback时的<Preloader />加载态,再在服务端补渲染getStaticProps。对应的getStaticProps同时拉取页面数据与最新 2 篇文章(getPostsData({ page: 1, pageSize: 2 })),供落地页尾部嵌入博客区块;取数失败时返回notFound: true,页面侧配合router.isFallback!page两个分支分别渲染加载器与ErrorPage statusCode={404}

pages/blog/[slug].js 遵循完全相同的模式,并额外展示了 SEO 元数据的写法:从文章字段生成<title>meta description以及 Open Graph / Twitter Card 标签;正文通过dangerouslySetInnerHTML注入 CMS 返回的 HTML,配图则使用next/imagelayout="fill"+objectFit="cover")。

3. ISR 增量再验证:分类页

pages/blog/category/[slug].js 在getStaticProps中返回了revalidate: 1

return { props: { posts: camelcaseKeys(blogPosts), categories, slug }, revalidate: 1, };

这意味着分类页在首次请求生成后,最多 1 秒就会触发后台再验证——CMS 中该分类的新文章几乎实时出现在已缓存的静态页面上。getStaticPaths则遍历getCategories()生成/blog/category/:slug路径集合,同样使用fallback: true

4. 搜索页(SSR):pages/blog/search.js

pages/blog/search.js 是唯一采用服务端逐请求渲染的页面,因为搜索词无法在构建期枚举:

export async function getServerSideProps({ query: { query } }) { const blogPosts = await searchPosts({ query }); const categories = await getCategories(); return { props: { posts: camelcaseKeys(blogPosts), categories, query }, }; }

这与 README 中"already-implemented search functionality"的描述对应:搜索走 ButterCMS 的post.search接口,每次请求实时执行。

5. 全局菜单:_app.js 的 getInitialProps

主菜单不是页面级数据,而是全局数据,因此放在 pages/_app.js 的getInitialProps中获取并注入到HeaderSection/FooterSection

MyApp.getInitialProps = async (appContext) => { const appProps = await App.getInitialProps(appContext); const authToken = process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY; let mainMenu = []; if (authToken) { try { mainMenu = await getMainMenu(); } catch (e) { console.error("Couldn't load main menu links.", e); } } return { ...appProps, mainMenu }; };

同文件中还通过Router.eventsrouteChangeStart/Complete/Error事件驱动全局<Preloader>,实现路由切换时的加载动画。

五、next.config.js:路由重写与远程图片域

examples/cms-buttercms/next.config.js 除前述redirects外,还包含两类配置:

首页重写——把根路径固定指向 ButterCMS 中的样例落地页:

async rewrites() { return [ { source: "/", destination: "/landing-page/landing-page-with-components", }, ]; },

远程图片白名单——next/image默认禁止加载任意外域图片,而 Butter 的媒体托管在cdn.buttercms.com,因此需要显式声明:

images: { remotePatterns: [ { protocol: "https", hostname: "cdn.buttercms.com", port: "", pathname: "/my-account/**", }, ], },

/my-account/**的 pathname 限定说明只放行当前账号媒体空间下的资源。

六、落地页的"分节组件"模式

落地页是 CMS 驱动的典型场景:营销页的区块顺序与类型都由内容方在后台配置。pages/landing-page/[slug].js 中按 CMS 返回的page.fields.body数组逐节渲染:

{page.fields.body.map(({ type, fields: sectionData }, index) => ( <LandingPageSection key={index} type={type} sectionData={sectionData} /> ))}

components/landing-page-sections/landing-page-section.js 负责把type映射到具体组件,且每个组件都用next/dynamic动态导入、失败时回退到MissingSection

const sectionsComponentPaths = () => ({ hero: dynamic(() => import(".../hero").catch(() => () => MissingSection), { loading: Preloader }), two_column_with_image: dynamic(() => import(".../two-column-with-image").catch(() => () => MissingSection), { loading: Preloader }), features: dynamic(() => import(".../features").catch(() => () => MissingSection), { loading: Preloader }), testimonials: dynamic(() => import(".../testimonials").catch(() => () => MissingSection), { loading: Preloader }), }); const SectionComponent = sectionsComponentPaths()[type] || MissingSection;

从源码结构看,这套"类型 → 动态组件 + 缺失兜底"的映射表使 CMS 端新增/删减区块类型不会直接击穿前端:未知type渲染占位组件而非报错,未匹配到的区块数据走MissingSection

七、预览模式(Preview Mode)实战

README 的 Step 4 描述了本示例最有实用价值的功能之一:草稿内容预览。行为与开启方式如下:

  1. 在 ButterCMS 后台新建一篇博客,标题设为Draft Post Test,填入任意正文与摘要;
  2. 关键:不要点击 Publish,而是点击Save Draft
  3. 先在.env.local中设置PREVIEW=false并重启本地开发服务器,访问博客列表(如http://localhost:3000/#blog)——此时看不到这篇草稿,因为其状态尚未变为published
  4. 随后从.env.local删除PREVIEW=false(即回到默认开启状态),刷新后新草稿文章即出现在列表中。

结合 lib/api.js 第 5–11 行的代码可以确认其机制:预览开关直接作为Butter(apiKey, preview)的第二个参数传给 SDK,由 API 侧决定草稿可见性;由于默认值为"开启",本地开发时草稿默认可见,而生产部署若不希望暴露草稿,应显式设置PREVIEW=false

提示:README 还建议在 ButterCMS 后台为部署在 Vercel 上的页面配置 Preview URL,即可在 Butter 账号内对线上站点做实时内容预览(该功能属于 ButterCMS 平台侧能力,需在外部后台配置)。

八、部署到 Vercel

README 的 Step 5 给出两条部署路径:

从官方模板一键部署

README 提供了 Vercel "Deploy" 按钮链接,点击后会在你的 Git 账号中复制一份 starter 项目并立即部署,同时引导你填入NEXT_PUBLIC_BUTTER_CMS_API_KEY环境变量,形成完整的内容工作流。

从仓库文件看,一键部署所需的元数据就写在 app.json 中:

{ "name": "ButterCMS Next.js Starter Project", "description": "Drop-in proof-of-concept NextJs app, fully integrated with your ButterCMS account.", "repository": "https://github.com/ButterCMS/nextjs-starter-buttercms", "keywords": ["Next.js", "buttercms", "cms", "blog"], "buildpacks": [{ "url": "heroku/nodejs" }], "env": { "NEXT_PUBLIC_BUTTER_CMS_API_KEY": { "description": "The API token of your ButterCMS account", "value": "" } } }

env段声明了部署时必须提供的NEXT_PUBLIC_BUTTER_CMS_API_KEY(值为空,需部署者填入),这是部署清单与环境变量的直接对应关系。

部署自己的本地项目

对已做修改的本地项目,将其推送到 GitHub / GitLab / Bitbucket 并导入 Vercel。重要:导入时务必在 Vercel 控制台的Environment Variables中配置与本地.env.local一致的变量(至少包括NEXT_PUBLIC_BUTTER_CMS_API_KEY,以及按需的PREVIEW),否则线上会触发上文所述的/missing-token重定向,所有页面都落到提示页。

小结:该示例可借鉴的工程模式

以 examples/cms-buttercms 为样本,CMS 驱动型 Next.js 站点可提炼出以下可复用模式:

  • 单一 API 封装层:所有 CMS 调用集中在lib/api.js,页面层只消费函数而非 SDK,便于替换数据源;
  • 静态生成为主、按需降级:列表/详情用getStaticPaths(fallback: true)+getStaticProps,高频更新的分类页用revalidate: 1的 ISR,不可枚举的搜索用getServerSideProps
  • 缺凭据友好:环境变量守卫 + 条件redirects+missing-token提示页,让项目在无 Key 状态下可运行、可诊断;
  • CMS 内容容错:API 失败时页面降级为空数据或 404,未知落地页区块类型回退到占位组件;
  • 预览模式默认开启PREVIEW未设置即视为草稿可见,降低本地调试成本。

除本示例外,同一目录下的其他 CMS 集成可作横向参考,例如 cms-agilitycms、cms-contentful、cms-datocms、cms-ghost、cms-prismic、cms-sanity、cms-storyblok、cms-wordpress 以及 blog-starter。

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 19:40:31

微信小程序自助洗衣房预约系统:从并发锁到支付回调的完整实现

1. 项目概述1.1 这个预约系统到底解决什么问题先说一个我观察到的现象。大学宿舍楼、公寓楼里的自助洗衣房&#xff0c;通常就几台洗衣机&#xff0c;高峰时段排队是家常便饭。我见过最夸张的情况是晚上九点半&#xff0c;洗衣房里七八个学生抱着盆子围着两台洗衣机&#xff0c…

作者头像 李华
网站建设 2026/9/7 19:40:28

Qt5 USB设备检测实战:Linux udev与Windows通知机制解析

简介&#xff1a;需要为Qt应用加入USB设备热插拔检测的开发者&#xff0c;可直接使用这份基于wang-bin-qdevicewatcher、并已适配QT5环境的项目包。资源面向有一定Qt基础、正在做设备管理或硬件交互的开发者&#xff0c;解决跨平台实时监控USB设备插拔事件的问题。包内共72个文…

作者头像 李华
网站建设 2026/9/7 19:38:39

基于SpringBoot的城市美食排行榜网站的设计与实现源码+文档

温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台…

作者头像 李华
网站建设 2026/9/7 19:38:02

在很多人生的十字路口,处于35岁左右的女性朋友往往会遇到一个职业的分水岭:有的人面临发展瓶颈需要复合能力升级,有的人因家庭原因短暂停职后希望再就业,还有的人想彻底转换赛道寻找更有长期发展空间的工作。

当您在搜索“35岁女想考个实用的证”时&#xff0c;说明您已经意识到了提升自我的重要性。但在当下的求职现状中&#xff0c;证书的种类繁多&#xff0c;培训机构的宣传口径又五花八门。很多女性朋友的核心痛点在于&#xff1a;花了大量时间和金钱考下来的证&#xff0c;到了面…

作者头像 李华
网站建设 2026/9/7 19:35:26

伯努利试验与二项分布:从经典真题到工程实践

伯努利试验与二项分布&#xff0c;这两个概念在概率论里属于“看着简单、用着容易翻车”的那一类。最近刚好在一套考研复习题里看到一道关于重复独立试验的经典真题&#xff0c;评论区不少人因为“恰好命中”和“至少命中一次”之间的转换出了问题。我干脆把这类题背后的核心逻…

作者头像 李华
网站建设 2026/9/7 19:34:19

VSCode 配置 MATLAB 开发环境:从安装到调试全指南

开头 前阵子帮一个师弟配 MATLAB 开发环境&#xff0c;他习惯用 VSCode 写代码&#xff0c;不想每次跑个脚本都切回 MATLAB 桌面。折腾了一会儿把环境搭好后&#xff0c;我觉得这套流程值得整理出来。网上关于“VSCode 中使用 MATLAB”的教程不少&#xff0c;但要么是零散记一笔…

作者头像 李华