上篇记了从零到本地预览的流程。这篇解决三个真正开始写站之后绕不开的问题:图片怎么 跟文章放在一起、主题模板想微调该改哪、以及怎么把站点发到自己的服务器上。

1. 用 Page Bundle 管理一篇文章的所有资源

Hugo 里一篇文章有两种放法。第一种是上篇那种单个文件 content/posts/hello.md;当文章 需要带图片、附件时,更推荐用 Page Bundle(页面包):把文章改成一个目录,里面放 index.md 和它用到的所有资源。

content/posts/
└── my-post/
    ├── index.md      # 文章正文(文件名必须是 index.md)
    ├── cover.png     # 封面
    └── images/
        └── shot.png  # 正文插图

Page Bundle 的目录结构

这样做的好处是:图片和文章是一个整体。正文里直接用相对路径引用即可,Hugo 会自动 找到同目录资源并处理:

![截图说明](images/shot.png)

封面则写在 front matter 里,PaperMod 主题会把它渲染成文章头图,还会自动生成多尺寸 响应式图片:

cover:
  image: "cover.png"
  alt: "封面说明"
  caption: "图注文字"

小坑:Bundle 目录里正文文件必须叫 index.md(叶子 Bundle)。图片放在 Bundle 内, 既能被 Markdown 相对路径引用,也能被 cover.image 匹配到。

2. 模板查找顺序:改主题,别动主题

直接去 themes/PaperMod/ 里改模板是下策——下次 git submodule update 升级主题就被 覆盖了。Hugo 的做法是在站点根目录的同名路径放一个文件去覆盖。

查找规则简单说就是:站点自己的 layouts/ 优先,找不到才去 themes/<主题>/layouts/。 所以想改页脚,不用动主题,只要新建:

layouts/
└── partials/
    └── footer.html     # 覆盖 themes/PaperMod/layouts/partials/footer.html

Hugo 渲染时会优先用你这份,主题升级也不受影响。我这个站就用这个方式加了自定义页脚 (备案信息之类)。几个最常覆盖的位置:

想改的东西覆盖路径(放在站点根 layouts/ 下)
页脚partials/footer.html
页头额外代码partials/extend_head.html
文章正文末尾partials/extend_post_content.html
单篇文章模板_default/single.html

3. 配置里的 taxonomy 与菜单

hugo.toml 里显式声明分类法后,tags、categories 这些页面才会按预期生成;菜单也 在这里配:

[taxonomies]
  category = "categories"
  tag = "tags"
  series = "series"

[menu]
  [[menu.main]]
    name = "文章"
    url = "/posts/"
    weight = 10

weight 决定菜单从左到右的顺序。改配置不需要重启,hugo server 会自动热加载。

4. 构建:生成纯静态文件

预览没问题后,一条命令产出可部署的静态站点:

hugo --minify

hugo –minify 生产构建输出

它会把所有 Markdown 渲染、套用模板、压缩 CSS/HTML,结果全部输出到 public/ 目录。 输出末尾的统计表格会告诉你生成了多少页面、处理了多少张图片——这个 public/ 就是 要上传到服务器的全部东西,里面没有任何需要服务器运行的程序。

--minify 会压缩产物;线上构建还建议保证配置里 baseURL 填的是真实域名,否则 RSS、站点地图里的绝对链接会不对。

5. 一键部署到自己的服务器

我的部署方式是:本地打包源码 → 上传 → 服务器上 hugo 构建 → 同步到 Nginx 根目录。 之所以在服务器上构建而不是本地构建,是为了保证环境一致,也省去本地传一大堆产物。 核心脚本长这样:

# 1. 本地打包源码(排除 public 和主题的 .git)
tar czf /tmp/blog-src.tar.gz --exclude='blog/public' blog

# 2. 上传
scp /tmp/blog-src.tar.gz ubuntu@<服务器>:/tmp/

# 3. 服务器上解包并构建
ssh ubuntu@<服务器> "cd ~/blog-src/blog && hugo --minify"

# 4. 备份旧站,再同步到 Nginx 根目录(保护证书续期目录)
ssh ubuntu@<服务器> "sudo cp -a /var/www/blog /var/www/blog.bak.\$(date +%Y%m%d)
  && sudo rsync -a --delete --filter='protect .well-known/' \
     ~/blog-src/blog/public/ /var/www/blog/"

一键部署脚本的执行输出

几个我踩过、值得注意的点:

  • rsync --delete 很危险也很好用:它会让目标目录和源完全一致,删掉的文章也会从 线上消失。所以我在同步前先 cp -a 备份一份旧站,出问题能秒回滚。
  • 保护 .well-known/:acme.sh 做 HTTP-01 验证、以及证书续期都依赖 /.well-known/acme-challenge/ 这个路径。用 --filter='protect .well-known/' 让 rsync 不要删它,否则证书续期会失败(这坑我在 HTTPS 那篇 里详细记过)。
  • 文件权限:同步后把属主改成 www-data、目录 755、文件 644,Nginx 才能正常读。

Nginx 那边就是一个最普通的静态站点配置,root 指向 /var/www/blog, try_files $uri $uri/ =404; 即可,配合上篇提到的 HTTPS 就完整了。

小结

两篇下来,Hugo 的核心心智模型其实很清晰:content/ 里用 Markdown(和 Page Bundle) 描述内容,layouts/ 决定怎么渲染,hugo --minify 把一切烤成 public/ 里的纯静态 文件,最后随便用什么方式把 public/ 发到服务器就行。 理解了"内容 / 模板 / 产物" 这三层分离,剩下的主题定制和部署自动化都只是体力活。