Hugo 静态站点有一个很容易混乱的地方:源码和发布产物都在同一个目录附近,但它们其实不是一类东西。
源码是 content/、layouts/、assets/、static/、hugo.yaml 这些文件;发布产物是 Hugo 构建后生成的 HTML、CSS、JS、图片索引和站点地图。我的站点把这两部分拆成两个 Git 仓库:
/Users/mana/Documents/codex/hugo/ Hugo 源码仓库
/Users/mana/Documents/codex/hugo/public/ GitHub Pages 发布仓库这样做以后,日常操作也分成两个命令:
purodn 构建 Hugo,并把 public/ 推到 GitHub Pages 仓库
purodncode 验证 Hugo 源码,并把源码推到私有源码仓库这篇文章记录这套工作流,也顺便记一下这次 purodncode 失效的排查过程。
为什么要分成两个仓库
如果只维护一个仓库,很容易把 Hugo 生成物当成源码提交进去。比如这些文件和目录都不是源码:
public/
resources/
.hugo_build.lock
categories/
css/
js/
posts/
tags/
index.html
index.json
sitemap.xml它们要么是构建结果,要么是本地缓存。真正值得长期维护的是 Markdown 文章、文章图片、主题模板、样式脚本和站点配置。
双仓库的边界可以这样理解:
源码仓库:保存人写的东西
发布仓库:保存 Hugo 生成给浏览器看的东西源码仓库可以是私有仓库,里面有草稿、脚本和本地协作规则;发布仓库通常是公开仓库,GitHub Pages 直接从里面读取静态网页。
目录结构
本地 Hugo 源码目录:
/Users/mana/Documents/codex/hugo源码仓库远程:
git@github.com:hanigege/purodn.tk-source-code.git本地发布目录:
/Users/mana/Documents/codex/hugo/public发布仓库远程:
git@github.com:hanigege/purodn.tk.git注意,public/ 虽然在 Hugo 源码目录里面,但它自己也是一个独立 Git 仓库。检查源码状态时在 Hugo 根目录执行 git status;检查发布产物状态时要进入 public/,或者使用 git -C public status。
一键发布网站:purodn
purodn 的职责只有一个:构建站点,然后提交并推送 public/ 里的发布产物。
我把它写在 ~/.zshrc 里:
purodn() {
SRC_DIR="$HOME/Documents/codex/hugo"
PUBLIC_DIR="$SRC_DIR/public"
echo "Building Hugo site..."
cd "$SRC_DIR" || { echo "Failed to cd into $SRC_DIR"; return 1; }
hugo --minify --gc
rm -rf "$SRC_DIR/resources" "$SRC_DIR/.hugo_build.lock" "$SRC_DIR/.DS_Store"
echo "Checking public repository..."
cd "$PUBLIC_DIR" || { echo "Failed to cd into $PUBLIC_DIR"; return 1; }
if [ -z "$(git status --short)" ]; then
echo "No publish changes."
else
git add -A
git commit -m "Publish site updates" || return 1
echo "Pushing to origin main..."
git push origin main || return 1
echo "Published successfully."
fi
}这个命令的关键点是:
- 先在 Hugo 源码根目录运行
hugo --minify --gc。 - 构建完成后再进入
public/。 - 只提交
public/里的变化。 - 没有发布产物变化时直接输出
No publish changes.。
也就是说,purodn 不负责备份源码。它只负责发布网站。
一键备份源码:purodncode
源码备份我单独做成脚本:
/Users/mana/Documents/codex/hugo/scripts/purodncode~/.zshrc 里只保留一个轻量函数:
purodncode() {
/bin/sh "$HOME/Documents/codex/hugo/scripts/purodncode" "$@"
}脚本内容如下:
#!/bin/sh
set -eu
SITE_DIR="/Users/mana/Documents/codex/hugo"
SOURCE_REMOTE="git@github.com:hanigege/purodn.tk-source-code.git"
COMMIT_MESSAGE="${1:-Backup source updates}"
cd "$SITE_DIR"
if ! command -v hugo >/dev/null 2>&1; then
echo "Error: hugo command not found." >&2
exit 1
fi
if [ ! -d "$SITE_DIR/.git" ]; then
echo "Error: $SITE_DIR is not a Git repository." >&2
exit 1
fi
origin_url="$(git remote get-url origin)"
if [ "$origin_url" != "$SOURCE_REMOTE" ]; then
echo "Error: origin is $origin_url, expected $SOURCE_REMOTE." >&2
exit 1
fi
if [ -z "$(git status --short)" ]; then
echo "No source changes."
exit 0
fi
echo "Validating Hugo source..."
hugo --destination /private/tmp/hugo-build --cleanDestinationDir --minify --gc
rm -rf "$SITE_DIR/resources" "$SITE_DIR/.DS_Store"
if ! lsof -nP -iTCP:1313 -sTCP:LISTEN >/dev/null 2>&1; then
rm -f "$SITE_DIR/.hugo_build.lock"
fi
echo "Backing up Hugo source to origin/main..."
git add -A
if [ -z "$(git diff --cached --name-only)" ]; then
echo "No source changes after cleanup."
exit 0
fi
git commit -m "$COMMIT_MESSAGE"
git push origin main
echo "Source backup completed."这样拆开以后,日常操作很清楚:
purodn表示发布网站。
purodncode表示备份源码。
如果想自定义源码提交信息,可以这样传参数:
purodncode "Add Hugo dual repository workflow article"这次 purodncode 为什么失效
原来的 ~/.zshrc 写法是:
purodncode() {
"$HOME/Documents/codex/hugo/scripts/purodncode" "$@"
}它的意思是直接执行这个脚本文件。脚本本身有执行权限,也有 shebang:
#!/bin/sh但实际排查时发现一个现象:
/Users/mana/Documents/codex/hugo/scripts/purodncode直接执行失败。
而显式交给 /bin/sh 解释执行时正常:
/bin/sh /Users/mana/Documents/codex/hugo/scripts/purodncode输出:
No source changes.所以最后把 .zshrc 函数改成:
purodncode() {
/bin/sh "$HOME/Documents/codex/hugo/scripts/purodncode" "$@"
}这个写法不再依赖系统如何处理脚本文件的直接执行,而是明确告诉系统:用 /bin/sh 来解释这个脚本。
排查这类 shell 函数问题
遇到 .zshrc 里的命令突然不能用,可以按这个顺序看。
先确认当前 shell 是否加载了函数:
type purodncode正常应该看到类似输出:
purodncode is a shell function from /Users/mana/.zshrc如果显示 command not found,说明当前 shell 没有加载 .zshrc。可以重新加载:
source ~/.zshrc再检查 .zshrc 语法:
zsh -n ~/.zshrc没有输出通常就是语法通过。
然后分别测试“直接执行脚本”和“用 sh 执行脚本”:
/Users/mana/Documents/codex/hugo/scripts/purodncode
/bin/sh /Users/mana/Documents/codex/hugo/scripts/purodncode如果第二条正常,第一条异常,就可以像这次一样在 .zshrc 函数里显式加 /bin/sh。
使用时要记住的边界
这套流程最重要的不是命令本身,而是边界。
发布网站时:
purodn它会更新并推送 public/。
备份源码时:
purodncode它会验证 Hugo 源码,然后提交并推送源码仓库。
不要在 Hugo 根目录提交 public/,也不要在 public/ 里判断源码有没有备份。两个仓库各管各的,后面排查问题会轻松很多。
我的习惯是:
- 写文章或改模板后,先本地预览。
- 确认没问题后,用
purodncode备份源码。 - 需要公开发布时,再用
purodn推送生成后的网页。
这样即使某一步出问题,也能很快知道它属于源码问题、构建问题,还是发布仓库问题。