Next.js × Cosmic 静态博客:从 getStaticPaths 到 Draft Mode 预览的完整实现解析
【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js
本篇基于 Next.js 官方示例仓库中的 cms-cosmic 示例 展开,讲解如何以 Cosmic 作为 Headless CMS 数据源,利用 Next.js 的静态生成(Static Generation)构建一个博客站点。读完本文,你将完整掌握该示例的搭建步骤、三个环境变量与 Cosmic API 的对应关系、getStaticPaths的 fallback 机制、以及从/api/preview到setDraftMode的草稿预览(Preview/Draft Mode)全链路实现原理。
一、示例定位:静态生成博客 + Headless CMS
cms-cosmic 是 Next.js 仓库examples/目录下的一组 CMS 集成示例之一。它的核心定位是:展示如何用 Next.js 的静态生成功能,从 Cosmic(一种 Hosted Headless CMS)拉取文章数据,在构建期/请求期渲染为静态页面,并支持 CMS 后台的草稿预览。
与示例配套的源码结构如下:
- pages/index.tsx:首页,通过
getStaticProps拉取全部文章; - pages/posts/[slug].tsx:文章详情页,通过
getStaticPaths+getStaticProps实现动态路径的静态生成; - pages/api/preview.ts、pages/api/exit-preview.ts:预览模式开启/关闭的 API 路由;
- lib/api.tsx:封装 Cosmic SDK 的数据获取函数,是所有数据请求的唯一出口;
- lib/markdownToHtml.ts:Markdown 到 HTML 的转换;
- interfaces/index.ts:
PostType等 TypeScript 类型定义; - components/:布局与文章展示组件(预览提示条、文章头、正文等);
- next.config.js:Next.js 配置,重点是图片域名白名单。
整个示例的依赖非常精简,从 package.json 可以看到:数据层依赖cosmicjs(Cosmic 官方 SDK),Markdown 渲染依赖remark+remark-html,日期处理用date-fns,样式用 Tailwind CSS。
二、快速启动示例
仓库 README 给出三种包管理器方式,使用create-next-app引导式创建示例项目:
# npm npx create-next-app --example cms-cosmic cms-cosmic-app# Yarn yarn create next-app --example cms-cosmic cms-cosmic-app# pnpm pnpm create next-app --example cms-cosmic cms-cosmic-app执行后会得到一个名为cms-cosmic-app的独立项目,其中已包含示例目录下的全部源码与.env.local.example模板文件。
三、配置 Cosmic 账户与三个环境变量
Step 1:创建账户并安装 App
先在 Cosmic 官网注册账户,然后从 Cosmic App Marketplace 安装Next.js Static Blog应用。这个步骤会引导你在 Cosmic 后台创建好 Bucket、posts 数据模型(title、slug、excerpt、cover_image、author、content 等字段),是后续所有 API 调用能命中数据的前提。
Step 2:准备.env.local
进入 Cosmic 后台的Settings > Basic Settings,然后复制示例目录中的环境变量模板:
cp .env.local.example .env.local模板文件.env.local.example内容只有三行:
COSMIC_BUCKET_SLUG= COSMIC_READ_KEY= COSMIC_PREVIEW_SECRET=在.env.local中填入实际值:
COSMIC_BUCKET_SLUG:Cosmic 后台API Access区域的Bucket slug;COSMIC_READ_KEY:API Access区域的Read Key(只读密钥,足够本示例的读场景使用);COSMIC_PREVIEW_SECRET:任意随机字符串(避免空格),用于预览模式的身份校验。
这三个变量与源码的对应关系可以从 lib/api.tsx 得到印证:
const BUCKET_SLUG = process.env.COSMIC_BUCKET_SLUG; const READ_KEY = process.env.COSMIC_READ_KEY; const bucket = Cosmic().bucket({ slug: BUCKET_SLUG, read_key: READ_KEY, });而COSMIC_PREVIEW_SECRET只在 pages/api/preview.ts 中参与校验(见下文预览模式一节)。
Step 3:启动开发模式
npm install npm run dev # 或 yarn install yarn dev启动后博客运行在http://localhost:3000。
四、静态生成链路:getStaticPaths 与 getStaticProps
首页:全量文章拉取
pages/index.tsx 的getStaticProps调用 lib/api.tsx 中的getAllPostsForHome:
export const getAllPostsForHome = async (preview: boolean): Promise<PostType[]> => { const params = { query: { type: "posts", }, props: "title,slug,metadata,created_at", sort: "-created_at", ...(preview && { status: "any" }), }; const data = await bucket.getObjects(params); return data.objects; };注意几个 Cosmic API 的用法细节:
query: { type: "posts" }限定只查 posts 类型对象;props字段白名单让响应只携带渲染首页所需的字段,减少传输量;sort: "-created_at"按创建时间倒序,首页据此把第一篇作为 Hero 大图文章、其余进入"更多文章"列表(见 index.tsx 中allPosts[0]与allPosts.slice(1)的切分);...(preview && { status: "any" })是预览模式的关键开关:只有处于预览态时,才向 Cosmic 追加status: "any",从而把草稿(draft)状态的文章也一并返回;正常构建时只会拿到已发布(published)内容。
文章页:动态路径 + fallback
pages/posts/[slug].tsx 中的getStaticPaths在构建期向 Cosmic 查询全部文章 slug:
export async function getStaticPaths() { const allPosts = (await getAllPostsWithSlug()) || []; return { paths: allPosts.map((post) => `/posts/${post.slug}`), fallback: true, }; }getAllPostsWithSlug只请求props: "slug"(见 lib/api.tsx),是最轻量的查询;fallback: true意味着构建期未包含的 slug 不会直接 404,而是在首次访问时触发getStaticProps按需生成并缓存——这对"CMS 里随时可能新增文章"的场景非常关键。
页面组件里对应的兜底逻辑是:
const router = useRouter(); if (!router.isFallback && !post?.slug) { return <ErrorPage statusCode={404} />; }即 fallback 期间显示 "Loading…" 占位标题,若最终拿不到文章则渲染 404(见 [slug].tsx)。
getStaticProps侧调用getPostAndMorePosts(slug, preview)一次拿到当前文章 + 最多 2 篇"相关文章"(Cosmic 查询limit: 3,再过滤掉当前 slug 后slice(0, 2),见 lib/api.tsx),并把metadata.content中的 Markdown 在服务端转成 HTML:
const content = await markdownToHtml(data["post"]?.metadata?.content || "");Markdown 渲染
lib/markdownToHtml.ts 的完整实现只有 7 行:
import { remark } from "remark"; import html from "remark-html"; const markdownToHtml = async (markdown: string) => { const result = await remark().use(html).process(markdown); return result.toString(); }; export default markdownToHtml;由于转换发生在getStaticProps中,客户端收到的已经是最终 HTML,浏览器端无需任何 Markdown 解析开销。文章数据结构由 interfaces/index.ts 定义:PostType包含title、slug、content、created_at以及metadata(封面图cover_image、作者author、摘要excerpt),其中图片类型为ImgixType(同时保存url与imgix_url),为下一节的图片处理做准备。
五、Preview Mode(Draft Mode):从 CMS 后台一键预览草稿
这是本示例最有实战价值、也最能体现 Next.js 静态生成与 CMS 配合技巧的部分。原文档 Step 5 的操作流程与源码实现一一对应:
1. 在 Cosmic 后台配置 Preview Link
进入Posts > Edit Settings,在 "Preview Link" 区域填入(README 原图展示了 Cosmic 后台该区域的截图位置):
http://localhost:3000/api/preview?secret=<secret>&slug=[object_slug]<secret>即你在.env.local中设置的COSMIC_PREVIEW_SECRET;[object_slug]是 Cosmic 的 shortcode,点击时会被自动替换为该文章的slug字段值。
2./api/preview的安全校验与开启
pages/api/preview.ts 的完整逻辑值得逐段对照阅读:
export default async function preview(req, res) { // 校验 secret 与 slug 参数;secret 只应被本路由和 CMS 知道 if ( req.query.secret !== process.env.COSMIC_PREVIEW_SECRET || !req.query.slug ) { return res.status(401).json({ message: "Invalid token" }); } // 回查 CMS,确认该 slug 真实存在 const post = await getPreviewPostBySlug(req.query.slug); if (!post) { return res.status(401).json({ message: "Invalid slug" }); } // 通过设置 Cookie 开启 Draft Mode res.setDraftMode({ enable: true }); // 重定向到从 CMS 取回的 slug 对应路径 // 注意:不使用 req.query.slug 重定向,避免开放重定向(open redirect)漏洞 res.writeHead(307, { Location: `/posts/${post.slug}` }); res.end(); }三个要点:
- 双重校验:先比对
COSMIC_PREVIEW_SECRET,再调用getPreviewPostBySlug回查 Cosmic。该函数(lib/api.tsx)携带status: "any"查询,即草稿也能查到——这正是"COSMIC 里有草稿但页面没发布"时预览依然生效的原因; res.setDraftMode({ enable: true }):示例采用的是 Next.js 新版 Draft Mode API(通过设置专用 Cookie 开启),而非旧版setPreviewData。开启后,该浏览器的请求命中getStaticProps时会自动带上preview: true参数——这正是前面getAllPostsForHome(preview)、getPostAndMorePosts(slug, preview)签名中preview参数的来源,它触发status: "any",让草稿数据流入静态渲染;- 防开放重定向:代码注释明确说明,重定向目标不是用户传入的
req.query.slug,而是从 CMS 回查结果里取回的post.slug,避免构造恶意slug把用户重定向到外部站点。
3. 草稿态的完整体验
按 README 描述的实验流程:把某篇文章标题改成带[Draft]前缀,只点Save Draft而不点 Publish。此时:
- 直接访问该文章页 → 看到旧标题(静态页面仍是已发布版本);
- 点击 Cosmic 后台的Preview Link按钮 → 走
/api/preview开启 Draft Mode 后 307 重定向到/posts/[slug]→ 看到带[Draft]的新标题。
4. 退出预览
页面顶部的提示条由 components/alert.tsx 渲染:处于预览态时显示 "This page is a preview. Click here to exit preview mode.",链接指向/api/exit-preview。该路由(pages/api/exit-preview.ts)实现极简:
export default function exit(_, res) { // 通过移除 Cookie 退出 Draft Mode res.setDraftMode({ enable: false }); res.writeHead(307, { Location: "/" }); res.end(); }六、图片配置:Cosmic 的 Imgix 域名白名单
Cosmic 的图片托管基于 Imgix,文章的imgix_url指向imgix.cosmicjs.com。为了让<Image>组件合法加载这些远程图,next.config.js 配置了remotePatterns:
module.exports = { images: { remotePatterns: [ { protocol: "https", hostname: "imgix.cosmicjs.com", port: "", pathname: "/my-account/**", }, ], }, };从源码结构看,pathname前缀/my-account/**是 Cosmic Bucket 图片 URL 的公共前缀(my-account为占位命名,实际 URL 以各自 Bucket 为准)。配置时若域名不匹配,next/image会在开发期报 "doesn't match any configured remotepatterns" 错误(对应仓库错误文档 next-image-unconfigured-localpatterns.mdx)。
七、部署
本地开发验证通过后,按 README 的 Step 6,可将项目推送到 Git 仓库再导入 Vercel 部署。部署时的关键动作是在 Vercel 的Environment Variables中配置与.env.local相同的三个变量(COSMIC_BUCKET_SLUG、COSMIC_READ_KEY、COSMIC_PREVIEW_SECRET),否则构建期拉取数据会因缺少凭证而失败。README 同时提供了基于官方模板的一键 Deploy 入口,其中预设了同样这三个环境变量的名称。
八、小结与延伸阅读
cms-cosmic 示例的价值在于用最小的代码量串起了 SSG 博客的三个工程要点:构建期数据预取(getStaticPaths+fallback: true)、按字段白名单 +status: "any"精细控制 CMS 查询、以及secret 校验 +setDraftMode+ 安全重定向组成的草稿预览闭环。这套模式同样适用于接入其他 Headless CMS。
仓库中还有大量结构相近的 CMS 集成示例可作对照参考,例如:Contentful、Sanity、Prismic、WordPress、Payload、Tina,以及不带 CMS 的基础博客模板 Blog Starter。
【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考