Next.js 与 Umbraco Delivery API 构建静态博客:无头 CMS 接入、静态生成与 Preview Mode 全链路实战
本文以 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.json(
next、react 18、tailwindcss 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 在本地创建项目:
-
创建一个空文件夹并在其中打开终端;
-
安装 Umbraco .NET CLI 模板(要求 12.0 及以上版本):
dotnet new install Umbraco.Templates::13.* -
创建 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. 发布示例数据
示例内容初始状态全部未发布,必须发布后博客才会显示文章:
- 在 Content 树中点击 Posts 节点(它包含所有文章);
- 在浏览器窗口右下角找到绿色的 "Save and publish" 按钮;
- 点击按钮旁的小上箭头,选择 "Publish with descendants...";
- 在对话框中勾选 "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:dev 为 next、build 为 next build、start 为 next 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 / getStaticPaths。pages/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.ts 的 fetchSingle / 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-Key、Preview; - 两个核心请求:
- 单条:
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.ts:id、slug、title、coverImage、date、author、excerpt、content、tags):
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.tsx 的
getStaticProps调用getAllPostsForHome(preview)(取 10 篇、展开 author),第一篇作为 hero post,其余进入 "More Stories" 列表; - 详情页 pages/posts/[slug].tsx 的
getStaticPaths调用getAllPostSlugs(preview)(取 100 篇、不展开 author,减少不必要的数据传输),并设置fallback: false——只构建 slug 列表中存在的文章,其余路由返回 404。
关于查询效率:原文档特别指出,Content Delivery API 本身功能丰富,但本示例为控制复杂度省略了部分特性与优化,存在轻微 over-fetching(过度抓取),尤其在一次拉取多篇文章时(例如为取 slug 列表而取回 100 篇文章的完整结构)。生产项目中可按需裁剪 expand 与 fields 参数。
关键配置细节
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.css、tailwind.config.js),与博客视觉呈现无关的读者可以跳过。
部署(Step 9)
原文档给出的部署要点:
- 先部署 Umbraco:博客上线前,必须先将 Umbraco 站点部署到某云厂商,使博客数据对生产环境可访问(Azure 部署需遵循 Umbraco 官方的 Azure Web Apps 指南;也可使用 Umbraco Cloud);
- 再部署 Next.js 应用:将项目推送到代码托管平台并导入 Vercel;
- 关键步骤——同步环境变量:导入项目后务必在 Vercel 的 Environment Variables 中把
UMBRACO_SERVER_URL、UMBRACO_DELIVERY_API_KEY、UMBRACO_PREVIEW_SECRET设置为与 Umbraco 生产部署一致的值; - 生产环境不要再携带
NODE_TLS_REJECT_UNAUTHORIZED=0(该开关仅为本地自签证书场景服务)。
小结
该示例完整演示了一条"无头 CMS + 静态生成"的落地链路:
| 环节 | 机制 |
|---|---|
| 内容管理 | Umbraco backoffice + Delivery API(Enabled: true + ApiKey) |
| 数据抓取 | 构建/请求时 fetch Delivery API v2,expand/sort/take 控制查询 |
| 静态渲染 | getStaticPaths(fallback: 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.ts 的 extractPost 映射与 types/post.ts 的数据结构。
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 StartedRust0626
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