这篇记录一下这个博客的搭建过程。

需求很明确,本地用 Markdown 写文章,本地预览,发布到自己的服务器。服务器上不跑博客后台,不维护数据库,不开放登录入口,只用 OpenResty 提供静态文件。

最后用的是这套:

Hugo + PaperMod + GitHub 私有仓库 + systemd timer + 1Panel OpenResty

一开始想过 WordPress 和 Ghost,但暂时没必要。它们适合需要后台编辑器、账号、评论、订阅、插件生态的场景。我现在更需要的是一个可以长期放笔记的地方,写作和发布都尽量简单。

本地站点

先创建 Hugo 项目。

hugo new site blog
cd blog
git init

主题先用 PaperMod。

git submodule add https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod

hugo.yaml 里主要配置这些:

baseURL: https://example.com/
title: my blog
theme: PaperMod
timeZone: Asia/Shanghai
hasCJKLanguage: true

outputs:
  home:
    - HTML
    - RSS
    - JSON

outputs.home 里的 JSON 是给搜索用的。PaperMod 的搜索页依赖 index.json,如果这里没开,页面可能能打开,但搜不到内容。

新文章放在 content/posts/

hugo new posts/my-note.md

文章头部大概这样:

---
title: "文章标题"
date: 2026-06-16T04:00:00+08:00
draft: false
tags:
  - Hugo
---

本地预览:

hugo server -D

默认地址:

http://127.0.0.1:1313/

生成静态文件:

hugo --gc --minify

生成结果在 public/。这个目录不提交到 Git,只作为构建产物。

服务器

服务器这边用 1Panel 管理 OpenResty,建一个普通静态站点。站点目录指向最终发布目录,概念上类似:

server {
    server_name example.com;
    root /path/to/site/root;
    index index.html;
}

1Panel 会生成 OpenResty 配置,尽量不要手动改整份配置。更稳的是在面板里处理域名、证书、HTTPS 和站点目录,发布脚本只负责把 Hugo 生成结果同步过去。

最早的发布方式是本地构建后直接同步:

hugo --gc --minify
rsync -a --delete public/ user@your-server:/path/to/site/root/

这个方法能用,但不适合多端写作。换一台电脑,就要重新配置 SSH、远端路径和发布脚本。后面改成 GitHub 做中心仓库,服务器自己拉取并构建。

现在的发布链路是:

本地写文章
git push 到 GitHub
服务器定时 git pull
服务器本地 hugo 构建
rsync 到 OpenResty 站点目录

这样本地电脑、VSCode、Obsidian、Codex 都只需要和 GitHub 交互。日常不需要 SSH 到服务器发布。

GitHub 同步

GitHub 仓库存源码和文章,不存 public/。服务器用只读 Deploy Key 拉仓库。这个 key 只需要读权限,不需要写权限,也不是 GitHub 账号密码。

服务器同步脚本的核心逻辑:

git fetch --prune origin main
git reset --hard origin/main
git submodule update --init --recursive
hugo --gc --minify --destination /tmp/blog-build
rsync -a --delete /tmp/blog-build/ /path/to/site/root/

这里要注意 --delete。它会让线上目录和构建目录保持一致,适合静态站发布,但前提是源目录和目标目录都写对。脚本里最好固定构建目录和站点目录,不要临时拼危险路径。

定时任务用 systemd timer

service 大概是:

[Service]
Type=oneshot
ExecStart=/usr/local/bin/blog-sync

timer 大概是:

[Timer]
OnBootSec=30s
OnUnitActiveSec=2min
Persistent=true

这样不用开 webhook,也不用让 GitHub 主动访问服务器。缺点是会有一点延迟,通常一两分钟。个人博客可以接受。

排查时看这几个:

systemctl status blog-sync.service
systemctl status blog-sync.timer
journalctl -u blog-sync.service -n 80 --no-pager

这套跑通后,本地的 scripts/deploy.sh 就删掉了。发布入口只保留 Git。

DNS 和 HTTPS

域名接入 Cloudflare 后,排查时不要只看浏览器结果。浏览器缓存、旧跳转规则、Cloudflare 规则都可能让判断变复杂。

先看响应头:

curl -I https://example.com/

再看浏览器最终跳转。

这次主要检查了几类东西:

  • DNS 是否指向正确服务器;
  • Cloudflare 里是否还有旧跳转规则;
  • 1Panel/OpenResty 站点是否绑定了正确域名;
  • HTTPS 模式和源站证书是否一致;
  • 访问根域名和文章页是否都返回 200。

静态站本身不复杂,真正容易误判的是缓存和跳转。

写作流程

以后写文章就按这个来:

cd blog
git pull --rebase
hugo new posts/my-note.md
hugo server -D
git add .
git commit -m "docs: add my note"
git push

服务器定时任务会自动拉取并发布。

VSCode 建议直接打开整个 Hugo 项目,因为 Git、主题、配置和终端都在同一个目录里。

Obsidian 有两种用法:

  • 只写文章,可以把 content/ 当成 vault;
  • 想用 Obsidian Git 插件,就打开整个仓库。

如果用 Obsidian,记得忽略 .obsidian/,不要把编辑器配置提交到博客仓库。

图片

静态博客最麻烦的不是 Markdown,而是图片放哪里。

这次接了一个独立图床。仓库里只保留上传脚本和变量示例,真正的 token 放在本地 .env,并加入 .gitignore

示例配置:

LSKY_BASE_URL=https://your-image-host.example
LSKY_TOKEN=

上传图片:

./scripts/lsky-upload.sh /path/to/image.png

脚本返回图片 URL 后,再写进文章头部:

cover:
  image: "https://your-image-host.example/i/2026/06/17/example.png"
  alt: "图片说明"

这样文章源码里只有公开图片地址,没有账号和密钥。换电脑写作时,只需要重新配置本机 .env

以后再补

现在先保持简单。后续真需要时再加:

  • 评论;
  • 搜索优化;
  • 统计;
  • 图片压缩;
  • 主题细节调整;
  • 自动备份。

第一版先到这里。现在这套流程已经能满足基本写作:本地写,GitHub 同步,服务器自动发布。