Hugo

Hugo + PicList 多图床搭建实践

Hugo 写作时,Markdown 通过路径或 URL 引用图片,不直接管理图片资源。随着文章和图片增多,可借助 PicList 统一处理上传与链接生成,由图床负责存储访问。

写作与渲染流程
写作与渲染流程

下面以 Typora + PicList + Hugo 为例,介绍一种可在本地与云端灵活切换的图片写作方案,并分别说明本地、GitHub 与 Cloudflare R2 三种低成本图床的配置方式。

准备工作

在开始之前,准备以下工具:

  • PicList:用于图床图片的上传与管理,需提前配置好目标图床。
  • Typora:Markdown 编辑器,用于编辑与预览文章内容。
  • Git(可选):用于 GitHub 图床的同步及博客的版本管理。
  • VS Code(可选):用于博客项目管理及 Markdown 内容编辑。

搭建图床

本地图床

本地图床是一种简单且低成本的图床方案,PicList 支持将本地文件夹作为图床目录,提供三种处理方式:

  • 预处理:上传图片至本地目录,Hugo 构建时修正路径,适合长期写作和维护。
  • 后处理:上传图片生成绝对路径,Hugo 引用外链,配置成本最低,但本地无法预览。
  • 手动处理:Typora 将图片保存到指定目录,依赖人工管理,不适合图片数量多的项目。

图片一般存放于 assetsstatic 目录 —— assets 适合需要编译、压缩或转换的资源;static 直接复制到 public 目录,适合存放原始图片。

GitHub 图床

GitHub 图床通过代码仓库存储图片,PicList 自动上传生成外链,并可配合 CDN 加速。

打开 GitHub,点击 +,创建 New Repository

  • Repository name:如 hugo-img
  • Configuration:勾选 Add a README.md,其余保持默认设置 → 点击 Create Repository

点击头像 > Settings > Developer Settings > Personal access tokens,创建 Generate new token(classic)

  • Note:如 Typora-PicList
  • Expiration:No expiration
  • Select scopes:勾选 repo 权限。

生成并保存 Token(关闭页面后无法再次查看)。

Cloudflare R2

Cloudflare R2 是兼容 S3 的对象存储,支持自定义域名和全球 CDN,零流量费,提供一定免费额度,超出部分按需付费。1

以下是必需的操作

点击 存储和数据库 > R2 对象存储 > 绑定支付方式 > 创建存储桶(如 blog),选择亚太地区

其中,支付方式支持绑定 Visa 或 PayPal。如果无信用卡,可使用 PayPal 绑定国内储蓄卡完成验证,之后可解除绑定。

R2 存储桶提供的公共访问链接受服务器限制,几乎无法访问,建议绑定自定义域名使用。

购买并实名认证域名 (如 阿里云万网)后,将域名接入 Cloudflare:

  • 打开 Cloudflare,点击 添加域,输入域名(如 example.com),选择 Free 套餐,记录其分配的两条名称服务器。
  • 打开 阿里云域名控制台,点击 域名列表 > 选择域名 > DNS 修改,将名称服务器替换为 Cloudflare 提供的两条地址。
  • 打开 R2 对象存储,选择存储桶,点击 设置,启用 公共开发 URL,并 添加自定义域(如 img.example.com)。

完成后,关闭 公共开发 URL

API 密钥将用于 PicList 配置中,确保图片能够正常上传。

打开 R2 对象存储,点击 API Tokens > Manage,创建 Account API 令牌:

  • 权限:对象读与写。
  • 其他设置:保持默认。

创建后,保存 AccessKey IDSecretAccessKey(关闭页面后不可再次查看)。

以下是可选的操作

通过防盗链限制仅允许站点自身引用图片,避免无效流量消耗。

打开 ,选择域名,点击 安全规则,创建 自定义规则

  • 规则名称:防盗链保护。
  • 表达式:(http.host eq "img.example.com" and not http.referer contains "example.com" and http.referer ne "" and not http.referer contains "http://localhost:1313")
  • 选择操作:阻止。

其中,img.example.com 为 R2 存储桶自定义子域,example.com 为站点主域名。

边缘缓存启用后,请求将优先由 Cloudflare 全球 CDN 响应,减少回源请求并降低费用。

打开 ,选择域名,点击 规则,创建 页面规则

  • URL:R2 存储桶自定义子域(如 img.example.com/*)。
  • 设置:缓存级别 - 缓存所有内容;边缘缓存 TTL - 1 个月;浏览器缓存 TTL - 8 小时。

通过配置 Cache-Control 响应头,进一步控制 CDN 与浏览器缓存行为。

打开 ,选择域名,点击 规则 > 概述,创建 响应标头转换规则

  • 规则名称:图片缓存设置。
  • 如果传入请求匹配:自定义筛选表达式 → 主机名等于存储桶自定义子域(如 img.example.com)。
  • 则:添加静态 → Cache-Control = publicmax-age=31536000, immutable

当博客域名与图床域名不一致时,需要配置 CORS 以避免浏览器跨域拦截。

打开 R2 对象存储,选择存储桶,点击 设置 > CORS 策略 > 编辑(记得替换 example.com):

Plaintext
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[
  {
    "AllowedOrigins": [
      "http://localhost:1313",
      "https://example.com",
      "https://*.example.com"
    ],
    "AllowedMethods": ["GET"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": ["Content-Length", "ETag"],
    "MaxAgeSeconds": 86400
  }
]

用量通知可用于监控异常访问或流量激增,避免意外费用。

打开 Cloudflare 控制台,点击 管理账户 > 通知 > 添加通知 > 选择账单

  • 通知名称:R2 用量警报。
  • 产品:R2 Storage Class B Operations。
  • 总数 number of Storage Class B Operations 超过时发出通知:50000。

配置 PicList

打开 PicList,点击 设置 > 上传,建议启用以下选项:

  • 相册内删除时同步删除云端文件:自动清理无用图片,避免图床残留。
  • 图片预处理设置:压缩质量 85%,格式转换为 webp,在保证清晰度的同时减少体积。
  • 高级重命名:建议使用 {Y}{m}{d}_{str-6} 规则,避免文件重名冲突。
  • 上传后删除本地文件:上传成功后自动删除本地文件,保持本地整洁。

点击 图床 > 本地上传 > 新增图床配置(按实际使用场景选择),完成后点击保存并设为默认图床。

如采用 预处理 模式,则 自定义域名和网站路径需留空,由 Hugo 模板在构建阶段补全。

GitHub 原始域名 raw.githubusercontent.com 在国内访问较慢,如需加速可填入自定义域名,如:

  • jsDelivr CDN:如 https://cdn.jsdelivr.net/gh/<username>/<reponame>@<branch>/
  • Cloudflare CDN:将仓库部署到 Pages 并绑定域名(如 https://img.example.com)。

在 PicList 中,Cloudflare R2 通过 AWS S3 协议接入,可直接按 S3 图床方式配置。

其中,<bucket_id>S3 API,位于 R2 > 存储桶 > 设置 > 常规

配置 Typora

打开 Typora,点击 文件 > 偏好设置 > 图像,勾选以下选项:

  • 插入图片时:选择上传图片,并勾选下方三个选项。
  • 上传服务:选择 PicList,并设置 PicList 可执行文件路径。
  • 图片语法偏好:仅在使用本地预处理时勾选 优先使用相对路径,其余无需勾选。

配置完成后,点击 验证图片上传选项,确认图片可正常上传。

上传后图片路径示例如下:

Markdown
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
本地图床(预处理)
![示例图](../../assets/images/20250706_abc123.webp "示例图")

本地图床(后处理)
![示例图](https://example.com/images/20250706_abc123.webp "示例图")

GitHub 图床
![图片原名称](https://raw.githubusercontent.com/<username>/<reponame>/main/images/2025/图片新名称.png "图片原名称")

Cloudflare R2
![图片原名称](http://img.example.com/images/2025/图片新名称.png "图片原名称")

⚠️ 本地预处理方式的路径修正:

本地预处理下 Typora 图片路径(如 ../../assets/images/...)能正常预览,但 Hugo 渲染时 无法正确解析,因为 assets 目录不参与 Markdown 路径解析,需通过 Hugo Render Hook 修正路径。

创建以下文件:

HTMLlayouts/_markup/render-image.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
{{- /* 覆盖来源:FixIt/layouts/_markup/render-image.html */ -}}

{{- /*
  Markdown image render hook.

  Components of a Markdown image:
    ![white kitten](/images/kitten.jpg "A kitten!")
    ------------  ------------------  ---------
    description      destination        title
    ------------  ------------------  ---------
    .Text            .Destination       .Title
*/ -}}
{{- $params := .Page.Params | merge site.Params.page -}}

{{- /* ======================== 修改点 1:开始 ======================== */ -}}
{{- /* 路径转换:解决 Typora 等编辑器生成的相对路径问题 */ -}}
{{- $fixedDestination := .Destination -}}

{{- /* 检测并转换 Typora 特有的 assets 相对路径格式 */ -}}
{{- if hasPrefix .Destination "../../../assets" -}}
  {{- $fixedDestination = replace .Destination "../../../assets" "" -}}
{{- else if hasPrefix .Destination "../assets" -}}
  {{- $fixedDestination = replace .Destination "../../assets" "" -}}
{{- end -}}

{{- /* 图片优化配置:响应式尺寸及 WebP 格式,质量 75% */ -}}
{{- $optim := slice
  (dict "Process" "resize 800x webp q75" "descriptor" "800w")
  (dict "Process" "resize 1200x webp q75" "descriptor" "1200w")
  (dict "Process" "resize 1600x webp q75" "descriptor" "1600w")
-}}

{{- /* 情况 1:图片同时包含标题(title)和说明(text),生成带 figcaption 的 figure 元素 */ -}}
{{- if .Title | and .Text -}}
  {{- $linked := ne $params.lightgallery false -}}
  <figure>
    {{- /* 使用转换后的路径调用图片组件 */ -}}
    {{- dict "Src" $fixedDestination "Alt" .PlainText "Title" .Title "Caption" .Text "Linked" $linked "Loading" "lazy" "Resources" .Page.Resources "OptimConfig" $optim | partial "plugin/image.html" -}}
    <figcaption class="image-caption">
      {{- .Title | safeHTML -}}
    </figcaption>
  </figure>

{{- /* 情况 2:普通图片(无标题或无说明),只生成 img 标签(可能支持 lightgallery) */ -}}
{{- else -}}
  {{- $linked := (eq $params.lightgallery "force") | or ($params.lightgallery | and (ne .Title "")) -}}
  {{- /* 使用转换后的路径调用图片组件 */ -}}
  {{- dict "Src" $fixedDestination "Alt" .PlainText "Title" .Title "Linked" $linked "Loading" "lazy" "Resources" .Page.Resources "OptimConfig" $optim | partial "plugin/image.html" -}}
{{- end -}}
{{- /* ======================== 修改点 1:结束 ======================== */ -}}
FixIt 2026.5.26 | 更改

💡 本地图床未引用图片的清理

本脚本仅适用于本地图床,用于扫描 assets/images/ 目录下所有 .webp 格式的图片,与 content/ 目录下 Markdown 文件中引用的图片进行比对,找出未被任何文章引用的图片,并在用户确认后删除它们。

创建以下文件:

Pythonscripts/clean-unused-images.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import os
import re
from pathlib import Path

assets_dir = Path("assets/images")
content_dir = Path("content")

# 获取所有 webp 文件名(不含路径)
all_images = {f.name for f in assets_dir.glob("*.webp")}

# 找到所有 content 下的 md 文件
used_images = set()
for md_file in content_dir.rglob("*.md"):
    text = md_file.read_text(encoding="utf-8")
    # 搜索文件名(作为独立子串)
    for img in all_images:
        if img in text:
            used_images.add(img)

unused = all_images - used_images

if unused:
    print("发现以下未引用图片:")
    for img in sorted(unused):
        print(f"  {img}")
    
    answer = input("\n是否删除这些图片?输入 yes 确认: ")
    if answer.lower() == "yes":
        for img in unused:
            img_path = assets_dir / img
            img_path.unlink()  # 删除文件
            print(f"已删除: {img}")
        print("删除完成。")
    else:
        print("已取消,未删除任何文件。")
else:
    print("没有未引用的图片。")

运行以下命令,执行脚本:

Bash
1
python scripts/clean-unused-images.py

图床迁移

至此,写作环境已配置完成,日常写作时只需在 Typora 中插入图片,提交代码即可自动完成上传。

如需图床迁移,详见《Hugo 图床迁移:从本地到云端》、《Hugo 图床迁移:从云端到本地》。

留言交流