使用 Cloudflare R2 作为博客相册图床
最近给博客加了一个「光影集」相册页面, 照片存储在 Cloudflare R2 上, 每次推送代码时 GitHub Actions 会自动从 R2 拉取图片列表并生成相册数据, 整个流程无需手动维护. 这篇文章记录一下完整的配置过程.
为什么选择 Cloudflare R2
博客托管在 GitHub Pages 上, 如果把大量照片直接放进仓库, 会导致仓库体积膨胀, clone 和构建都会变慢. 常见的图床方案有很多, 但 Cloudflare R2 有几个比较吸引我的点:
- 免费额度足够个人使用: 每月 10 GB 存储, 1000 万次 Class A 操作, 1000 万次 Class B 操作
- 零出口流量费用: 不像 AWS S3 那样按流量收费
- S3 兼容 API: 可以直接用 AWS CLI 操作, 生态工具丰富
- 自带 CDN: Cloudflare 的全球网络本身就是 CDN
整体架构
整个相册的工作流程如下:
- 手动将照片上传到 Cloudflare R2 Bucket
- 推送代码到 GitHub 触发 Actions
- Actions 中的脚本通过 S3 API 列出 R2 中的所有图片
- 脚本生成
_data/gallery.yml数据文件 - Jekyll 构建时读取该数据文件, 渲染相册页面
- 部署到 GitHub Pages
第一步: 创建 Cloudflare R2 Bucket
注册 Cloudflare 账号
如果还没有 Cloudflare 账号, 前往 Cloudflare Dashboard 注册一个.
创建 Bucket
- 登录 Cloudflare Dashboard, 在左侧导航栏找到 R2 对象存储
- 点击 创建存储桶
- 输入存储桶名称, 比如
my-blog-gallery - 选择一个离你较近的区域 (如果不确定就选自动)
- 点击 创建存储桶
配置公开访问
相册图片需要能被公开访问, R2 提供两种方式:
方式一: 使用 R2.dev 子域名 (简单快速)
- 进入刚创建的存储桶, 点击 设置 标签
- 找到 公开访问 部分, 启用 R2.dev 子域名
- 确认后会得到一个类似
https://pub-xxxxxxxx.r2.dev的公开 URL
方式二: 绑定自定义域名 (推荐)
如果你有自己的域名并且已经托管在 Cloudflare 上:
- 在存储桶的 设置 中, 找到 自定义域名
- 点击 连接域名, 输入你想使用的子域名, 比如
img.yourdomain.com - Cloudflare 会自动配置 DNS 记录和 SSL 证书
自定义域名的好处是 URL 更简洁, 而且自带 Cloudflare CDN 缓存加速.
上传照片
你可以通过以下方式上传照片到 Bucket:
- Cloudflare Dashboard 网页端直接拖拽上传
- 使用 AWS CLI (因为 R2 兼容 S3 API)
- 使用 rclone 等第三方工具
使用 AWS CLI 上传示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 配置 AWS CLI (使用 R2 的 Access Key)
aws configure --profile r2
# 输入 Access Key ID 和 Secret Access Key
# Region 填 auto
# Output format 填 json
# 上传单张照片
aws s3 cp photo.jpg s3://my-blog-gallery/ \
--endpoint-url https://<ACCOUNT_ID>.r2.cloudflarestorage.com \
--profile r2
# 批量上传整个文件夹
aws s3 sync ./photos/ s3://my-blog-gallery/ \
--endpoint-url https://<ACCOUNT_ID>.r2.cloudflarestorage.com \
--profile r2
第二步: 创建 R2 API Token
GitHub Actions 需要通过 API 访问 R2, 所以我们需要创建一个 API Token.
- 在 Cloudflare Dashboard 左侧导航栏, 点击 R2 对象存储
- 点击 管理 R2 API 令牌
- 点击 创建 API 令牌
- 配置令牌:
- 令牌名称: 比如
github-actions-gallery - 权限: 选择 对象读取 (只需要读取权限即可)
- 指定存储桶: 选择你刚创建的存储桶 (最小权限原则)
- 令牌名称: 比如
- 点击 创建 API 令牌
- 记录下生成的 Access Key ID 和 Secret Access Key (只会显示一次)
同时记下你的 Cloudflare Account ID, 可以在 Dashboard 右侧边栏或 URL 中找到.
第三步: 配置 GitHub Secrets
在 GitHub 仓库中配置 Actions 所需的密钥:
- 进入仓库的 Settings → Secrets and variables → Actions
- 点击 New repository secret, 依次添加以下 Secrets:
| Secret 名称 | 值 |
|---|---|
R2_ACCOUNT_ID | Cloudflare Account ID |
R2_ACCESS_KEY_ID | R2 API Token 的 Access Key ID |
R2_SECRET_ACCESS_KEY | R2 API Token 的 Secret Access Key |
R2_BUCKET_NAME | 存储桶名称, 如 my-blog-gallery |
R2_PUBLIC_URL | 公开访问 URL, 如 https://pub-xxx.r2.dev |
第四步: 编写相册生成脚本
这个脚本是整个流程的核心, 它负责从 R2 获取图片列表并生成 Jekyll 数据文件.
创建 scripts/generate-gallery-r2.sh:
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#!/bin/bash
# 从 Cloudflare R2 自动获取图片列表,生成 _data/gallery.yml
set -euo pipefail
# 从 _config.yml 读取默认值(如果环境变量未设置)
CONFIG_FILE="_config.yml"
R2_PUBLIC_URL="${R2_PUBLIC_URL:-$(grep -A5 'r2_gallery:' "$CONFIG_FILE" \
| grep 'public_url:' | sed 's/.*public_url: *"\(.*\)"/\1/' | tr -d ' ')}"
R2_PREFIX="${R2_PREFIX:-$(grep -A5 'r2_gallery:' "$CONFIG_FILE" \
| grep 'prefix:' | sed 's/.*prefix: *"\(.*\)"/\1/' | tr -d ' ')}"
# 必须的环境变量检查
: "${R2_ACCOUNT_ID:?请设置 R2_ACCOUNT_ID 环境变量}"
: "${R2_ACCESS_KEY_ID:?请设置 R2_ACCESS_KEY_ID 环境变量}"
: "${R2_SECRET_ACCESS_KEY:?请设置 R2_SECRET_ACCESS_KEY 环境变量}"
: "${R2_BUCKET_NAME:?请设置 R2_BUCKET_NAME 环境变量}"
: "${R2_PUBLIC_URL:?请设置 R2_PUBLIC_URL 环境变量或在 _config.yml 中配置}"
OUTPUT="_data/gallery.yml"
ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"
SUPPORTED_EXT="jpg|jpeg|png|gif|webp|avif"
echo "# 此文件由 scripts/generate-gallery-r2.sh 自动生成,请勿手动编辑" > "$OUTPUT"
echo "# 图片来源: ${R2_PUBLIC_URL}/${R2_PREFIX}" >> "$OUTPUT"
echo "" >> "$OUTPUT"
echo "正在从 R2 获取图片列表..."
# 使用 AWS CLI(S3 兼容)列出对象
aws s3api list-objects-v2 \
--bucket "$R2_BUCKET_NAME" \
--prefix "$R2_PREFIX" \
--endpoint-url "$ENDPOINT" \
--query "Contents[].{Key: Key, LastModified: LastModified, Size: Size}" \
--output json 2>/dev/null | \
python3 -c "
import json, sys, re
data = json.load(sys.stdin)
if not data:
sys.exit(0)
ext_pattern = re.compile(r'\.($SUPPORTED_EXT)$', re.IGNORECASE)
photos = [item for item in data if ext_pattern.search(item['Key'])]
photos.sort(key=lambda x: x['LastModified'], reverse=True)
for photo in photos:
key = photo['Key']
filename = key.rsplit('/', 1)[-1]
name = filename.rsplit('.', 1)[0]
title = name.replace('-', ' ').replace('_', ' ')
url = '${R2_PUBLIC_URL}/' + key
print(f'- title: \"{title}\"')
print(f' image: \"{url}\"')
print(f' key: \"{key}\"')
print()
print(f'# 共 {len(photos)} 张照片', file=sys.stderr)
" >> "$OUTPUT" 2>&1
COUNT=$(grep -c '^- title:' "$OUTPUT" 2>/dev/null || echo "0")
echo "Gallery 生成完成: 共 ${COUNT} 张照片"
脚本的工作逻辑:
- 从环境变量或
_config.yml读取 R2 配置 - 通过
aws s3api list-objects-v2列出 Bucket 中的所有对象 (R2 兼容 S3 API) - 用 Python 过滤出图片文件 (支持 jpg/png/gif/webp/avif), 按修改时间倒序排列
- 从文件名自动生成标题, 拼接完整的公开 URL
- 输出为 YAML 格式写入
_data/gallery.yml
生成的 _data/gallery.yml 格式如下:
1
2
3
4
5
6
7
- title: "sunset over the sea"
image: "https://pub-xxx.r2.dev/sunset-over-the-sea.jpg"
key: "sunset-over-the-sea.jpg"
- title: "mountain view"
image: "https://pub-xxx.r2.dev/mountain-view.png"
key: "mountain-view.png"
第五步: 配置 Jekyll
在 _config.yml 中添加 R2 相册的配置:
1
2
3
4
# Cloudflare R2 Gallery Settings
r2_gallery:
public_url: "https://pub-xxxxxxxx.r2.dev"
prefix: ""
public_url: 你的 R2 公开访问域名, 不要以/结尾prefix: Bucket 中相册图片的前缀路径, 如果照片直接放在根目录就留空, 如果放在子目录比如gallery/就填gallery/
第六步: 创建相册页面
创建 _tabs/gallery.md 作为相册的展示页面:
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
---
title: 光影集
icon: fas fa-camera-retro
order: 5
---
<style>
.g-item { margin-bottom: 12px; cursor: pointer; }
.g-item img { width: 100%; border-radius: 6px; display: block; }
.lb { display:none; position:fixed; inset:0; z-index:9999;
background:rgba(0,0,0,0.9); justify-content:center; align-items:center; }
.lb.on { display:flex; }
.lb img { max-width:92vw; max-height:90vh; border-radius:6px; }
.lb-x { position:absolute; top:12px; right:16px; background:none;
border:none; color:#fff; font-size:2rem; cursor:pointer; }
</style>
<div class="lb" id="lb" onclick="closeLB()">
<button class="lb-x" aria-label="关闭">×</button>
<img id="lb-img" alt="" onclick="event.stopPropagation()" />
</div>
{% if site.data.gallery.size > 0 %}
{% for photo in site.data.gallery %}
<div class="g-item" onclick="openLB('{{ photo.image }}')">
<img src="{{ photo.image }}" alt="照片" loading="lazy" />
</div>
{% endfor %}
{% else %}
<p class="text-muted text-center mt-5">📷 还没有照片</p>
{% endif %}
<script>
var lb=document.getElementById('lb'), lbImg=document.getElementById('lb-img');
function openLB(s){lbImg.src=s;lb.classList.add('on');document.body.style.overflow='hidden';}
function closeLB(){lb.classList.remove('on');document.body.style.overflow='';}
document.addEventListener('keydown',function(e){if(e.key==='Escape')closeLB();});
</script>
这个页面的要点:
- 使用 Chirpy 主题的
_tabs机制, 会自动出现在侧边栏导航中 order: 5控制在导航栏中的排列顺序- 通过 Liquid 模板遍历
site.data.gallery渲染图片列表 - 内置了一个简单的 Lightbox 效果, 点击图片可以全屏查看
- 支持
Escape键关闭 Lightbox - 图片使用
loading="lazy"实现懒加载, 避免一次性加载所有图片
第七步: 配置 GitHub Actions
在 .github/workflows/pages-deploy.yml 中, 需要在 Jekyll 构建之前添加相册生成步骤:
1
2
3
4
5
6
7
8
9
10
11
12
13
- name: Generate gallery from R2
env:
R2_ACCOUNT_ID: $
R2_ACCESS_KEY_ID: $
R2_SECRET_ACCESS_KEY: $
R2_BUCKET_NAME: $
R2_PUBLIC_URL: $
AWS_ACCESS_KEY_ID: $
AWS_SECRET_ACCESS_KEY: $
AWS_DEFAULT_REGION: auto
run: |
pip install awscli --quiet
bash scripts/generate-gallery-r2.sh
注意几个关键点:
- 这个步骤必须放在
Build site之前, 因为 Jekyll 构建时需要读取生成的_data/gallery.yml AWS_ACCESS_KEY_ID和AWS_SECRET_ACCESS_KEY是给 AWS CLI 用的, 值和 R2 的 Key 相同AWS_DEFAULT_REGION设为auto, 这是 R2 的要求- 先安装
awscli, 因为 GitHub Actions 的 Ubuntu runner 默认没有预装
完整的 workflow 执行顺序:
1
Checkout → Setup Pages → Setup Ruby → Generate gallery from R2 → Build site → Test site → Upload → Deploy
日常使用流程
配置完成后, 日常添加照片的流程非常简单:
- 将照片上传到 R2 Bucket (通过 Dashboard 或 CLI)
- 推送任意代码改动到
main分支 (或手动触发 workflow) - GitHub Actions 自动拉取最新图片列表, 构建并部署
如果只是想更新相册而不改代码, 可以在 GitHub 仓库的 Actions 页面手动触发 workflow (因为配置了 workflow_dispatch).