首页
/ 基于 Next.js 与 Ghost 的静态生成博客:cms-ghost 示例深度解析

基于 Next.js 与 Ghost 的静态生成博客:cms-ghost 示例深度解析

2026-09-06 15:36:48作者:宗隆裙

本文围绕 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:图片懒加载;
  • classnamesdate-fns:样式类合并与日期格式化;
  • tailwindcss(^3.0.15)+ autoprefixer + postcss(devDependencies):原子化 CSS 方案。

示例的整体目录结构如下:

  • pages/index.js(首页)、posts/[slug].js(文章详情页)、api/preview.jsapi/exit-preview.js(草稿模式开关);
  • lib/api.js(Ghost 数据访问层)、constants.js(站点常量)、defaults.js(演示站默认凭据);
  • components/:约 18 个展示组件(hero-postpost-previewcover-imagemarkdown-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/errorErrorPage 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 的完整逻辑:

  1. 校验:请求必须携带与 process.env.GHOST_PREVIEW_SECRET 一致的 secret 参数以及 slug 参数,否则返回 401 Invalid token。这个 secret 只应由 CMS 端(通常在 Ghost 的自定义集成/钩子中配置)与本 API 路由共享;
  2. 存在性检查:调用 getPreviewPostBySlug 确认 slug 真实存在,否则返回 401 Invalid slug,防止开启无效的草稿模式;
  3. 开启草稿模式res.setDraftMode({ enable: true }) 设置 Next.js 的草稿模式 Cookie;
  4. 安全重定向:重定向目标不是直接信任 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 的指引操作:

  1. 在 Ghost Admin 面板的 Integrations → Add custom integration 中创建集成,生成 Content API Key(注意必须是 Content API Key,而不是 Admin API Key);
  2. 在项目根目录创建 .env.local
GHOST_API_URL=...
GHOST_API_KEY=...

若要走通草稿预览流程,还需要配置 GHOST_PREVIEW_SECRET(即 pages/api/preview.js 中校验的 secret)。

一个容易踩坑的部署注意事项:README 特别指出,Ghost 实例部署在 localhost 时无法用于公开站点——因为文章中的图片(feature_imagehttps://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,两条路径:

  1. 部署本地工程:将代码推送到 Git 仓库后导入 Vercel。关键步骤是在 Vercel 的 Environment Variables 中配置与本地 .env.local 一致的变量(GHOST_API_URLGHOST_API_KEY,如启用预览再加 GHOST_PREVIEW_SECRET),否则构建时 getStaticProps 会回落到演示站默认值,生产站点将展示演示内容而非你的内容;
  2. 从模板直接部署:通过示例仓库提供的 Deploy 按钮一键导入模板并填写上述环境变量。

构建阶段(next build)会对 getStaticPaths 声明的全部路径执行预渲染,因此构建时间随文章数量线性增长;对于超大站点,可以考虑按需回退或分页加载文章列表(示例保持简单,始终 limit: "all")。

八、小结与延伸

cms-ghost 示例以最小依赖演示了一套可复用的 Headless CMS 集成模式,其可迁移的经验包括:

  • 单一数据访问层lib/api.js):组件不直接触碰 Content API,查询参数(fieldsincludeorderstatus)集中管理,preview 标志统一控制草稿可见性;
  • 静态生成为主、fallback 兜底getStaticPaths 预生成 + fallback: true 处理新发布文章 + 404 容忍,三层配合覆盖内容生命周期;
  • 安全的预览流程:secret 校验、slug 存在性校验、基于服务端数据的重定向,三个环节缺一不可;
  • 环境变量 + 默认值回退:开发免配置、生产可切换,通过 GHOST_API_URL / GHOST_API_KEY 实现零代码迁移。

Next.js 仓库中还有大量结构相近的其他 CMS 集成示例可供对照参考,例如 examples/cms-contentfulexamples/cms-datocmsexamples/cms-storyblokexamples/cms-tinaexamples/cms-wordpress 等,它们的 API 层封装与 cms-ghost 的思路一致,可按所选 CMS 直接替换数据层。

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