为什么不用托管平台
托管平台的静态站部署体验确实好:推代码、自动构建、全球 CDN。但它有三个隐性成本:
- 域名和流量被绑定——想换平台就要动 DNS。
- 构建分钟数有额度——博客这种低频更新场景,额度基本用不完,但一旦要跑重构建(比如图片处理),就开始算钱。
- 调试不透明——重定向、缓存、响应头出问题的时候,你只能猜。
而一台最低配的云服务器(2 核 4G,一年几十块)能承载的静态站,量级远超个人博客的需求。nginx 处理静态文件的能力,是被严重低估的。
一、生成器:三个依赖就够了
核心逻辑其实很短:Markdown → HTML → 写文件。需要的库只有三个:
pip install markdown pygments jinja2
| 库 | 作用 |
|---|---|
markdown |
解析 Markdown,产出 HTML |
pygments |
代码块语法高亮 |
jinja2 |
套用页面模板 |
构建脚本的核心就三步:
import markdown, yaml
from jinja2 import Environment, FileSystemLoader
from pathlib import Path
# 1. 读文章 + 解析 frontmatter
def load_post(path):
raw = path.read_text(encoding="utf-8")
meta, body = split_frontmatter(raw) # --- YAML --- 分隔
md = markdown.Markdown(extensions=["extra", "toc", "codehilite", "tables"])
return {**meta, "html": md.convert(body), "toc": md.toc}
# 2. 套模板
env = Environment(loader=FileSystemLoader("templates"), autoescape=True)
page = env.get_template("post.html").render(post=load_post(p))
# 3. 写盘
out = Path("dist/posts") / slug / "index.html"
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(page, encoding="utf-8")
关键设计取舍:输出目录结构用 posts/<slug>/index.html 而不是 posts/<slug>.html。这样 URL 里没有 .html 后缀,将来想换生成器也不用改任何链接。
二、nginx 配置:短,但每一行都有用
server {
listen 80;
listen [::]:80;
server_name mirror-works.net www.mirror-works.net;
root /home/admin/blog/dist;
index index.html;
# 无后缀 URL → 找同名目录
location / {
try_files $uri $uri/ =404;
}
error_page 404 /404.html;
# 静态资源长缓存:文件名带指纹才敢这么干,
# 否则改样式用户看不到
location ~* \.(css|js|svg|png|jpg|webp|woff2)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
}
gzip on;
gzip_min_length 512;
gzip_types text/plain text/css application/javascript
application/json text/xml application/xml image/svg+xml;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
}
四个容易踩的点:
try_files的顺序必须是$uri→$uri/,反过来目录会优先,性能更差。gzip_types不会自动包含 text/html——nginx 默认已经为text/html开了 gzip,重复写反而会覆盖掉默认值。- 缓存时间要和构建策略匹配。 如果 CSS 文件名里没有内容哈希(hash),
max-age=604800会让你改完样式刷新十次都看不到效果。稳妥做法是把缓存降到 1 小时,或者加?v=<构建时间戳>。 add_header在location里会覆盖外层同名头,所以要么都写always,要么都放同一层。
三、HTTPS:交给 Cloudflare
自己签 Let’s Encrypt 证书需要 80/443 都通、还要配 certbot 的自动续期。如果域名本来就托管在 Cloudflare,直接开代理模式(小云朵变橙)更省事:
- 源站只需要暴露 80 端口
- TLS 在边缘终结,证书自动续
- 顺手拿到 DDoS 防护和 CDN 缓存
唯一的注意点:Cloudflare 的 SSL 模式必须从 Flexible 改成 Full 或 Full (strict),否则源站到边缘这一段是明文,而且会触发重定向循环。
# 验证 HTTPS 与响应头
curl -sI https://mirror-works.net | grep -iE "http/|server|cf-cache-status|strict-transport"
四、自动化:一条 cron 解决发布
写文章和发布彻底解耦——文章就是仓库里的 .md 文件,发布是构建:
# 每 10 分钟检查一次是否有新 commit,有就重建
*/10 * * * * cd /home/admin/blog && \
git pull -q --ff-only && \
python3 build.py >> /var/log/blog-build.log 2>&1
如果想要更快的反馈,加个 inotifywait 监听 posts/ 目录,文件一变就重建:
inotifywait -m -e close_write -e moved_to posts/ | while read _ _ file; do
case "$file" in *.md) python3 build.py ;; esac
done
五、这套方案的真实边界
诚实地讲,它不适合:
- 日均百万 PV 的站点——没有分布式缓存,单机的连接数有上限
- 需要动态能力的场景——评论、搜索服务端渲染、用户系统,这些得另外搭
- 需要全球低延迟——单一地域的服务器在跨洲访问时延迟会明显
但对一个个人技术博客来说,它的性价比高得离谱:没有构建额度、没有平台锁定、没有隐藏账单,出问题时你手上有一整个 shell 可以排查。
可控性不是性能指标,但它是你在半夜三点出问题时唯一在乎的东西。