目录
这篇文章记录一套可以复现的博客工作流:用 Markdown 写文章,Hugo 和 Prism 生成静态页面,再用 Cloudflare CLI 发布到 Workers Static Assets。下面以 Windows x64 和 PowerShell 7 为例,包含项目文件、依赖、配置和命令。
1. 下载示例项目
下载博客示例项目(ZIP)。压缩包包含 Prism 主题源码、示例文章、默认封面和构建与发布脚本,不包含云端账号或凭证。
在 PowerShell 中进入下载文件所在的目录,然后解压:
Expand-Archive -Path .\blog-starter.zip -DestinationPath .\blog-starter
Set-Location .\blog-starter
后续命令都在解压后的项目根目录运行。项目主要文件如下:
blog-starter/
├── blog-config.json # Worker 名称和博客域名
├── hugo.toml # 博客标题、主题和 Hugo 设置
├── content/post/ # Markdown 文章
├── assets/default-cover.webp
├── themes/prism/ # Prism 主题及其许可
├── prepare-content.ps1 # 准备文章的临时副本
├── build.ps1 # 构建静态网站
└── deploy.ps1 # 构建、预检并发布
这个示例包含自动补标题和默认封面的自定义配置;单独安装 Prism 主题并不会自动得到这些功能。
2. 安装运行环境
先安装 PowerShell 7 和 Node.js;Cloudflare CLI 需要 Node.js 22.18 或更新版本。在 PowerShell 7 中进入项目根目录,再安装本教程测试过的 Cloudflare CLI:
npm install --global cf@0.15.0
cf --version
接着下载 Hugo Extended 和 Dart Sass。下面的命令将它们解压到项目的 tools 目录,并加入当前 PowerShell 窗口的 PATH:
New-Item -ItemType Directory -Force .\tools | Out-Null
Invoke-WebRequest -Uri 'https://github.com/gohugoio/hugo/releases/download/v0.169.0/hugo_extended_0.169.0_windows-amd64.zip' -OutFile .\tools\hugo.zip
Expand-Archive .\tools\hugo.zip -DestinationPath .\tools\hugo
Invoke-WebRequest -Uri 'https://github.com/sass/dart-sass/releases/download/1.107.0/dart-sass-1.107.0-windows-x64.zip' -OutFile .\tools\sass.zip
Expand-Archive .\tools\sass.zip -DestinationPath .\tools
$env:PATH = "$PWD\tools\hugo;$PWD\tools\dart-sass;$env:PATH"
hugo version
sass --version
Hugo 版本输出应包含 extended。新开 PowerShell 窗口后,需要再次运行上面的 $env:PATH = ... 这一行,或者自行把两个工具目录加入系统 PATH。本示例经过 Hugo Extended 0.169.0、Dart Sass 1.107.0 和 Cloudflare CLI 0.15.0 验证。
3. 配置自己的 Cloudflare 账号和域名
登录 Cloudflare:
cf auth login
按照提示在浏览器完成授权。接着编辑项目里的 blog-config.json:
{
"workerName": "my-blog",
"customDomain": "blog.example.com"
}
把 workerName 换成自己账号中尚未使用的 Worker 名称,把 customDomain 换成自己的博客域名。域名需要属于自己账号中处于启用状态的 Cloudflare zone;先确认这个子域名没有被其他服务或冲突的 DNS 记录占用。部署成功后,Cloudflare 会为自定义域名配置相应的 DNS 和证书,生效可能需要一点时间。
示例中的 blog.example.com 只是占位值,必须替换。发布配置由脚本读取,也用于生成站点的完整链接。博客标题和简介则在 hugo.toml 中修改。
4. 写第一篇 Markdown
在 content/post 下创建 my-first-post.md,例如:
# 我的第一篇文章
这是文章的第一段。
## 一个小节
这里是正文。
不需要为普通文章手写 HTML 或 CSS。对于没有元信息的 Markdown,示例脚本会在构建副本中将开头的一级标题作为文章标题;没有一级标题则使用文件名。YAML 或 TOML 元信息中的标题会保留,缺标题时使用文件名。带 JSON 元信息的文章应自行填写标题。没有指定封面时,文章使用默认封面;没有指定日期时,使用文件修改时间。原始 Markdown 不会被改写。
如果想固定日期或设置分类、标签,可以使用 Hugo 的元信息:
---
title: 我的第一篇文章
date: 2026-10-29
description: 这篇文章的简介。
tags:
- 随笔
---
这里是正文。
要指定文章自己的封面,可以在 content/post 下创建文章文件夹,将文章命名为 index.md,图片放在同一文件夹,然后在元信息中写 image: cover.webp。不指定封面也能发布。
5. 构建、预检和发布
在项目根目录运行:
.\deploy.ps1
脚本会先运行 build.ps1:复制文章到临时目录,补充缺失的标题,用 Hugo、Prism 和 Dart Sass 构建静态网站。然后它会依次执行:
cf deploy --prebuilt --dry-run
cf deploy --prebuilt
第一条命令预检构建结果;预检成功后,第二条命令才上传文件并部署 Worker。部署完成后,打开自己的博客域名,检查首页和文章是否正常。
如果只想先构建或预览,不发布,可以运行:
.\build.ps1 -Preview
python -m http.server 1313 --directory .cloudflare\output\v0\workers\default\assets
这一步需要安装 Python;启动后在浏览器打开 http://localhost:1313/。-Preview 会把预览资源链接指向本机;修改文章后重新运行预览构建,再刷新页面。正式发布时,deploy.ps1 会自动重新构建成正式域名。
项目脚本完整源码
上面的 .\deploy.ps1 是运行项目根目录里的 PowerShell 脚本,不是 Cloudflare 自带的命令。下载的 ZIP 已包含这些文件;以下是同一份源码。脚本依赖 ZIP 里的主题、默认封面、content/post/_index.md 和 layouts/_partials/helper/image.html,仅复制脚本本身还不够。
deploy.ps1:发布入口
这个文件在项目根目录。它调用构建脚本,检查预检结果,并在成功后发布:
$ErrorActionPreference = 'Stop'
Set-Location -LiteralPath $PSScriptRoot
if (-not (Get-Command cf -ErrorAction SilentlyContinue)) { throw '请先安装 Cloudflare CLI。' }
& (Join-Path $PSScriptRoot 'build.ps1')
cf deploy --prebuilt --dry-run
if ($LASTEXITCODE -ne 0) { throw '预检失败,未进行部署。' }
cf deploy --prebuilt
if ($LASTEXITCODE -ne 0) { throw 'Cloudflare 部署失败。' }
build.ps1:生成网站
该脚本读取 blog-config.json,准备 Cloudflare Build Output,并使用 Hugo 构建。-Preview 用于生成本地预览链接:
param([switch]$Preview)
$ErrorActionPreference = 'Stop'
Set-Location -LiteralPath $PSScriptRoot
foreach ($command in @('hugo', 'sass')) {
if (-not (Get-Command $command -ErrorAction SilentlyContinue)) { throw "请先安装 $command 并加入 PATH。" }
}
$config = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'blog-config.json') -Raw -Encoding utf8 | ConvertFrom-Json
if ($config.workerName -notmatch '^[a-z0-9][a-z0-9-]*[a-z0-9]$') { throw 'workerName 必须使用小写字母、数字和连字符。' }
if (-not $config.customDomain -or $config.customDomain -eq 'blog.example.com' -or $config.customDomain -notmatch '^[a-zA-Z0-9.-]+$') { throw '请在 blog-config.json 中填写自己的博客域名。' }
$output = Join-Path $PSScriptRoot '.cloudflare\output\v0'
$worker = Join-Path $output 'workers\default'
$assets = Join-Path $worker 'assets'
New-Item -ItemType Directory -Force -Path $worker | Out-Null
@{ buildContext = @{ isPreview = $false } } | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath (Join-Path $output 'config.json') -Encoding utf8
@{
name = $config.workerName
compatibilityDate = '2026-10-29'
domains = @($config.customDomain)
assets = @{ notFoundHandling = '404-page' }
} | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath (Join-Path $worker 'worker.config.json') -Encoding utf8
$stagingRoot = Join-Path $PSScriptRoot '.cloudflare\staging'
New-Item -ItemType Directory -Force -Path $stagingRoot | Out-Null
$staging = Join-Path $stagingRoot ([guid]::NewGuid().ToString('N'))
try {
& (Join-Path $PSScriptRoot 'prepare-content.ps1') -Destination $staging
$baseURL = if ($Preview) { 'http://localhost:1313/' } else { "https://$($config.customDomain)/" }
& hugo --minify --cleanDestinationDir --contentDir $staging --destination $assets --baseURL $baseURL
if ($LASTEXITCODE -ne 0) { throw 'Hugo 构建失败。' }
} finally {
if (Test-Path -LiteralPath $staging) {
$resolvedStaging = (Resolve-Path -LiteralPath $staging).Path
$resolvedRoot = (Resolve-Path -LiteralPath $stagingRoot).Path
if ([IO.Path]::GetDirectoryName($resolvedStaging) -ne $resolvedRoot) { throw '临时目录不在预期位置。' }
Remove-Item -LiteralPath $resolvedStaging -Recurse -Force
}
}
prepare-content.ps1:准备 Markdown
该脚本处理临时副本中的 Markdown,补充缺失标题,并保留源文件不变:
param(
[Parameter(Mandatory = $true)][string]$Destination,
[string]$Source = (Join-Path $PSScriptRoot 'content')
)
$ErrorActionPreference = 'Stop'
New-Item -ItemType Directory -Force -Path $Destination | Out-Null
Copy-Item -Path (Join-Path $Source '*') -Destination $Destination -Recurse -Force
# Add titles only to the temporary build copy; the original Markdown remains untouched.
Get-ChildItem -LiteralPath (Join-Path $Destination 'post') -Filter '*.md' -File -Recurse | Where-Object { $_.Name -ne '_index.md' } | ForEach-Object {
$file = $_
$modified = $file.LastWriteTimeUtc
$document = Get-Content -LiteralPath $file.FullName -Raw -Encoding utf8
if ($null -eq $document) { $document = '' }
$title = if ($file.BaseName -eq 'index') { $file.Directory.Name } else { $file.BaseName }
$quotedTitle = ConvertTo-Json -InputObject $title -Compress
if ($document -match '(?s)\A---\s*\r?\n(.*?)\r?\n---(?:\r?\n|$)') {
$frontMatter = $Matches[1]
if ($frontMatter -notmatch '(?im)^title\s*:') {
$document = [regex]::Replace($document, '\A---\s*\r?\n', "---`ntitle: $quotedTitle`n", 1)
}
} elseif ($document -match '(?s)\A\+\+\+\s*\r?\n(.*?)\r?\n\+\+\+(?:\r?\n|$)') {
$frontMatter = $Matches[1]
if ($frontMatter -notmatch '(?im)^title\s*=') {
$document = [regex]::Replace($document, '\A\+\+\+\s*\r?\n', "+++`ntitle = $quotedTitle`n", 1)
}
} elseif ($document -notmatch '(?s)\A\s*\{.*?\}\s*\r?\n') {
$heading = [regex]::Match($document, '\A\s*#\s+([^\r\n]+)\s*(?:\r?\n|$)')
if ($heading.Success) {
$quotedTitle = ConvertTo-Json -InputObject $heading.Groups[1].Value.Trim() -Compress
$document = $document.Substring($heading.Length)
}
$document = "---`ntitle: $quotedTitle`n---`n`n$document"
}
Set-Content -LiteralPath $file.FullName -Value $document -Encoding utf8 -NoNewline
$file.LastWriteTimeUtc = $modified
}
hugo.toml:站点配置
该文件启用 Prism、中文和文章目录;正式构建时,build.ps1 用你在 blog-config.json 填写的域名覆盖示例 baseURL:
baseURL = 'https://example.com/'
defaultContentLanguage = 'zh'
hasCJKLanguage = true
title = '我的博客'
theme = 'prism'
[languages.zh]
label = '简体中文'
locale = 'zh-CN'
[params]
mainSections = ['post']
description = '记录与分享'
rssFullContent = true
[params.Default.Hero]
Blurry = true
Vibrant = true
[params.footer]
since = 2026
[taxonomies]
category = 'categories'
tag = 'tags'
[frontmatter]
date = ['date', 'publishDate', ':filename', ':fileModTime']
[markup]
_merge = 'deep'
[markup.goldmark.renderer]
unsafe = false
[[menu.main]]
identifier = 'home'
name = '首页'
url = '/'
weight = 10
构建输出是什么
示例的 build.ps1 会生成 Cloudflare CLI 需要的目录:
.cloudflare/output/v0/
├── config.json
└── workers/default/
├── worker.config.json
└── assets/ # Hugo 生成的 HTML、CSS、图片等
其中 config.json 的内容是:
{"buildContext":{"isPreview":false}}
worker.config.json 由脚本根据 blog-config.json 生成,结构类似下面的示例;名称和域名会替换成读者自己配置的值:
{
"name": "my-blog",
"compatibilityDate": "2026-10-29",
"domains": ["blog.example.com"],
"assets": {"notFoundHandling": "404-page"}
}
--prebuilt 告诉 CLI 使用这些已经生成的文件。这里展示的是本教程所验证的 CLI 0.15.0 和 Build Output v0 格式;Cloudflare CLI 与该格式仍在发展,后续版本如果改变格式,脚本可能需要调整。有关当前行为可查看 Cloudflare CLI 项目文档。
日常更新
以后新增或修改文章,只需保存 Markdown,然后再次运行 .\deploy.ps1;也可以把文章交给助手,由助手完成发布。保存文件本身不会自动上线,这个示例没有配置 Git 推送后的自动部署。
