我的博客搭建经历

在各种一站式写作平台、社交媒体和知识库产品层出不穷的 2026 年,为什么还要费尽心机去自己折腾一个独立博客?

对于我来说,答案很简单:拥有一个完全属于自己的数字花园。 在这里,我拥有对内容的 100% 控制权,没有算法推荐的裹挟,没有格式排版的限制,更重要的是,博客本身的设计、架构、自动化脚本的演进过程,就是一个极佳的程序员技术沙盒。

今天,我想把这个博客从搭建、优化、重构至今的完整历程和技术选型细节记录下来。

在建站之初,我对比了 Hexo、Gatsby、Astro 以及 Hugo。最终我选择了 Hugo Extended。

  • 速度的绝对降维打击:Hugo 是用 Go 编写的,构建速度往往以毫秒计。对于一个需要频繁预览和调整的个人博客来说,保存即渲染的即时反馈体验极为畅快。
  • SCSS 的原生处理能力:由于我选用了功能非常丰富的 DoIt 主题,它需要动态处理大量的样式表。Hugo 的 Extended 版本 内置了对 SCSS 和编译管道的支持,我可以非常轻松地在 assets/css/_override.scss 中覆盖主题变量(例如我将全站的非代码字体覆盖为了 LXGW Bright 霞鹜文楷,而无需修改主题子模块的代码)。

为了让大家有一个直观的宏观认识,我将整个博客的“本地写作、本地脚本安检、代码托管、云端 CI/CD 构建、CDN 分发及图床/评论闭环”的工作流,绘制成如下拓扑图。本博客主题原生支持 Mermaid,无需任何多余配置即可直接渲染:

graph TD
    subgraph Local ["1. 本地写作与开发 - WSL / Windows"]
        A["Zed 编辑器"] -->|"1. 新建并编写博文"| B("Markdown 源文件")
        B -->|"2. 本地热预览"| C["Hugo 本地 Server"]
        C -->|"3. 浏览器实时调试 localhost:1313"| D(("本地预览"))
    end

    subgraph Deploy ["2. 自动化部署 - 双端脚本"]
        B -->|"4. 一键部署指令 deploy.sh / deploy.ps1"| E{"五重安全雷达检测"}
        E -->|"未拉取主题"| F["自愈子模块"]
        E -->|"落后远程"| G["拦截并提示 git pull"]
        E -->|"草稿检测"| H["显示 draft: true 列表并警告"]
        E -->|"检测通过"| I["暂存并生成本地 Backup 提交"]
        I -->|"5. 安全推送 Git Push"| J["GitHub 源码私有仓库"]
    end

    subgraph Cloud ["3. 云端构建与 CDN 分发"]
        J -->|"6. Webhook 自动触发"| K["Cloudflare Pages 构建管道"]
        K -->|"7. 拉取子模块并运行 Hugo Build"| L["全量静态 HTML/CSS/JS"]
        L -->|"8. 部署至全球边缘网络"| M["Cloudflare CDN 边缘节点"]
    end

    subgraph User ["4. 读者端访问与交互"]
        N(("读者浏览器")) -->|"9. 访问你的域名"| M
        N -->|"10. 匿名请求无 Referrer 图片"| O["Cloudflare Workers 图床"]
        N -->|"11. 读写文章评论"| P["Utterances 评论 / GitHub Issues"]
    end

独立博客好看不好用,往往是因为“图片难存、双语难翻、评论难搭”。为了解决这三个痛点,我逐步搭建了属于自己的解决方案。

我的博客是一套中英文双语系统(中文默认,英文为辅)。

最初,我使用的是 Hugo 默认的“文件名后缀模式”:

content/posts/my-post.md     # 中文文章
content/posts/my-post.en.md  # 英文文章

当文章数量少时还好,一旦变多,两类语言的文件就会在同一个文件夹下乱成一团,且编辑器的侧边栏看起来杂乱无章。

为了彻底重构为 “基于目录的多语言管理模式” (Content Directory Multilingual),我把中英文文章物理隔离到独立的语言目录下:

  • 将中文文章全部隔离到 content/zh/posts/
  • 将英文文章全部隔离到 content/en/posts/

并在 hugo.toml 中做了如下的完整映射配置(已脱敏):

# hugo.toml
defaultContentLanguage = "zh-cn"

[languages]
  [languages.zh-cn]
    contentDir = "content/zh"  # 中文内容源目录
    weight = 1
    title = "我的个人博客"
    locale = "zh-CN"
    label = "简体中文"
    hasCJKLanguage = true       # 开启中日韩字数精确统计

  [languages.en]
    contentDir = "content/en"  # 英文内容源目录
    weight = 2
    title = "My Blog"
    locale = "en"
    label = "English"
    hasCJKLanguage = false

现在,中英文文章的文件名完全相同(比如都叫 my-post.md),Hugo 会自动通过同名相对路径在渲染时把它们关联为互为翻译。我的工作区瞬间变得极度清爽!

为了随处写作且保证图片能在全球范围内高速、稳定地访问,我没有选择把图片存在博客 Git 仓库里(会拖慢 clone 速度),而是使用了自建的 blog-images.your-domain.workers.dev(基于 Cloudflare Workers 代理的对象存储图床)。

防盗链与本地预览 403 问题的终极解决方案: 由于很多第三方图床会识别 Referer。当你在本地 http://localhost:1313 预览时,浏览器发送的本地 Referer 会导致图床直接返回 403 Forbidden。

我们可以通过重写 Hugo 的 Markdown 图片渲染器,强制图片在加载时不发送 Referer。在项目根目录下新建 layouts/_markup/render-image.html:

<!-- layouts/_markup/render-image.html -->
{{- $lightgallery := .Page.Params.lightgallery | default site.Params.Page.lightgallery | default false -}}
{{- $destination := replaceRE "^/static" "" .Destination -}}

{{- if .Title -}}
<figure>
    {{- if $lightgallery -}}
    <a class="lightgallery" href="{{ $destination }}" title="{{ .Text }}" data-thumbnail="{{ $destination }}" data-sub-html="<h2>{{ .Title }}</h2>">
    {{- end -}}
    <!-- 关键点:注入 referrerpolicy="no-referrer" -->
    <img loading="lazy" referrerpolicy="no-referrer" src="{{ $destination }}" alt="{{ .Text }}">
    {{- if $lightgallery -}}
    </a>
    {{- end -}}
    <figcaption class="image-caption">{{ .Title | safeHTML }}</figcaption>
</figure>
{{- else -}}
<!-- 关键点:注入 referrerpolicy="no-referrer" -->
<img class="tw:inline" loading="lazy" referrerpolicy="no-referrer" src="{{ $destination }}" alt="{{ .Text }}">
{{- end -}}

这一行小小的 HTML 属性 referrerpolicy="no-referrer",完美根治了本地预览图床图片报 403 的顽疾!

如果你用的是 Git 子模块安装的主题,直接改主题里的 CSS 会导致升级困难。最好的做法是在项目级重写样式。在 assets/css/_override.scss 中覆盖全局字体(以引入霞鹜文楷字体为例):

/* assets/css/_override.scss */
@font-face {
    font-family: 'LXGW Bright';
    src: url('/fonts/LXGWBright.woff2') format('woff2');
    font-weight: normal;
    font-style: normal;
    font-display: swap;
}

:root {
    /* 覆盖 DoIt 主题内置的全局字体变量 */
    --global-font-family: 'LXGW Bright', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

我不希望引入多余的第三方臃肿评论脚本,更不希望为此去维护一个后端的数据库。因此,我选用了基于 GitHub Issues 的 Utterances。 其在 hugo.toml 中的配置非常轻量:

# hugo.toml
[params.page.comment]
  enable = true
  [params.page.comment.utterances]
    enable = true
    repo = "your-github-username/your-blog-comments-repo" # 绑定的公共 GitHub 仓库
    issueTerm = "title"                                    # 用博文标题作为 issue 的匹配项
    label = "comments"                                     # 自动打上 comments 标签
    lightTheme = "github-light"
    darkTheme = "github-dark"

不仅加载速度极快,而且非常契合技术博客的格调。

如果每一次写完文章,都需要手动去清理缓存、全量构建、暂存提交、然后安全推送,写博客的积极性很快就会被这些重复机械的劳动磨灭。

为此,我编写了两套功能对齐的部署脚本(Linux/WSL 下的 ./deploy.sh 与 Windows 下的 deploy.ps1)。以下是脚本中最核心、最有技术含量的三大防护模块的脱敏代码实现:

脚本会在执行部署前拉取远程状态,比对本地与远程的提交差异。如果发现本地落后,立即强行终止,防御代码被强制覆盖的悲剧:

# deploy.sh 片段
local remote_ref=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || echo "")
if [ -n "$remote_ref" ]; then
    echo "拉取远程仓库状态 (git fetch)..."
    git fetch --quiet 2>/dev/null
    
    # 统计本地落后于远程的 commit 数量
    local behind_commits=$(git rev-list --count HEAD.."$remote_ref" 2>/dev/null || echo "0")
    if [ "$behind_commits" -gt 0 ]; then
        echo "❌ 部署被拦截:本地分支落后远程分支 $behind_commits 个提交!"
        echo "请先执行 git pull,解决可能存在的冲突后再尝试部署。"
        exit 1
    fi
fi

利用 Hugo 内置指令列出所有带有 draft: true 的文件并优雅高亮,避免你把半成品文章不小心发到线上:

# deploy.sh 片段
check_drafts() {
    if command -v hugo &> /dev/null; then
        # hugo list drafts 会输出草稿列表,用 tail 剥离掉首行表头
        local drafts=$(hugo list drafts 2>/dev/null | tail -n +2 || true)
        if [ -n "$drafts" ]; then
            echo "═══════════════════════════════════════"
            echo " 📝 提醒:以下文章当前仍为草稿 (draft: true)"
            echo " 它们不会被部署到线上版本:"
            echo "$drafts" | while read -r line; do
                local filename=$(echo "$line" | cut -d',' -f1)
                local title=$(echo "$line" | cut -d',' -f2)
                echo "   - $filename ($title)"
            done
            echo "═══════════════════════════════════════"
        fi
    fi
}

当在常规终端里双击或运行时,它会等待用户敲击键盘确认;若被 VS Code Task、后台定时器或 CI/CD pipeline 自动触发,它能自动感知并直接跳过阻碍性的 TTY 确认,防止任务永久挂起死锁:

# deploy.sh 片段
if [ "$confirm" = true ]; then
    # [ -t 0 ] 用来判断标准输入是否来源于真实的交互式终端
    if [ -t 0 ]; then
        read -p "是否继续部署? [y/N] " -n 1 -r
        echo
        if [[ ! $REPLY =~ ^[Yy]$ ]]; then
            echo "❌ 已取消部署"
            exit 0
        fi
    else
        echo "ℹ 检测到非交互式环境,自动跳过人工确认步骤"
    fi
fi

博客的源文件被推送到 GitHub 后,会由 Cloudflare Pages 自动接管。

  • 它的构建容器会自动检测博客的更新,全自动下载 Hugo 依赖并拉取子模块。
  • 重新编译后,把静态文件全球分发。静态文件不需要我推送到第二个仓库,我的 Git 源码仓库就是唯一的真理来源(Source of Truth)。

一个看似简单的博客,其背后蕴含着多语言架构设计、CDN 图床缓存、Git 子模块管理、双系统 shell/PowerShell 脚本编写、无服务器计算 (Serverless Workers) 以及 CI/CD 管道构建等一系列工程实践。

生命不息,折腾不止。这个博客就是我最好的代码试验田,希望未来的日子里,它能记录下我更多、更有价值的思考与探索。