首页
/ Next.js 与 Umbraco Delivery API 构建静态博客:无头 CMS 接入、静态生成与 Preview Mode 全链路实战

Next.js 与 Umbraco Delivery API 构建静态博客:无头 CMS 接入、静态生成与 Preview Mode 全链路实战

2026-09-06 16:39:12作者:蔡丛锟

本文以 Next.js 官方仓库中的 examples/cms-umbraco 示例为主体,完整讲解如何用 Umbraco 的 Content Delivery API 作为无头数据源、用 Next.js 的 Static Generation 特性生成博客站点,并实现 Preview Mode 草稿预览。读完本文,你将掌握从 Umbraco .NET 后端搭建、Delivery API 启用、环境变量配置,到前端静态数据抓取(getStaticPaths / getStaticProps)与预览模式 Cookie 机制的完整落地方案。

示例概览

该示例是一个静态生成的博客(A statically generated blog example using Next.js and Umbraco CMS),核心特征是:

  • 数据源:Umbraco CMS 原生的 Content Delivery API(Headless 内容交付 API),Next.js 在构建/请求时通过 HTTP 拉取内容;
  • 渲染方式:基于 Next.js 的 Static Generation,文章列表与详情页均为构建时/请求时预渲染的静态 HTML;
  • 预览能力:通过 Next.js 的 Preview Mode(Draft Mode)配合 Delivery API 的 Preview 请求头,查看尚未发布的 Umbraco 内容变更;
  • 技术栈:React 18 + TypeScript + Tailwind CSS,见 package.jsonnextreact 18tailwindcss 3)。

仓库内还有 Umbraco heartcore 等同类示例,本文只聚焦本示例。

快速开始:脚手架初始化

使用 create-next-app 拉取本示例(npm / Yarn / pnpm 三种方式):

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

执行后会在 umbraco-app 目录下生成完整的 Next.js 博客工程,示例目录结构与主要文件对应关系如下(以仓库内目录为准):

目录/文件 作用
lib/api.ts 所有 Delivery API 请求与数据抽取逻辑
pages/index.tsx 首页,getStaticProps 拉取文章列表
pages/posts/[slug].tsx 文章详情页,getStaticPaths + getStaticProps
pages/api/preview.ts 开启 Preview Mode 的 API 路由
pages/api/exit-preview.ts 退出 Preview Mode 的 API 路由
next.config.js 将 Umbraco 域名加入 next/image 白名单
.env.local.example 环境变量模板
types/post.ts 文章的数据模型

构建数据源:Umbraco 后端(Step 1~5)

博客数据来自本地运行的 Umbraco 站点,需要先把它搭起来。以下是原文档给出的完整步骤。

Step 1. 创建 Umbraco 项目

使用 .NET CLI 在本地创建项目:

  1. 创建一个空文件夹并在其中打开终端;

  2. 安装 Umbraco .NET CLI 模板(要求 12.0 及以上版本):

    dotnet new install Umbraco.Templates::13.*
    
  3. 创建 Umbraco 项目:

    dotnet new umbraco
    

Step 2. 安装示例数据

为避免手工创建整套博客数据集,官方提供了名为 Umbraco.Sample.Headless.Blog 的 NuGet 包,在 Umbraco 项目的终端中安装:

dotnet add package Umbraco.Sample.Headless.Blog

Step 3. 启用 Delivery API

Umbraco Delivery API 是博客的数据源,必须显式启用。打开 Umbraco 项目中的 appsettings.json,在 Umbraco::CMS 节点内添加 DeliveryApi 配置:

"Umbraco": {
  "CMS": {
    "DeliveryApi": {
      "Enabled": true,
      "ApiKey": "my-secret-api-key"
    },
    ...
  }
}

其中 ApiKey 是可选配置——只有需要测试博客的 Preview(草稿预览)功能时才是必需的,后续 Next.js 端会通过请求头 Api-Key 带上这个密钥。

Step 4. 运行 Umbraco

在 Umbraco 项目终端中启动:

dotnet run

按安装向导完成 Umbraco 初始化;完成后会跳转到 Umbraco backoffice(后台),此时示例数据已经安装好。

Step 5. 发布示例数据

示例内容初始状态全部未发布,必须发布后博客才会显示文章:

  1. 在 Content 树中点击 Posts 节点(它包含所有文章);
  2. 在浏览器窗口右下角找到绿色的 "Save and publish" 按钮;
  3. 点击按钮旁的小上箭头,选择 "Publish with descendants...";
  4. 在对话框中勾选 "Include unpublished content items",一次性发布 Posts 及其下所有文章。

Authors 节点重复同样操作。

配置 Next.js 端环境变量(Step 6)

umbraco-app 目录下找到 .env.local.example,复制一份命名为 .env.local 并填写。仓库中的模板文件 .env.local.example 内容为:

# This is necessary when you run locally against a self-signed server. Do NOT include this in production.
NODE_TLS_REJECT_UNAUTHORIZED=0

# Add your Umbraco server URL here. Please do not include a trailing slash.
UMBRACO_SERVER_URL =

# Add your Umbraco Delivery API key here if you want to use preview.
UMBRACO_DELIVERY_API_KEY =

# Add the secret token that will be used to "authorize" preview
UMBRACO_PREVIEW_SECRET =

三个核心变量的含义:

  • UMBRACO_SERVER_URL:Umbraco 站点的基础 URL,不要带尾部斜杠
  • UMBRACO_DELIVERY_API_KEY:即 Step 3 中在 appsettings.json 里配置的 API key,仅测试 Preview Mode 时需要;
  • UMBRACO_PREVIEW_SECRET:任意随机字符串(避免空格),如 my-preview-secret,用于授权触发预览,仅 Preview Mode 需要。

填写完成后大致形如:

NODE_TLS_REJECT_UNAUTHORIZED=0
UMBRACO_SERVER_URL = 'https://localhost:12345'
UMBRACO_DELIVERY_API_KEY = 'my-secret-api-key'
UMBRACO_PREVIEW_SECRET = 'my-preview-secret'

关于 NODE_TLS_REJECT_UNAUTHORIZED=0:本地运行 .NET 站点时会自动创建自签名 SSL 证书以支持 HTTPS 绑定,而 Node.js 默认不信任自签证书,因此需要这个开关绕过 TLS 证书校验。切勿在生产环境使用,这也是模板文件头部注释明确强调的。

运行开发模式(Step 7)

umbraco-app 项目目录中执行:

npm install
npm run dev

# 或

yarn install
yarn dev

博客即可在 http://localhost:3000 访问。对应 package.json 中的 scripts:devnextbuildnext buildstartnext start

Preview Mode 原理剖析(Step 8)

如果在 Umbraco 中修改文章但不发布,默认情况下 http://localhost:3000 不会显示这些变更。开启 Preview Mode 后就能看到未发布的内容。原文档给出的操作方式:

  • 进入 http://localhost:3000/api/preview?secret=<secret> 开启预览,<secret>.env.local 中的 UMBRACO_PREVIEW_SECRET
  • 访问修改过的文章页即可看到未发布变更;
  • 访问 http://localhost:3000/api/exit-preview 退出预览。

结合示例源码可以看到其完整实现链路:

1. 开启预览 —— pages/api/preview.ts

const { secret } = req.query;

// Check the secret and next parameters
// This secret should only be known by this API route
if (!secret) {
  return res.status(401).json({ message: "No token provided" });
}

if (secret !== process.env.UMBRACO_PREVIEW_SECRET) {
  return res.status(401).json({ message: "Invalid token" });
}

res.setDraftMode({ enable: true });

res.redirect("/");

路由先校验查询参数 secret 与环境变量 UMBRACO_PREVIEW_SECRET 是否一致(不一致返回 401),再调用 res.setDraftMode({ enable: true }) 写入 Draft Mode Cookie,最后重定向回首页。

2. 退出预览 —— pages/api/exit-preview.ts

res.setDraftMode({ enable: false });
res.writeHead(307, { Location: "/" });
res.end();

通过 setDraftMode({ enable: false }) 删除 Draft Mode Cookie,并以 307 重定向回首页。

3. 预览如何传递到数据层:Next.js 在 Draft Mode 下会把 preview: boolean 注入 getStaticProps / getStaticPathspages/posts/[slug].tsx 中:

export async function getStaticPaths({ preview }: { preview: boolean }) {
  const slugs = await getAllPostSlugs(preview);
  return {
    paths: slugs.map((slug) => `/posts${slug}`),
    fallback: false,
  };
}

preview 一路传到 lib/api.tsfetchSingle / fetchMultiple,作为 Delivery API 请求头 Preview: true/false 发出(见下文)。也就是说:Umbraco 侧的 ApiKey 认证 + Next.js 侧的 Preview 请求头共同决定了是否返回未发布内容,这正是 Step 3 中 ApiKey 配置"仅预览时需要"的原因。

静态生成数据抓取:Delivery API 查询细节(源码剖析)

所有对 Umbraco 的 HTTP 请求集中在 lib/api.ts。从源码结构看,其请求模式为:

  • Base URL${UMBRACO_SERVER_URL}/umbraco/delivery/api/v2/content,即 Delivery API v2 端点;
  • 统一请求头Start-Item(指定查询起点节点,本示例为 posts)、Api-KeyPreview
  • 两个核心请求
    • 单条:GET {base}/item/{slug},用于文章详情(fetchSingle);
    • 多条:GET {base}/?{query},用于文章列表(fetchMultiple)。

列表查询参数(见 fetchPosts):

return await fetchMultiple(
  `fetch=children:/&expand=${expand}&sort=updateDate:desc&take=${take}`,
  "posts",
  preview,
);
参数 含义
fetch=children:/ 抓取 Start-Item 节点(posts)下的直接子级
expand=properties[author] 展开文章上的 author 属性(将作者内联返回,省去二次请求);首页/详情页列表需要,而纯 slug 列表不需要
sort=updateDate:desc 按更新时间倒序
take=${take} 限制条数(详情场景取 3、首页取 10、slug 列表取 100)

返回数据的抽取逻辑将 Delivery API 的原始结构映射为前端模型(types/post.tsidslugtitlecoverImagedateauthorexcerptcontenttags):

const extractSlug = (item: any): string => item.route.path;

const extractPost = (post: any): Post => {
  // NOTE: author is an expanded property on the post
  const author = extractAuthor(post.properties.author);
  return {
    id: post.id,
    slug: extractSlug(post),
    title: post.name,
    coverImage: {
      url: `${UMBRACO_SERVER_URL}${post.properties.coverImage[0].url}`,
    },
    date: post.updateDate,
    author: author,
    excerpt: post.properties.excerpt,
    content: post.properties.content.markup,
    tags: post.properties.tags,
  };
};

几个值得注意的实现细节:

  • slug 取自 route.path,即 Umbraco 内容节点的发布路由,而非内容 ID;
  • 图片 URL 需要拼接 UMBRACO_SERVER_URL:Delivery API 返回的图片是相对路径,前端补全为绝对地址;
  • 正文为 properties.content.markup(RTE 富文本的 HTML),由 components/post-body.tsx 通过 dangerouslySetInnerHTML 渲染;
  • 详情页的"相关文章"getPostAndMorePosts 在取完当前文章后再拉 3 篇最新文章,过滤掉当前文章自身后取前 2 篇作为 morePosts

对应的页面数据入口为:

  • 首页 pages/index.tsxgetStaticProps 调用 getAllPostsForHome(preview)(取 10 篇、展开 author),第一篇作为 hero post,其余进入 "More Stories" 列表;
  • 详情页 pages/posts/[slug].tsxgetStaticPaths 调用 getAllPostSlugs(preview)(取 100 篇、不展开 author,减少不必要的数据传输),并设置 fallback: false——只构建 slug 列表中存在的文章,其余路由返回 404。

关于查询效率:原文档特别指出,Content Delivery API 本身功能丰富,但本示例为控制复杂度省略了部分特性与优化,存在轻微 over-fetching(过度抓取),尤其在一次拉取多篇文章时(例如为取 slug 列表而取回 100 篇文章的完整结构)。生产项目中可按需裁剪 expandfields 参数。

关键配置细节

1. next/image 图片域名白名单

文章封面图直接来自 Umbraco 服务器,因此 next.config.js 从环境变量解析出域名并加入白名单:

module.exports = {
  images: {
    // add the Umbraco server domain as allowed domain for serving images
    domains: [process.env.UMBRACO_SERVER_URL.match(/.*\/\/([^:/]*).*/)[1]],
  },
};

从源码结构看,这里用正则从 UMBRACO_SERVER_URL 中截取主机名——因此该环境变量必须是带协议的完整 URL(如 https://localhost:12345),否则域名解析会失败。

2. 组件与样式:页面由 components/ 下的 17 个组件拼装(hero-post、post-preview、post-header、more-stories 等),样式采用 Tailwind + CSS Modules(styles/index.csstailwind.config.js),与博客视觉呈现无关的读者可以跳过。

部署(Step 9)

原文档给出的部署要点:

  1. 先部署 Umbraco:博客上线前,必须先将 Umbraco 站点部署到某云厂商,使博客数据对生产环境可访问(Azure 部署需遵循 Umbraco 官方的 Azure Web Apps 指南;也可使用 Umbraco Cloud);
  2. 再部署 Next.js 应用:将项目推送到代码托管平台并导入 Vercel;
  3. 关键步骤——同步环境变量:导入项目后务必在 Vercel 的 Environment Variables 中把 UMBRACO_SERVER_URLUMBRACO_DELIVERY_API_KEYUMBRACO_PREVIEW_SECRET 设置为与 Umbraco 生产部署一致的值;
  4. 生产环境不要再携带 NODE_TLS_REJECT_UNAUTHORIZED=0(该开关仅为本地自签证书场景服务)。

小结

该示例完整演示了一条"无头 CMS + 静态生成"的落地链路:

环节 机制
内容管理 Umbraco backoffice + Delivery API(Enabled: true + ApiKey
数据抓取 构建/请求时 fetch Delivery API v2,expand/sort/take 控制查询
静态渲染 getStaticPathsfallback: false)+ getStaticProps 预渲染文章页
草稿预览 /api/preview 校验 secret 后 setDraftMode({ enable: true }),Draft Mode 触发带 Preview: true 请求头的重新生成
图片 Umbraco 域名经 next.config.js 加入 images.domains 白名单

如果你需要在自建 Umbraco 站点上复刻这套流程,直接以 examples/cms-umbraco 为模板、按本文 Step 1~9 执行即可;调整内容模型时重点参照 lib/api.tsextractPost 映射与 types/post.ts 的数据结构。

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