前言

我最近完成了一项“大工程”:将个人博客从传统的 Typecho 平台迁移到了基于 Docker 的 Halo 2.x。

这次迁移并非简单的“搬家”,因为我面临着几个复杂的现实挑战:

  1. SEO 权重保卫战:旧文章链接必须通过 301 永久重定向到新路径。

  2. 多项目共存:根目录下还有多个独立的 PHP 小工具(如 Speedtest、图片托管等)不能中断。

  3. 性能极致追求:利用手头 ARM 4H24G 的高配服务器,实现物理级动静分离。

在反复踩坑 403 错误和 Nginx 配置冲突后,我整理出了这套“终极方案”。


image
Typecho 转 Halo 2.X

核心挑战与解决方案

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;
}

迁移后的避坑总结

  1. 不要依赖宝塔的反向代理插件:对于复杂的 301 跳转需求,插件生成的代码往往会产生优先级冲突。直接修改“配置文件”是最稳妥的选择。

  2. 正则排除很重要:在做文章重定向时,务必排除掉 /admin、/api 等 Halo 系统路径,否则你会发现后台无法登录。

  3. 权限检查:动静分离使用 alias 映射时,确保网站运行用户(通常是 www)对 Docker 挂载的物理目录有读取权限。

  4. JVM 调优:如果你有像我一样充裕的内存(24G),别忘了在 docker-compose.yml 中给 Halo 分配足够的内存(如 -Xmx2048m),这样在处理高并发请求时会更从容。

结语

从 Typecho 到 Halo 的迁移,不只是博客程序的更换,更是对站点架构的一次全面梳理。通过 Nginx 的灵活配置,我们既能享受新程序带来的便利,又能通过“动静分离”和“重定向优化”保障站点的访问体验与搜索权重。

希望这篇指南能帮助你打造一个更稳固、专业的 Halo 博客。修改完成后,记得在宝塔面板中重载 Nginx 配置即可生效,必要时重启一下 Nginx!


本文所涉及的 Nginx 配置已在 1.28.3 版本下实测通过。