Woodzow 个人网站搭建与部署笔记

本文记录当前网站从本地开发到云服务器自动部署的主要结构、命令和验证方法。网站地址为 https://woodzow.top,源码仓库为 Woodzow/woodzow_web。

1. 技术方案

  • 静态网站生成器:Hugo Extended
  • 主题:PaperMod(Git 子模块)
  • 本地环境:macOS、VS Code、Git
  • 代码托管:GitHub
  • 服务器:Ubuntu、Nginx、Certbot
  • 更新方式:服务器定时拉取 GitHub main 分支,构建成功后发布

整体流程:

本地修改并预览 → 推送 GitHub → systemd 定时检查 → Hugo 构建 → rsync 发布 → Nginx 提供 HTTPS 访问

2. 项目结构

woodzow_web/
├── hugo.yaml                  # Hugo、菜单、主页和 PaperMod 参数
├── content/                   # 页面和 Markdown 内容
│   ├── about.md
│   ├── academic/
│   ├── blog/
│   └── paper-notes/
├── layouts/                   # 对 PaperMod 的模板覆盖和短代码
├── assets/css/extended/       # 自定义样式
├── static/
│   ├── fonts/                 # 随网站部署的字体
│   └── images/                # 头像等静态图片
├── archetypes/                # 新建内容时使用的模板
├── themes/PaperMod/           # PaperMod Git 子模块
├── .vscode/tasks.json         # VS Code 预览、构建和新建文章任务
├── .gitmodules                # PaperMod 子模块地址
└── .gitignore                 # 忽略 public、构建缓存和系统文件

不要直接修改 themes/PaperMod/。需要定制时,应在项目根目录的 layouts/ 或 assets/ 中创建同路径文件进行覆盖。

当前站点的主要定制

  • 站点仅保留中文,导航栏为 HomePage、Blog、Academic、PaperNotes、About。
  • Blog 下分 Technology、Research、Essays、Reflections。
  • Academic 页面组织 Research 与 Publications,论文通过 publication 短代码显示为卡片。
  • 首页头像位于 static/images/woodzow2.png。
  • 中文使用霞鹜文楷等宽,英文使用 Source Sans 3,代码使用 Fira Mono。
  • 自定义字体、悬停变色、下拉菜单、卡片和悬浮目录主要写在 assets/css/extended/custom.css。

PaperMod 的几个关键参数:

params:
  defaultTheme: auto
  disableThemeToggle: false
  ShowToc: true
  profileMode:
    enabled: true
  • profileMode.enabled: true 使用 Profile 首页;改成 false 后使用 homeInfoParams。
  • defaultTheme: auto 跟随系统明暗模式。
  • disableThemeToggle: false 显示月亮/太阳按钮,该按钮只切换明暗主题。
  • ShowToc: true 允许文章显示目录,页面 Front Matter 可以单独覆盖。
  • 邮箱链接必须使用 mailto:邮箱地址。

PaperMod 官方演示站的语言入口可能加载不同内容和参数,所以切换语言后看起来像更换模板;语言按钮本身并不负责切换 Profile/Home Info 模式。

3. 本地安装与首次拉取

macOS 安装必要工具:

brew install hugo git
hugo version
git --version

克隆仓库及主题:

git clone --recurse-submodules git@github.com:Woodzow/woodzow_web.git
cd woodzow_web
git submodule update --init --recursive

Hugo 应显示 extended。PaperMod 子模块状态前没有 - 或 + 表示正常:

git submodule status

4. VS Code 编辑与浏览器预览

在 VS Code 中打开仓库:

code /Users/zhongwu/woodzow_web

启动本地预览:

hugo server -D --navigateToChanged

浏览器访问 http://localhost:1313/。其中:

  • -D:预览 draft: true 的草稿。
  • --navigateToChanged:内容变化后浏览器跳转到相应页面。

也可以在 VS Code 中运行任务 Hugo: 本地预览。停止服务使用 Ctrl+C。

正式构建验证:

hugo --cleanDestinationDir --gc --minify

正式部署不包含草稿,准备发布的文章应设置:

draft: false

5. 日常内容更新

新建内容:

hugo new content paper-notes/new-note.md
hugo new content blog/technology/new-post.md

本地预览正常后提交:

git status
git add <修改的文件>
git commit -m "描述本次修改"
git push origin main

服务器将在约 5 分钟内自动更新。不要把生成目录 public/ 提交到 Git;服务器会自行构建。

6. 服务器基础部署

服务器使用独立的 deploy 用户执行 Git 拉取和 Hugo 构建,该用户不需要 sudo 权限:

sudo adduser --disabled-password --gecos "" deploy
sudo install -d -m 755 -o deploy -g deploy /home/deploy/woodzow_web
sudo install -d -m 755 -o deploy -g deploy /home/deploy/woodzow-build
sudo install -d -m 755 -o deploy -g deploy /var/www/woodzow.top

为服务器创建只读 GitHub 密钥,并将公钥添加到仓库的 Deploy keys:

sudo install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
sudo -u deploy ssh-keygen -t ed25519 \
  -f /home/deploy/.ssh/github_woodzow_readonly \
  -C "woodzow server deploy"

/home/deploy/.ssh/config 指定该密钥:

Host github.com
    HostName github.com
    User git
    IdentityFile /home/deploy/.ssh/github_woodzow_readonly
    IdentitiesOnly yes

验证并克隆:

sudo -u deploy ssh -T git@github.com
sudo -u deploy git clone --recurse-submodules \
  git@github.com:Woodzow/woodzow_web.git \
  /home/deploy/woodzow_web

GitHub 返回“successfully authenticated, but GitHub does not provide shell access”表示认证成功;该测试返回状态码 1 是正常现象。

服务器应安装官方 Hugo Extended 软件包,不使用 Snap 版 Hugo。验证:

hugo version
sudo -u deploy hugo version

两条命令都应显示 extended linux/amd64。

首次构建和发布:

sudo -u deploy hugo \
  --source /home/deploy/woodzow_web \
  --destination /home/deploy/woodzow-build \
  --cleanDestinationDir --gc --minify

sudo -u deploy rsync -a --delete \
  /home/deploy/woodzow-build/ \
  /var/www/woodzow.top/

7. Nginx、域名与 HTTPS

DNS 应将根域名指向服务器公网 IP,并让 www 指向根域名。验证:

dig +short woodzow.top
dig +short www.woodzow.top

Nginx 站点的核心配置:

server {
    listen 80;
    server_name woodzow.top www.woodzow.top;
    root /var/www/woodzow.top;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

检查并加载配置:

sudo nginx -t
sudo systemctl reload nginx
curl -I -H 'Host: woodzow.top' http://127.0.0.1

申请 HTTPS 证书并启用 HTTP 自动跳转:

sudo certbot --nginx --redirect \
  -d woodzow.top \
  -d www.woodzow.top

证书验证:

sudo certbot certificates
sudo certbot renew --dry-run
systemctl list-timers --all | grep -i certbot
curl -I https://woodzow.top

阿里云安全组应允许公网访问 TCP 80、443;SSH 22 最好只允许可信 IP。

8. GitHub 自动部署

自动部署脚本位于:

/usr/local/bin/deploy-woodzow

它按顺序执行:拉取 origin/main、更新 PaperMod 子模块、在独立目录构建、构建成功后用 rsync --delete 发布,并记录已经部署的 Git 提交。构建失败时不会覆盖当前线上网站。

对应的 systemd 单元:

/etc/systemd/system/woodzow-deploy.service
/etc/systemd/system/woodzow-deploy.timer

加载并启用:

sudo systemctl daemon-reload
sudo systemctl start woodzow-deploy.service
sudo systemctl enable --now woodzow-deploy.timer

验证结果:

systemctl show woodzow-deploy.service -p Result -p ExecMainStatus
systemctl status woodzow-deploy.timer --no-pager
systemctl list-timers --all | grep woodzow
sudo journalctl -u woodzow-deploy.service -n 100 --no-pager

正常的一次性服务执行结束后会显示 inactive (dead),这是 Type=oneshot 的预期状态。真正需要常驻等待的是 timer。成功日志应包含:

Successfully deployed revision: <Git 提交哈希>

9. 本次搭建中遇到的主要问题

9.1 浏览器显示 documentation example domain

这是因为配置或内容中仍存在 example.com、example.org 等专门用于文档示例的占位域名,不是 Hugo 错误。可以搜索:

rg -n "example\\.com|example\\.org|example\\.net" . \
  -g '!themes/**' \
  -g '!public/**'

把对应 URL 换成真实地址。邮件按钮应使用:

- name: email
  url: "mailto:name@example.com"

9.2 本站和 PaperMod 官方预览不同

PaperMod 提供布局和功能,官方演示站还包含自己的内容、语言和参数,新站不会自动复制全部演示效果。重点检查:

rg -n "profileMode|homeInfoParams|defaultTheme|languages|menu:" hugo.yaml

需要修改模板时使用根目录的 layouts/ 覆盖主题,不要直接改 themes/PaperMod/。

9.3 Markdown 标题旁出现 # 号

这是标题锚点。点击后 URL 会带上标题标识,用于直接分享文章中的某一节,不是错误。

9.4 dig 返回 198.18.0.x

如果本机开启代理软件,这通常是代理的 Fake IP,不是云服务器真实公网 IP。可以临时关闭代理,或指定公共 DNS:

dig @1.1.1.1 +short woodzow.top
dig @8.8.8.8 +short woodzow.top

结果应与云服务器控制台显示的公网 IP 一致。

9.5 SSH 提示 PTY allocation failed

当 deploy 账号或授权密钥禁止交互终端时,连接可能显示:

PTY allocation request failed on channel 0

这不一定代表认证失败。用远程命令验证:

ssh -i ~/.ssh/woodzow_deploy \
  deploy@<SERVER_IP> 'id'

能返回用户信息,说明密钥认证和远程命令执行正常。

9.6 GitHub SSH 验证成功但仓库仍无法拉取

ssh -T 成功只代表 GitHub 接受了某把密钥,不一定代表该密钥拥有目标仓库权限。服务器只读公钥应添加到目标仓库的 Settings → Deploy keys,并在 /home/deploy/.ssh/config 中指定私钥:

Host github.com
    HostName github.com
    User git
    IdentityFile /home/deploy/.ssh/github_woodzow_readonly
    IdentitiesOnly yes

设置权限并检查实际选择的密钥:

sudo chown deploy:deploy /home/deploy/.ssh/config
sudo chmod 600 /home/deploy/.ssh/config

sudo -u deploy ssh -G github.com | grep -i identityfile
sudo -u deploy git ls-remote \
  git@github.com:Woodzow/woodzow_web.git HEAD

GitHub 返回 successfully authenticated, but GitHub does not provide shell access,表示 SSH 认证成功。GitHub 不提供交互式 Shell,所以 ssh -T 返回状态码 1 是正常的。

SSH 主机指纹用于确认连接目标确实是 GitHub,降低中间人攻击风险。首次写入 known_hosts 前,应与 GitHub 官方公布的指纹核对。

9.7 PaperMod 子模块没有拉取

检查:

git submodule status

开头符号的含义:

  • -:尚未初始化。
  • +:当前提交与主仓库记录不一致。
  • 没有异常前缀:正常。

修复:

git submodule sync --recursive
git submodule update --init --recursive

9.8 public 已加入 .gitignore,但仍被提交

.gitignore 不会停止跟踪已经提交过的文件,需要执行一次:

git rm -r --cached public
git rm --cached .hugo_build.lock
git add .gitignore
git commit -m "Stop tracking generated Hugo files"

–cached 只取消 Git 跟踪,不删除本地生成文件。

9.9 Snap Hugo 的 cgroup 错误

在 admin 会话中执行 sudo -u deploy hugo 时,Snap 版 Hugo 曾出现:

... is not a snap cgroup for tag snap.hugo.hugo

这是 Snap 隔离和用户会话问题,不是 Hugo 内容错误。服务器最终改用 Hugo 官方 Extended .deb。示例安装如下,版本变化时以 Hugo Releases 页面为准:

cd /tmp
HUGO_VERSION="0.164.0"

wget -O "hugo_extended_${HUGO_VERSION}_linux-amd64.deb" \
  "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb"

wget -O "hugo_${HUGO_VERSION}_checksums.txt" \
  "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_checksums.txt"

sha256sum --check --ignore-missing \
  "hugo_${HUGO_VERSION}_checksums.txt"

sudo apt install \
  "./hugo_extended_${HUGO_VERSION}_linux-amd64.deb"

安装后同时验证 admin 和 deploy 用户:

hugo version
sudo -u deploy hugo version

两条结果都应包含 extended linux/amd64。

9.10 Hugo 安装包校验失败

下载中断可能产生 .deb.1、.deb.2 等文件并导致:

FAILED
computed checksum did NOT match

此时不能安装。删除明确的临时下载文件,使用 wget -O 固定文件名重新下载,再执行 SHA-256 校验。校验失败通常表示文件不完整、文件选错或代理返回了异常页面。

9.11 Hugo 输出 deprecated 警告

WARN deprecated 表示主题调用了未来将移除的 Hugo 接口。只要命令最终成功、退出码为 0 且生成页面,当前部署仍然完成。后续更新 PaperMod 即可;警告不等同于构建失败。

9.12 systemd 服务显示 inactive/dead 和 exit 3

woodzow-deploy.service 是 Type=oneshot,执行完就退出,因此成功后显示 Active: inactive (dead) 是正常的。systemctl status 对不活动服务可能返回退出码 3。

使用以下命令判断上次执行结果:

systemctl show woodzow-deploy.service \
  -p Result \
  -p ExecMainStatus

sudo journalctl -u woodzow-deploy.service \
  -n 100 --no-pager

正常结果:

Result=success
ExecMainStatus=0
Successfully deployed revision: <Git 提交哈希>

真正持续等待的是 woodzow-deploy.timer,应显示 active (waiting)。

9.13 Certbot timer 有下一次时间但没有上次记录

刚启用的定时器可能尚未运行。验证:

sudo certbot renew --dry-run
systemctl status snap.certbot.renew.timer --no-pager
systemctl list-timers --all | grep -i certbot

定时器只负责周期性检查,证书接近到期时才会续期,不会每次检查都签发新证书。

10. 自动部署脚本详解

服务器脚本 /usr/local/bin/deploy-woodzow 的完整核心逻辑:

#!/usr/bin/env bash
set -Eeuo pipefail

REPO="/home/deploy/woodzow_web"
BUILD="/home/deploy/woodzow-build"
WEBROOT="/var/www/woodzow.top"
STATE_DIR="/home/deploy/.local/state/woodzow"
STATE_FILE="${STATE_DIR}/deployed-revision"

mkdir -p "$BUILD" "$STATE_DIR"

echo "Checking GitHub for updates..."
git -C "$REPO" fetch --prune origin main

TARGET_REVISION="$(git -C "$REPO" rev-parse origin/main)"
DEPLOYED_REVISION=""

if [[ -f "$STATE_FILE" ]]; then
    DEPLOYED_REVISION="$(<"$STATE_FILE")"
fi

if [[ "$TARGET_REVISION" == "$DEPLOYED_REVISION" ]]; then
    echo "Already up to date: $TARGET_REVISION"
    exit 0
fi

git -C "$REPO" switch main
git -C "$REPO" merge --ff-only origin/main
git -C "$REPO" submodule sync --recursive
git -C "$REPO" submodule update --init --recursive

echo "Building Hugo site..."
hugo \
  --source "$REPO" \
  --destination "$BUILD" \
  --cleanDestinationDir --gc --minify

echo "Publishing site..."
rsync -a --delete "$BUILD"/ "$WEBROOT"/

printf '%s\n' "$TARGET_REVISION" > "$STATE_FILE"
echo "Successfully deployed revision: $TARGET_REVISION"

权限:

sudo chown root:root /usr/local/bin/deploy-woodzow
sudo chmod 755 /usr/local/bin/deploy-woodzow

状态文件只在发布成功后写入。即使 Git 已拉到新提交,只要构建失败,下一轮仍会重试,同时不会覆盖线上版本。

手动测试、启用和观察:

sudo systemctl daemon-reload
sudo systemctl start woodzow-deploy.service
sudo systemctl enable --now woodzow-deploy.timer

systemctl status woodzow-deploy.timer --no-pager
systemctl list-timers --all | grep woodzow
sudo journalctl -u woodzow-deploy.service -f

日志跟踪使用 Ctrl+C 退出。

11. 日常自动发布流程

本地修改并预览:

cd /Users/zhongwu/woodzow_web
hugo server -D --navigateToChanged

另开终端验证并提交:

hugo --cleanDestinationDir --gc --minify
git status
git diff
git add <修改的文件>
git commit -m "Update website"
git push origin main

约 5~6 分钟后检查:

sudo journalctl -u woodzow-deploy.service \
  -n 50 --no-pager
curl -I https://woodzow.top

浏览器仍显示旧内容时可强制刷新,macOS 常用 Command+Shift+R。不要直接修改服务器仓库或 /var/www/woodzow.top 中的生成文件;修复应在本地完成后重新推送。

12. 最终检查清单

每次重要更新后可以依次检查:

# 本地
hugo --cleanDestinationDir --gc --minify
git status

# 服务器
sudo journalctl -u woodzow-deploy.service -n 50 --no-pager
sudo nginx -t
curl -I https://woodzow.top
systemctl --failed

同时确认:

  • 首页、菜单、文章、图片、字体和移动端显示正常。
  • HTTPS 可访问,HTTP 会跳转到 HTTPS。
  • GitHub 新提交能被自动部署。
  • 页脚使用真实的网站 ICP 备案号;公安联网备案通过后再添加真实的公安备案号和图标。
  • 定期执行系统安全更新,并在可控时间重启服务器。