Hugo 是一个用 Go 语言编写的开源静态网站生成器。它的核心作用是把你用 Markdown 写的文章内容、模板文件和配置设置,转换成完整的、由纯 HTML、CSS 和 JavaScript 构成的静态网站。
核心特点与优势:
极速生成: 这是 Hugo 最著名的特点。得益于 Go 语言的高效,Hugo 生成网站的速度极其快,通常只需要毫秒级的时间,即使是拥有成千上万页面的超大型网站。这使得开发和内容更新体验非常流畅。
简单易用:
安装简单:通常只需下载一个二进制文件即可运行,无需复杂的运行环境(如 PHP、Ruby、Node.js)。
内容管理简单:内容通常用 Markdown 格式编写(.md 文件),结构清晰。
配置清晰:主要配置文件是 config.toml/config.yaml/config.json,易于理解和修改。
跨平台: 作为一个独立的二进制文件,Hugo 可以在 Windows, macOS, Linux 等主流操作系统上完美运行。
强大的灵活性:
模板引擎: 使用 Go 的 html/template 库,功能强大且灵活,可以创建复杂的布局和功能。
内容组织: 通过灵活的目录结构和 Front Matter(文件顶部的元数据块,如 YAML, TOML, JSON)来组织内容、定义变量(标题、日期、标签、分类等)。
主题系统: 拥有丰富的主题生态系统,用户可以直接使用或自定义主题来快速改变网站外观。
短代码: 允许在 Markdown 内容中嵌入可重用的自定义 HTML 片段或复杂功能。
安全性高: 生成的网站是纯静态文件,没有数据库或服务器端脚本执行,因此不易受到常见的动态网站攻击(如 SQL 注入、PHP 漏洞等)。
部署简单: 生成的静态文件可以轻松部署到任何支持托管静态文件的服务器或服务上,例如 Netlify, Vercel, GitHub Pages, GitLab Pages, AWS S3, Cloudflare Pages 等,通常免费或成本极低。
活跃的社区: 拥有一个庞大且活跃的社区,提供丰富的文档、主题、教程和支持。
Hugo 的部署本质只有一句话:本地(或 CI)执行 hugo 生成 public/ 目录,再把这一堆纯静态文件交给任意 Web 服务器托管。剩下所有差异都来自「谁来构建、谁来托管」。
两条主流路线先做个对比,按自己的情况选一条为主、另一条备用即可:
| 维度 | 服务器 + 域名 | GitHub Pages |
|---|---|---|
| 费用 | 轻量云服务器活动价几十到一百多一年,续费会上涨;域名几十一年 | 完全免费(公开仓库) |
| 国内访问 | 备案后稳定、速度快 | *.github.io 时快时慢,部分网络不可达 |
| 可控性 | 高:任意 Nginx 配置、重定向、缓存、Headers | 低:只能改 CNAME 与少量配置 |
| 运维成本 | 需要管证书续期、系统更新、备份 | 几乎没有 |
| 适合场景 | 长期主力站、需要国内可用的正式博客 | 试水、写给自己看、读者主要在海外 |
下面两条路线都从「本地建一个 Hugo 站点」开始,第 1~3 步是共用的。
按「云服务器 + Nginx + Let's Encrypt + 国内云 DNS / Cloudflare」这套最常见的组合写。
scoop install hugo-extended,macOS 用 brew install hugo),执行 hugo version 能看到版本号,建议 ≥ 0.146。bash# 生成站点,配置文件用 yaml
hugo new site myblog --format yaml
cd myblog
git init
装主题(以 PaperMod 为例,用 submodule 方便后续升级):
bashgit submodule add https://github.com/adityatelange/hugo-PaperMod themes/PaperMod
编辑 hugo.yaml,这是一份能直接跑起来的最小配置:
yamlbaseURL: "https://example.com/"
languageCode: "zh-cn"
defaultContentLanguage: "zh-cn"
title: "我的博客"
theme: "PaperMod"
enableRobotsTXT: true
hasCJKLanguage: true
pagination:
pagerSize: 20
markup:
goldmark:
renderer:
unsafe: true # 允许正文里写裸 HTML,按需开启
params:
env: production
description: "记录与分享"
author: "你的名字"
ShowReadingTime: true
ShowCodeCopyButtons: true
ShowToc: true
baseURL 必须以 / 结尾,且要和最终访问的协议、域名完全一致。这是后面「页面能打开但样式全丢」的头号原因。
bashhugo new content posts/hello-world/index.md
生成的文章是草稿(draft: true),改成 false 才会出现在构建结果里:
yaml---
title: "你好,世界"
date: 2026-10-25T10:00:00+08:00
draft: false
tags: ["随笔"]
categories: ["生活"]
---
这里开始写正文。
本地预览(-D 让草稿也显示):
bashhugo server -D --bind 0.0.0.0 --port 1313
bashhugo --minify --gc --cleanDestinationDir
产物在 public/。发布前确认三件事:
public/index.html 存在;public/index.html,或者扫一眼 HTML 里的链接是 https://example.com/... 而不是 http://localhost:1313/...;du -sh public 是几 MB 级别,而不是把 content/、.git 打进去了。bashssh ubuntu@1.2.3.4
sudo apt update && sudo apt install -y nginx
sudo mkdir -p /var/www/myblog
sudo chown -R $USER:$USER /var/www/myblog
sudo ufw allow 'Nginx Full' # 用 ufw 的话;云厂商安全组也要放行 80/443
部署目录用 /var/www/myblog,后续所有发布动作都只是往这个目录里写文件。建议用普通用户 + SSH 密钥登录,不要直接拿 root 跑 rsync。
新建 /etc/nginx/sites-available/myblog.conf:
nginxserver { listen 80; listen [::]:80; server_name example.com www.example.com; root /var/www/myblog; index index.html; location / { try_files $uri $uri/ =404; } # HTML 不缓存,改了立刻生效 location ~* \.html$ { add_header Cache-Control "no-cache"; } # 带哈希指纹的静态资源可以长期缓存;没有指纹的话把 30d 改小 location ~* \.(css|js|woff2?|png|jpe?g|webp|svg|ico)$ { expires 30d; add_header Cache-Control "public, max-age=2592000"; } error_page 404 /404.html; add_header X-Content-Type-Options "nosniff" always; gzip on; gzip_types text/css application/javascript application/json image/svg+xml; gzip_min_length 1024; }
启用并检查:
bashsudo ln -s /etc/nginx/sites-available/myblog.conf /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
此时用 IP 或 http://域名 应该已经能看到站点;看不到就先查安全组和 sudo systemctl status nginx,别急着弄证书。
bashsudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d example.com -d www.example.com
certbot 会自动改写 Nginx 配置、追加 443 段并配置 HTTP 跳转。证书 90 天有效,系统自带 systemd timer 会自动续期,验证一次即可:
bashsudo certbot renew --dry-run sudo systemctl list-timers | grep certbot
服务器 IP 假设为 1.2.3.4,最少需要两条记录:根域名 @ 和 www。
| DNS 服务商 | 配置入口 | 记录 | 注意点 |
|---|---|---|---|
| Cloudflare | 添加站点后把 NS 改到 Cloudflare,进入 DNS → Records | A @ → 1.2.3.4、A www → 1.2.3.4 | 橙色云朵 = 走 CDN 代理,源站必须持有效证书并把 SSL/TLS 模式设为 Full (strict);用 Flexible 配合源站强制跳转 HTTPS 会出现重定向循环。签证书期间建议先把记录设为灰云(DNS only),签发成功再开代理 |
| 阿里云 / 腾讯云 | 控制台 → 云解析 DNS → 解析设置 → 添加记录 | A @ → 1.2.3.4、A www → 1.2.3.4,TTL 600 | 国内服务器必须先完成备案,否则解析生效也访问不了;同一域名不要在两家同时改,先改 NS 再操作 |
| DNSPod | 域名解析 → 添加记录 → 主机记录填 @ / www,线路「默认」 | 同上,TTL 600 | 免费版够用;改完等 TTL 过期再验证,别连按刷新 |
验证(Windows 用 nslookup example.com):
bashdig +short example.com dig +short www.example.com curl -I http://example.com curl -I https://example.com
期望 curl -I https:// 返回 200,并且响应里有 server: nginx(Cloudflare 代理时会显示 cloudflare,属于正常)。
方案 A:本地构建后上传(推荐,服务器不用装 Hugo)
bashhugo --minify --gc --cleanDestinationDir rsync -avz --delete public/ deploy@1.2.3.4:/var/www/myblog/
--delete 很关键,否则删掉的文章在服务器上会一直残留。
两个 Windows 上的坑:
scoop install rsync 装一个,或者改在 WSL 里执行。bashtar -czf - -C public . | ssh deploy@1.2.3.4 'find /var/www/myblog -mindepth 1 -delete && tar -xzf - -C /var/www/myblog'
方案 B:服务器直接拉代码构建(适合写作机器不固定)
服务器安装 Hugo,git clone 到 /srv/myblog,发布时:
bashcd /srv/myblog
git pull
git submodule update --init --recursive
hugo --minify --destination /var/www/myblog
代价是服务器要装 Hugo 并保留一份源码,好处是任何一台设备只要 git push,在服务器上跑一条命令就能上线。
方案 C:GitHub Actions 构建后推服务器
在下一节的 Actions 流程里,把「上传到 Pages」那一步换成 rsync action,把服务器 SSH 私钥放进仓库 Secrets 即可。适合既想要 git push 自动发布、又想要自己服务器速度的场景。
baseURL 与实际域名、协议一致,且以 / 结尾hugo --minify 构建无报错,public/ 里有 index.html/var/www/myblog 属主是部署用户,Nginx 可读sudo nginx -t 通过,certbot renew --dry-run 通过https://example.com 与 https://www.example.com 都能打开,且都跳 HTTPS--delete,删文章后服务器不残留全程零成本:仓库即源码,GitHub Actions 构建,GitHub Pages 托管。
仓库名有两种,直接影响 baseURL:
<用户名>.github.io,访问地址是 https://<用户名>.github.io/blog),访问地址是 https://<用户名>.github.io/blog/把本地站点(含 hugo.yaml、content/、themes/ submodule)推上去。主题是 submodule,所以 checkout 时必须递归拉取,这一步漏了就会报「theme not found」。
新建 .github/workflows/hugo.yaml:
yamlname: Deploy Hugo site to Pages
on:
push:
branches: ["main"]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
defaults:
run:
shell: bash
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: "0.147.0" # 建议锁一个具体版本,避免某天构建突然变样
extended: true
- name: Setup Pages
id: pages
uses: actions/configure-pages@v5
- name: Build
env:
HUGO_ENVIRONMENT: production
run: hugo --minify --baseURL "${{ steps.pages.outputs.base_url }}/"
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./public
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
要点:
configure-pages 输出的 base_url 覆盖配置里的 baseURL,用户站和项目站都能自动适配,不必手改。hugo-version 锁版本;主题要求的 Hugo 版本如果更高,按报错提示往上调。fetch-depth: 0 让 Hugo 拿到完整 Git 历史,.Lastmod、更新时间排序才准。仓库 Settings → Pages → Build and deployment → Source 选 GitHub Actions。
推送一次到 main,Actions 里应出现一次构建;成功后 Pages 面板会给出版址。之前必须先选 Source,否则 deploy-pages 会报环境不存在。
Settings → Pages → Custom domain 填 blog.example.com,保存(会在仓库里生成 CNAME)。
DNS 加记录:
CNAME blog → <用户名>.github.ioA @ → 185.199.108.153、.109.153、.110.153、.111.153(四条都要加)等解析生效后回到 Pages 勾选 Enforce HTTPS,证书由 GitHub 自动签发,通常几分钟到几小时。
*.github.io 在国内访问不稳定,挂自己的域名(尤其再套一层 Cloudflare)能改善,但要接受「免费 CDN 国内节点质量一般」这个现实。如果博客要长期给国内读者看,服务器方案仍是主力。
| 现象 | 原因 | 处理 |
|---|---|---|
| 构建报 theme 找不到 | 没拉 submodule | Actions 里确认 submodules: recursive,仓库里有 .gitmodules |
部署步骤报 Get Pages site failed | Pages 的 Source 还不是 GitHub Actions | 去 Settings → Pages 改 Source 后重跑 |
| 页面能开但样式全丢 | baseURL 少了末尾 /,或项目站路径不对 | 交给 configure-pages 的 base_url 覆盖,别手写死 |
| 自定义域名一直 pending | DNS 未生效或被代理 | dig 确认记录,Cloudflare 代理先关掉再验证 |
| 文章顺序不对 | 浅克隆导致无 Git 历史 | fetch-depth: 0 |
| 现象 | 优先检查 |
|---|---|
| 访问 404 | baseURL、public/ 是否上传到正确目录、Nginx root 路径 |
| 访问 403 | 目录权限、SELinux、备案状态 |
| 样式 / 图片 404 | 控制台看请求路径;检查 baseURL 与资源指纹 |
| 证书签发失败 | 80 端口是否可达、域名是否解析到本机、Cloudflare 是否开橙云 |
| 改了文章不生效 | HTML 被缓存;Ctrl + F5,确认 Nginx 对 .html 设了 no-cache |
| 中文显示异常 | 配置 languageCode: zh-cn,文件本身用 UTF-8 |
| 日期错乱 / 文章排序怪 | front matter 的 date 带时区(如 +08:00) |
目标:在 PC、手机、任何浏览器上都能写,写完立刻发布,不出现「稿子散落在三个平台」的情况。
Git 仓库是唯一的内容源,写作 = 提交,发布 = 推送。
任何设备改 content/**/*.md → commit → push → GitHub Actions(或本地脚本)执行 hugo → Pages / 服务器目录更新 → 几十秒后线上可见
只要守住这一点,多少端都只是「客户端」的差异。
myblog/ ├── archetypes/default.md # 新文章模板 ├── content/ │ └── posts/ │ └── hugo-deploy/ │ ├── index.md # 一篇文章一个目录(page bundle) │ └── cover.jpg # 图片跟着文章走 ├── static/ # 全站共用文件(favicon 等) ├── themes/PaperMod/ # submodule ├── hugo.yaml └── .github/workflows/hugo.yaml
三条约定,能省掉后面 90% 的图片路径问题:
index.md,图片放同目录,引用写 。删文章时图片跟着目录一起删,手机上也不用记 static/images/xxx 这种长路径。hugo-deploy),日期只写在 front matter 里。这样改日期不会改链接。/posts/hugo-deploy/),起名时想好,别上线后再改。编辑 archetypes/default.md,让所有端 hugo new 出来的骨架一致:
yaml---
title: "{{ replace .File.ContentBaseName "-" " " | title }}"
date: {{ .Date }}
draft: true
tags: []
categories: []
---
draft: true 是安全阀:草稿不会被构建(hugo server -D 除外),可以先 push 再慢慢写,最后改成 false 才算发布。date 统一带时区(+08:00),跨设备、跨时区也不会算错。hugo new 命令时,直接复制一篇已有文章的 front matter 改标题和日期。| 方案 | 平台 | 优点 | 代价 |
|---|---|---|---|
| GitHub 网页 / GitHub Mobile | 全平台 | 零安装;改错字、发短文最快;上传图片直接选相册 | 长文编辑体验弱,Markdown 预览有限 |
| Obsidian + Git 插件 | iOS / Android / 桌面 | 本地 Markdown 库、双链、离线可写,插件可直接 commit & push | 手机端合并冲突要手动处理;附件路径需和 page bundle 约定对齐 |
| Working Copy | iOS | 完整 Git 客户端,clone / pull / push / diff 都有 | 推送功能需付费解锁,Android 没有对应体验 |
| Termux + git | Android | 可以本地 hugo server 预览,能力最全 | 门槛最高,要自己配环境 |
| IntelliJ IDEA / VS Code | 桌面 | 编辑体验最好,内置 Git,可本地实时预览 | 只在电脑前可用 |
实际最省心的组合:
hugo server -D 实时预览,写完在 IDE 里提交推送。content/posts/xxx/index.md,Commit 即发布。git push --force,它会把另一端的稿子直接抹掉。post: 文章标题,出问题能一眼定位到哪篇。hugo.yaml;这类改动放桌面做,出问题好回滚。draft: true,确定要发再改 false,避免写一半被 RSS 和搜索引擎抓到。桌面端脚本 deploy.sh(GitHub Pages 路线删掉最后一行 rsync):
bash#!/usr/bin/env bash
set -euo pipefail
msg="${1:-post: 更新}"
git pull --rebase
hugo --minify --gc --cleanDestinationDir
git add -A
git commit -m "$msg" || echo "没有需要提交的改动"
git push
# 服务器路线才需要
rsync -avz --delete public/ deploy@1.2.3.4:/var/www/myblog/
echo "已发布 $(date '+%F %T')"
日常流程就三句话:桌面写完 → ./deploy.sh "post: 标题" → 手机上打开域名确认。手机端则只需要在 GitHub 页面上点一次 Commit,剩下的交给 Actions。
本文作者:Weee
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!