Hugo 双仓库工作流:用 purodn 发布网站,用 purodncode 备份源码

记录一个 Hugo 站点的源码仓库与 public 发布仓库分离方案,并整理 .zshrc 中一键发布和一键备份源码命令的写法与排错思路。

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
}

这个命令的关键点是:

  1. 先在 Hugo 源码根目录运行 hugo --minify --gc
  2. 构建完成后再进入 public/
  3. 只提交 public/ 里的变化。
  4. 没有发布产物变化时直接输出 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/ 里判断源码有没有备份。两个仓库各管各的,后面排查问题会轻松很多。

我的习惯是:

  1. 写文章或改模板后,先本地预览。
  2. 确认没问题后,用 purodncode 备份源码。
  3. 需要公开发布时,再用 purodn 推送生成后的网页。

这样即使某一步出问题,也能很快知道它属于源码问题、构建问题,还是发布仓库问题。