项目概览
这个站点采用 Hugo 静态生成 + PaperMod 主题 + Cloudflare Pages 免费部署 的技术栈,从域名注册到上线只用了一个下午。本文逐层拆解架构设计和自定义魔改。
技术选型
| 层级 | 技术 | 理由 |
|---|---|---|
| 静态生成 | Hugo v0.164 | Go 实现,构建 200ms,单二进制 |
| 主题 | PaperMod | 响应式、内置搜索/暗色模式/SEO |
| 托管 | Cloudflare Pages | 免费、全球 CDN、push 即部署 |
| 域名 | luostratus.cn | 腾讯云注册 → Cloudflare DNS 接管 |
| 代码托管 | GitHub | 免备案最优解 |
.cn 域名托管在境外服务器无需 ICP 备案,这是选择 Cloudflare Pages 的关键原因。
项目结构
luostratus.cn/
├── hugo.yaml # 站点配置
├── archetypes/
│ ├── default.md # 普通文章模板
│ └── gallery.md # 画廊模板
├── content/
│ ├── about.md
│ ├── posts/ # 博客文章
│ └── gallery/
│ ├── _index.md # 画廊主页(branch bundle)
│ └── *.png / *.jpg # 图片资源(bundle 成员)
└── layouts/
├── gallery/
│ ├── list.html # ★ 画廊核心:排序 + 瀑布流
│ └── single.html # 画廊详情
└── partials/
├── extend_head.html # 全站 CSS + 动效
└── extend_footer.html # 返回顶部 + 导航 JS
PaperMod 提供 extend_head 和 extend_footer 扩展点,覆盖这两个 partial 即可注入自定义样式与脚本,不修改主题一行代码,升级主题零冲突。
hugo.yaml 关键配置
baseURL: https://luostratus.cn/
locale: zh-cn
theme: PaperMod
# 分页 —— 注意版本兼容性
pagination:
pagerSize: 10
# 代码高亮
markup:
highlight:
style: catppuccin-macchiato
noClasses: false
# JSON 输出是 PaperMod 搜索功能必需的
outputs:
home:
- HTML
- RSS
- JSON
params:
ShowReadingTime: true
ShowToc: true
ShowBreadCrumbs: true
ShowCodeCopyButtons: true
ShowPostNavLinks: true
dateFormat: "2006-01-02" # Go 诞生日 = YYYY-MM-DD
部署踩坑
Cloudflare Pages 的 Hugo 版本(v0.147.7)比本地(v0.164.0)更旧。起初 paginate: 10 直接导致构建报错:
ERROR deprecated: site config key paginate was deprecated in Hugo v0.128.0
and subsequently removed. Use pagination.pagerSize instead.
改为 pagination.pagerSize 后解决。CI 环境与本地版本不一致 是静态站点部署中最常见的坑,后续可在 Cloudflare 环境变量中锁定 HUGO_VERSION。
画廊:核心自定义模块
画廊是这个站点最复杂的定制部分——单页展示、零子页面、CSS 瀑布流、智能排序。
Branch Bundle 设计
content/gallery/
├── _index.md ← Branch Bundle,可通过 .Resources 访问所有文件
├── 00-校色卡.png ← 固定首位
├── 2026-07-01_15.23.21.png
├── IMG_20250712_212757.jpg
└── ...
Hugo 的 Branch Bundle 允许 _index.md 通过 .Resources.ByType "image" 直接获取同目录下所有图片。不需要子文件夹、不要二级页面,用户进入即见所有图片。
文件名日期解析
34 张图片来源各异——截图、手机照片、无人机、微信——命名规范完全不同。模板中用 两层正则 逐级匹配:
{{ $dateStr := "—" }}
// 第一层:匹配 YYYY-MM-DD 格式(截图类文件)
{{ $m1 := findRE `^(\d{4}-\d{2}-\d{2})` $filename 1 }}
// 第二层:匹配 20xxMMDD 格式
// 关键是用 "20" 开头限定,避开 Unix 时间戳
{{ $m2 := findRE `(20\d{6})` $filename 1 }}
各文件名处理结果:
| 文件名 | 匹配层 | 提取日期 |
|---|---|---|
2026-07-01_15.23.21.png |
第一层 ^\d{4}-\d{2}-\d{2} |
2026-07-01 |
IMG_20250712_212757.jpg |
第二层 20\d{6} |
2025-07-12 |
DJI_20241111063817...jpg |
第二层 20\d{6} |
2024-11-11 |
mmexport..._20230511_*.jpg |
第二层 20\d{6} |
2023-05-11 |
1684758526619.png |
无匹配 | — |
第二层用 20\d{6} 而非 \d{8} 是关键:mmexport1682250305721_20230511_... 中第一个 8 位数字是时间戳的前缀 16822503,只有用 20 限定前缀才能命中真正的日期 20230511。
排序算法
输入:34 张图片(含日期的 N 张 + 无日期的 M 张 + 1 张固定图)
1. 分离
├── $pinned ← .Resources.GetMatch "00-校色卡*"
├── $dated ← 有日期的图片(格式化为 {r: resource, d: "2026-07-01"})
└── $undated ← 无日期的图片
2. 排序
├── $dated = sort $dated "d" "desc" // 新 → 旧
└── $undated = shuffle $undated // 随机
3. 穿插间隔
├── $gap = len($dated) / (len($undated) + 1)
└── 每隔 $gap 张已排序图片,插入 1 张无日期图片
4. 渲染
├── 首先渲染 $pinned(固定首位,不参与排序)
└── 循环渲染排序后的穿插序列
Hugo 模板的一个硬限制是 无法在 range 循环内修改外部变量。为此使用了 .Scratch 来维护计数状态:
{{ $.Scratch.Set "di" 0 }} // 初始化排序计数
{{ $.Scratch.Set "ui" 0 }} // 初始化无日期计数
{{ $.Scratch.Set "slot" $gap }} // 下一次插入槽位
{{ range $idx := seq (add $dt $ut) }}
{{ $di := $.Scratch.Get "di" }} // 读取当前状态
{{ $ui := $.Scratch.Get "ui" }}
...
{{ $.Scratch.Set "di" (add $di 1) }} // 更新状态(仅在 Scratch 作用域内)
{{ end }}
.Scratch 是 Hugo 模板中实现可变状态的唯一标准模式——虽然写法不够优雅,但能在不引入自定义 shortcode 或 data file 的前提下完成复杂逻辑。
CSS 瀑布流
.gallery-masonry {
column-count: 2; /* CSS3 多列布局,天然瀑布流 */
column-gap: 20px;
}
.gallery-item {
break-inside: avoid; /* 防止图片在列间断裂 */
}
.gallery-item img {
width: 100%;
height: auto; /* 保持原始宽高比,不裁切 */
border-radius: 8px;
cursor: zoom-in;
}
选择 column-count 而非 Grid/Flexbox 的原因:
- 天然错位:不需要计算每张图的高度,浏览器自动排列
- 零依赖:无需 Masonry.js(~9KB gzipped)
- 移动端自适应:
column-count: 1一行搞定
点击放大:零 DOM 方案
// 直接 toggle class,不需要额外的 overlay 元素
onclick="this.classList.toggle('zoomed')"
/* 放大状态:fixed 定位 + 超大阴影模拟遮罩 */
.gallery-item img.zoomed {
position: fixed;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
max-width: 92vw; max-height: 92vh;
box-shadow: 0 0 0 2000px rgba(0,0,0,0.8); /* 遮罩 */
z-index: 1000;
cursor: zoom-out;
}
这个方案的妙处在于:不需要创建 overlay div、不需要管理 body 滚动锁定、不需要 addEventListener,纯粹的声明式交互。一个 class 切换完成全部效果。
全站交互效果
导航菜单
导航是交互设计最密集的区域:
/* 类按钮化的链接 */
#menu a {
padding: 6px 14px;
border-radius: 8px;
transition: background 0.25s, color 0.25s, transform 0.2s;
}
/* hover:半透明底色浮现 */
#menu a:hover {
background: color-mix(in srgb, var(--primary) 8%, transparent);
transform: translateY(-1px);
}
/* 当前页:底部指示横条 */
#menu a[aria-current="page"]::after {
content: '';
width: 18px; height: 3px;
border-radius: 2px;
background: var(--primary);
transition: width 0.3s; /* hover 时向外伸展 */
}
当前页标记通过 JS 动态注入:
// 对比 URL 路径,标记匹配的导航项
const currentPath = window.location.pathname.replace(/\/$/, '') || '/';
document.querySelectorAll('#menu a').forEach(link => {
const linkPath = new URL(link.href).pathname.replace(/\/$/, '') || '/';
if (linkPath === currentPath) {
link.setAttribute('aria-current', 'page');
}
});
交互效果一览
| 效果 | 技术 |
|---|---|
| 页面入场动画 | @keyframes fadeIn 0.4s |
| 图片懒加载淡入 | opacity: 0→1,onload 触发 |
| 链接下划线滑入 | background-size: 0%→100% |
| 文章卡片抬起 | translateY(-3px) + 阴影 |
| 返回顶部按钮 | IntersectionObserver + scrollTo({behavior:'smooth'}) |
| 暗色模式过渡 | 全元素 transition: background-color/color/border-color 0.3s |
| 按钮点击反馈 | :active { transform: scale(0.96) } |
| 代码块 hover | 阴影浮现 |
| 选中文字 | ::selection 自定义配色 |
所有效果均为 纯 CSS + 少量原生 JS,零外部依赖,总计不到 200 行代码。
部署管线
本地编写 → git push → GitHub
↓
Cloudflare Pages 自动触发
↓
Hugo build(~1min)
↓
全球 330+ 节点分发
↓
luostratus.cn 可访问
全链路零成本:Hugo 开源、GitHub 免费、Cloudflare Pages 免费(无限带宽 + 无限请求)。
可优化方向
- 图片压缩:34 张原图约 40MB,可用 Hugo Image Processing 自动生成 WebP 缩略图
- 画廊分页:图片增多后可加无限滚动
- 评论系统:Giscus(基于 GitHub Discussions,免费无追踪)
- CI 版本锁定:在 Cloudflare 环境变量中设置
HUGO_VERSION - Open Graph 优化:为画廊页添加社交分享预览图