基于 Next.js 与 Ghost 的静态生成博客:cms-ghost 示例深度解析
本文围绕 Next.js 仓库中的 cms-ghost 示例展开,介绍如何用 Ghost Headless CMS 作为数据源、通过静态生成(Static Generation)构建一个完整的博客站点。读完后你将掌握 Ghost Content API 的封装方式、getStaticPaths / getStaticProps 的配合用法、草稿模式(Draft Mode)预发布流程,以及如何通过环境变量接入自己的 Ghost 实例并完成部署。
一、示例定位与技术栈
cms-ghost 示例位于 examples/cms-ghost,它是一个基于 Next.js Pages Router 的静态生成博客样板。其核心价值在于演示“Next.js 静态生成 + Headless CMS 数据源”这一标准组合:构建时从 Ghost 拉取全部内容并预渲染为静态页面,运行时几乎不依赖 CMS 的可用性。
从 package.json 可以确认示例的核心依赖:
@tryghost/content-api(^1.4.13):Ghost 官方 Content API 客户端,所有数据访问都基于它;next(latest)+react/react-dom(^18.2.0):框架与 UI 基座;lazysizes:图片懒加载;classnames、date-fns:样式类合并与日期格式化;tailwindcss(^3.0.15)+autoprefixer+postcss(devDependencies):原子化 CSS 方案。
示例的整体目录结构如下:
pages/:index.js(首页)、posts/[slug].js(文章详情页)、api/preview.js与api/exit-preview.js(草稿模式开关);lib/:api.js(Ghost 数据访问层)、constants.js(站点常量)、defaults.js(演示站默认凭据);components/:约 18 个展示组件(hero-post、post-preview、cover-image、markdown-styles.module.css等);styles/index.css+tailwind.config.js:Tailwind 入口与配置。
二、快速开始
README 中给出的接入方式是通过 create-next-app 引导示例工程,支持 npm、Yarn、pnpm 三种方式:
npx create-next-app --example cms-ghost cms-ghost-app
yarn create next-app --example cms-ghost cms-ghost-app
pnpm create next-app --example cms-ghost cms-ghost-app
初始化后进入工程安装依赖并启动开发服务器:
npm install
npm run dev
# or
yarn install
yarn dev
启动后访问 http://localhost:3000 即可看到博客。这里有一个重要的设计点:首次运行无需任何配置——示例内置了一个公共演示 Ghost 实例的 URL 和 Content API Key 作为回退默认值,开发模式下开箱即用。
这些默认值定义在 lib/defaults.js 中,以 base64 编码形式存储并解码导出:
export const GHOST_API_URL_DEFAULT = Buffer.from(
"aHR0cHM6Ly9jbXMuZ290c2J5Lm9yZw==",
"base64",
).toString("utf-8");
export const GHOST_API_KEY_DEFAULT = Buffer.from(
"Mzg3Zjk1NmVhYTk1MzQ1ZjdiYjQ4NGQwYjg=",
"base64",
).toString("utf-8");
对应的 API 封装在 lib/api.js 顶部,环境变量优先、默认值兜底:
const GHOST_API_URL = process.env.GHOST_API_URL || GHOST_API_URL_DEFAULT;
const GHOST_API_KEY = process.env.GHOST_API_KEY || GHOST_API_KEY_DEFAULT;
const api = new GhostContentAPI({
url: GHOST_API_URL,
key: GHOST_API_KEY,
version: "v3.0",
});
也就是说,@tryghost/content-api 客户端固定使用 Ghost Content API 的 v3.0 版本初始化,后续所有 browse / read 调用都基于该实例。
三、数据访问层:四个核心查询函数
lib/api.js 是全示例唯一接触 Ghost 的地方,四个导出函数覆盖了站点所需的全部数据场景:
1. getAllPostsForHome(preview) —— 首页文章列表
export async function getAllPostsForHome(preview) {
const params = {
limit: "all",
include: "authors",
order: "published_at DESC",
...(preview && { status: "all" }),
};
const posts = await api.posts.browse(params);
return posts;
}
要点:limit: "all" 拉取全部文章;include: "authors" 让返回对象携带 primary_author 字段(首页组件正是用它渲染作者信息);order: "published_at DESC" 保证最新发布的文章排在最前,即首页 Hero 位。参数中的 ...(preview && { status: "all" }) 是草稿模式的关键——开启预览后把过滤条件从默认的已发布文章放宽为全部状态(含 draft)。
2. getAllPostsWithSlug() —— 供 getStaticPaths 使用的 slug 清单
export async function getAllPostsWithSlug() {
const params = {
fields: "slug",
limit: "all",
};
const posts = await api.posts.browse(params);
return posts;
}
注意 fields: "slug" 只请求 slug 字段,最小化网络负载,因为构建阶段只需要路径参数。
3. getPostAndMorePosts(slug, preview) —— 单篇文章 + 相关推荐
const post = await api.posts.read(singleObjectParams).catch((error) => {
// Don't throw if an slug doesn't exist
if (is404(error)) return;
throw error;
});
const morePosts = (await api.posts.browse(moreObjectParams))
?.filter(({ slug }) => post.slug !== slug)
.slice(0, 2);
两个细节值得注意:一是 404 容忍策略,is404 通过正则 /not found/i 匹配错误消息,slug 不存在时返回 undefined 而不是抛错,页面据此渲染 404;二是“更多文章”取 3 篇再过滤掉当前文章自身、截取前 2 篇(slice(0, 2)),保证推荐位数量恒定为 2。
4. getPreviewPostBySlug(slug) —— 预览模式校验
try {
const post = await api.posts.read(params);
return post;
} catch (error) {
if (is404(error)) return;
throw error;
}
只请求 fields: "slug",用途是验证草稿链接携带的 slug 在 CMS 中真实存在,防止无效预览。
四、静态生成实现:首页与详情页
首页:构建时渲染全部内容
pages/index.js 的静态属性非常简洁:
export async function getStaticProps({ preview }) {
const allPosts = (await getAllPostsForHome(preview)) || [];
return {
props: { allPosts },
};
}
组件将首篇文章作为 Hero 位(heroPost = allPosts[0]),其余文章交给 MoreStories 组件以卡片网格展示;getStaticProps 返回的 allPosts 在构建时被序列化进 HTML,首页因此是完全静态的。
详情页:getStaticPaths + fallback: true
pages/posts/[slug].js 展示了动态路由的完整静态生成链路:
export async function getStaticPaths() {
const allPosts = (await getAllPostsWithSlug()) || [];
return {
paths: allPosts.map((post) => `/posts/${post.slug}`),
fallback: true,
};
}
export async function getStaticProps({ params, preview = null }) {
const { post, morePosts } = await getPostAndMorePosts(params.slug, preview);
return {
props: {
preview,
post,
morePosts: morePosts || [],
},
};
}
getStaticPaths在构建时为每篇文章的 slug 预生成/posts/{slug}路径;fallback: true表示未在构建期覆盖的路径(例如构建后才发布的新文章)会先渲染回退 UI,再按需生成。组件侧用router.isFallback判断该状态,在数据到达前先显示<PostTitle>Loading…</PostTitle>;- 当
getStaticProps返回的post为空(slug 不存在)时,页面渲染next/error的ErrorPage statusCode={404}:
if (!router.isFallback && !post?.slug) {
return <ErrorPage statusCode={404} />;
}
正文渲染则由 components/post-body.js 完成——Ghost 返回的是已渲染的 HTML(post.html),通过 dangerouslySetInnerHTML 注入,并用 markdown-styles.module.css 的 CSS Modules 类统一排版:
<div
className={markdownStyles["markdown"]}
dangerouslySetInnerHTML={{ __html: content }}
/>
这意味着排版样式完全由前端 CSS 控制,而不是依赖 Ghost 的主题模板,这正是 Headless 模式的典型特征。
五、草稿模式(Draft Mode)预览
示例内置了完整的“CMS 中点击预览 → 浏览器查看草稿 → 退出预览”闭环,由两个 API 路由实现。
开启预览:pages/api/preview.js
pages/api/preview.js 的完整逻辑:
- 校验:请求必须携带与
process.env.GHOST_PREVIEW_SECRET一致的secret参数以及slug参数,否则返回401 Invalid token。这个 secret 只应由 CMS 端(通常在 Ghost 的自定义集成/钩子中配置)与本 API 路由共享; - 存在性检查:调用
getPreviewPostBySlug确认 slug 真实存在,否则返回401 Invalid slug,防止开启无效的草稿模式; - 开启草稿模式:
res.setDraftMode({ enable: true })设置 Next.js 的草稿模式 Cookie; - 安全重定向:重定向目标不是直接信任
req.query.slug,而是使用从 CMS 重新拉取到的post.slug,源码注释明确说明这是为了避免开放重定向(open redirect)漏洞:
// Redirect to the path from the fetched post
// We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities
res.writeHead(307, { Location: `/posts/${post.slug}` });
草稿模式生效后,前文 getStaticProps({ preview }) 收到的 preview 为真值,getAllPostsForHome / getPostAndMorePosts 会附加 status: "all",从而能读取 draft 状态的未发布文章。页面顶部则通过 components/layout.js 中的 <Alert preview={preview} /> 显示预览状态提示条。
退出预览:pages/api/exit-preview.js
pages/api/exit-preview.js 只有三行核心逻辑:res.setDraftMode({ enable: false }) 移除草稿 Cookie,然后 307 重定向回首页。
六、接入自己的 Ghost:环境变量配置
当你要从演示站切换到自有 Ghost 实例时,按 README 的指引操作:
- 在 Ghost Admin 面板的 Integrations → Add custom integration 中创建集成,生成 Content API Key(注意必须是 Content API Key,而不是 Admin API Key);
- 在项目根目录创建
.env.local:
GHOST_API_URL=...
GHOST_API_KEY=...
若要走通草稿预览流程,还需要配置 GHOST_PREVIEW_SECRET(即 pages/api/preview.js 中校验的 secret)。
一个容易踩坑的部署注意事项:README 特别指出,Ghost 实例部署在 localhost 时无法用于公开站点——因为文章中的图片(feature_image 等 https://static.ghost.org/.../my-account/** 形式的 URL)在本地机器上,外部访客无法访问。示例中的演示实例(cms.gotsby.org,由 lib/defaults.js 的 base64 默认值解码可见)就部署在 Hetzner Cloud 上,属于可公网访问的远程实例。
图片域名白名单
由于 Ghost 文章的封面图托管在远程 CDN,使用 next/image 时必须放行该域名。next.config.js 给出了精确配置:
module.exports = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "static.ghost.org",
port: "",
pathname: "/my-account/**",
},
],
},
};
替换为自己的 Ghost 实例后,通常需要同步把 hostname / pathname 调整为你实例实际的图片 CDN 域名。图片组件 components/cover-image.js 使用 <Image layout="responsive" ...> 配合 srcset 做响应式处理,并将封面图包裹在 next/link 中跳转到 /posts/${slug}。
七、部署
README 推荐将示例部署到 Vercel,两条路径:
- 部署本地工程:将代码推送到 Git 仓库后导入 Vercel。关键步骤是在 Vercel 的 Environment Variables 中配置与本地
.env.local一致的变量(GHOST_API_URL、GHOST_API_KEY,如启用预览再加GHOST_PREVIEW_SECRET),否则构建时getStaticProps会回落到演示站默认值,生产站点将展示演示内容而非你的内容; - 从模板直接部署:通过示例仓库提供的 Deploy 按钮一键导入模板并填写上述环境变量。
构建阶段(next build)会对 getStaticPaths 声明的全部路径执行预渲染,因此构建时间随文章数量线性增长;对于超大站点,可以考虑按需回退或分页加载文章列表(示例保持简单,始终 limit: "all")。
八、小结与延伸
cms-ghost 示例以最小依赖演示了一套可复用的 Headless CMS 集成模式,其可迁移的经验包括:
- 单一数据访问层(
lib/api.js):组件不直接触碰 Content API,查询参数(fields、include、order、status)集中管理,preview标志统一控制草稿可见性; - 静态生成为主、fallback 兜底:
getStaticPaths预生成 +fallback: true处理新发布文章 + 404 容忍,三层配合覆盖内容生命周期; - 安全的预览流程:secret 校验、slug 存在性校验、基于服务端数据的重定向,三个环节缺一不可;
- 环境变量 + 默认值回退:开发免配置、生产可切换,通过
GHOST_API_URL/GHOST_API_KEY实现零代码迁移。
Next.js 仓库中还有大量结构相近的其他 CMS 集成示例可供对照参考,例如 examples/cms-contentful、examples/cms-datocms、examples/cms-storyblok、examples/cms-tina、examples/cms-wordpress 等,它们的 API 层封装与 cms-ghost 的思路一致,可按所选 CMS 直接替换数据层。
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 StartedRust0624
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