前言
我最近完成了一项“大工程”:将个人博客从传统的 Typecho 平台迁移到了基于 Docker 的 Halo 2.x。
这次迁移并非简单的“搬家”,因为我面临着几个复杂的现实挑战:
SEO 权重保卫战:旧文章链接必须通过 301 永久重定向到新路径。
多项目共存:根目录下还有多个独立的 PHP 小工具(如 Speedtest、图片托管等)不能中断。
性能极致追求:利用手头 ARM 4H24G 的高配服务器,实现物理级动静分离。
在反复踩坑 403 错误和 Nginx 配置冲突后,我整理出了这套“终极方案”。

核心挑战与解决方案
1. URL 重定向,比想象中麻烦
Typecho 的文章我使用的路径是 /slug/,而 Halo 必须要有前缀,综合考虑之后,我决定使用 /p/slug。为了不让朋友们的收藏夹失效,也不让搜索引擎抓取到 404,我们需要利用 Nginx 的正则匹配实现自动化跳转。以下是需要避坑的地方:
有的 URL 末尾带斜杠,有的不带
分类页面、独立页面、文章页面的规则全都不一样
最坑的是,如果不小心把 /admin 也重定向了,后台直接进不去
我一开始用宝塔的反代插件自动生成,结果生成的 rewrite 规则优先级完全不对,要么 404,要么循环重定向。最后干脆放弃插件,直接手写 Nginx 配置。
后面又改回 archives 了,主题默认写死了,我太怕麻烦了...
2. 绕过“空目录 403 错误”
由于 Halo 运行在 Docker 容器中,宿主机的网站根目录通常是空的。Nginx 默认会尝试在本地找 index.php,找不到且禁止列出目录时就会报 403。
方案:在 Nginx 中对首页进行“精确匹配”,直接转发给后端,不经过本地文件系统。
3. 物理级动静分离
Halo 虽然强大,但用 Java 处理静态资源(图片、CSS)效率不如 Nginx。通过 Docker 的卷挂载(Volumes),我们可以让 Nginx 直接读取磁盘上的附件,速度提升显著。
通过 location ~ ^/(themes|plugins|attachments)/ 块,我们让 Nginx 直接从磁盘读取图片、字体和脚本。这不仅大幅降低了 Halo 容器的 I/O 压力,还通过 Access-Control-Allow-Origin "*" 解决了部分主题在预加载字体时可能出现的跨域警告。
4. 解决宝塔保存报错
宝塔面板在保存配置文件时,会扫描 #SSL-START 块内是否包含特定的 error_page 404 注释行。这也是一个需要注意的地方。
5. 404的问题与解决方案
默认情况下,Nginx 的 proxy_intercept_errors on 会直接抛出 Nginx 自带的 404.html。
方案通过 @seo_logic 逻辑:
先尝试帮找回旧文章(SEO 补全)。
如果确实找不到,则通过 proxy_intercept_errors off 再次转发,此时 Nginx 会“闭嘴”,让 Halo 后端吐出主题 404 页面。
终极 Nginx 配置文件
以下是经过多次迭代、能够完美运行在宝塔面板环境下的主配置文件(已脱敏)。
server
{
listen 80;
listen 443 ssl;
http2 on;
server_name yourdomain.com; # 替换为你的域名
index index.php index.html index.htm;
root /www/wwwroot/your_site_folder; # 替换为你的站点根目录
include /www/server/panel/vhost/nginx/extension/yourdomain.com/*.conf;
# ==========================================================
# 【1. 基础 MIME 类型】
# ==========================================================
include mime.types;
types {
application/javascript js cjs mjs;
image/svg+xml svg;
}
# ==========================================================
# 【2. SSL 配置区】(保持宝塔兼容性)
# ==========================================================
#SSL-START
#error_page 404/404.html;
ssl_certificate /path/to/your/fullchain.pem; # 替换为实际证书路径
ssl_certificate_key /path/to/your/privkey.pem; # 替换为实际密钥路径
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
# ... 其他 SSL 优化参数 ...
#SSL-END
# ==========================================================
# 【3. 业务路由分发】
# ==========================================================
# 首页直连
location = / {
proxy_pass http://127.0.0.1:8090;
proxy_set_header Host $http_host;
# ... 常用 Proxy Header ...
}
# 静态资源直读
location ~ ^/(themes|plugins|attachments)/ {
root /www/wwwroot/your_site_folder/data;
add_header Access-Control-Allow-Origin "*";
access_log off;
expires 30d;
try_files $uri @halo_proxy;
}
# 兼容本地 PHP 工具
location ~ ^/(tool_folder1|tool_folder2) {
try_files $uri $uri/ /index.php?$query_string;
include enable-php-83.conf;
}
location / {
try_files $uri $uri/ @halo_proxy;
}
# ==========================================================
# 【4. SEO 重定向映射】
# ==========================================================
rewrite ^/feed/?$ /rss.xml permanent;
rewrite ^/archive/?$ /archives permanent;
rewrite ^/cross/?$ /moments permanent;
# 标签与分类映射 (请根据实际情况替换 category_name)
rewrite ^/tag/([^/]+)/?$ /tags/$1 permanent;
rewrite ^/(category1|category2|category3)/?$ /categories/$1 permanent;
# ==========================================================
# 【5. 后端代理与错误拦截】
# ==========================================================
location @halo_proxy {
proxy_pass http://127.0.0.1:8090;
proxy_set_header Host $http_host;
# ... WebSocket 支持配置 ...
proxy_intercept_errors on;
error_page 404 = @seo_logic;
}
# ==========================================================
# 【6. SEO 自动补全逻辑】
# ==========================================================
location @seo_logic {
# 排除列表
if ($uri ~* "^/(archives/|categories/|tags/|moments|admin|api|login|rss|search|attachments/|themes/|plugins/)") {
set $fallback "yes";
}
if ($uri ~* "\.") {
set $fallback "yes";
}
if ($fallback != "yes") {
rewrite ^/([^/]+)/?$ /archives/$1 permanent;
}
# 终极兜底:显示后端自定义 404
proxy_pass http://127.0.0.1:8090;
proxy_intercept_errors off;
proxy_set_header Host $http_host;
}
# ==========================================================
# 【7. 系统错误页】
# ==========================================================
#ERROR-PAGE-START
error_page 502 /502.html;
# ...
#ERROR-PAGE-END
access_log /www/wwwlogs/yourdomain.com.log;
error_log /www/wwwlogs/yourdomain.com.error.log;
}
迁移后的避坑总结
不要依赖宝塔的反向代理插件:对于复杂的 301 跳转需求,插件生成的代码往往会产生优先级冲突。直接修改“配置文件”是最稳妥的选择。
正则排除很重要:在做文章重定向时,务必排除掉 /admin、/api 等 Halo 系统路径,否则你会发现后台无法登录。
权限检查:动静分离使用 alias 映射时,确保网站运行用户(通常是 www)对 Docker 挂载的物理目录有读取权限。
JVM 调优:如果你有像我一样充裕的内存(24G),别忘了在 docker-compose.yml 中给 Halo 分配足够的内存(如 -Xmx2048m),这样在处理高并发请求时会更从容。
结语
从 Typecho 到 Halo 的迁移,不只是博客程序的更换,更是对站点架构的一次全面梳理。通过 Nginx 的灵活配置,我们既能享受新程序带来的便利,又能通过“动静分离”和“重定向优化”保障站点的访问体验与搜索权重。
希望这篇指南能帮助你打造一个更稳固、专业的 Halo 博客。修改完成后,记得在宝塔面板中重载 Nginx 配置即可生效,必要时重启一下 Nginx!
本文所涉及的 Nginx 配置已在 1.28.3 版本下实测通过。
从 Typecho 转向 Halo 2.x:全能型 Nginx 架构与 SEO 无损迁移指南
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
赞赏支持
如果觉得文章对你有帮助,可以请作者喝杯咖啡 ☕
评论交流
欢迎留下你的想法