重定向旧博客网站

旧的博客托管在 Vercel 上,速度和便利性都不错,尤其对我这种一丁点钱都不愿付的用户来说。

shimejisim_26.webp
图1  就是管理面板颇有 AI slop 风味 《蘑菇拟态日常》贴纸包

我在迁移之后没有立马关掉 Vercel 服务,因为旧博客上还有许多笔记内容没搬过来,而且以前在各处留下了笔记链接,万一还有人查看,就会碰上令人沮丧的 404。

比 404 page 更沮丧的是满屏白色的 404 page。

从 Vercel 文档中,我找到了一种方法,可以通过 vercel.json 文件定义站点的重定向逻辑。我只要把原有的博客链接重定向到对应的新链接,而还未迁移的笔记就直接重定向到本站首页,这样人们就不会迷路了。

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "redirects": [
    { "source": "/me", "destination": "/profile.html", "permanent": false }
  ]
}

为此,我需要获得所有旧 URL 和对应的新 URL

旧站点使用 Material for MkDocs 的内置博客插件,它有某种 URL 生成逻辑,我没有心情去摸清楚所有的规则再从文章标题重新生成。

我的思路是:既然旧站点还活着,那就可以发送 HTTP 请求,获得博客归档页面 HTML,从中解析出每条文章的链接。实际上,根本不需要任何爬虫框架,cURL 和 ripgrep 就足够了:

$ curl -s https://13m0n4de.vercel.app/blog/archive/{2024,2025,2026}.html \
    | rg '"toclink" href="\.\./(\d+/\d+/\d+/.*)"' -o -r 'https://13m0n4de.vercel.app/blog/$1'
/blog/2024/12/06/ricing-my-arch-linux.html
/blog/2024/11/09/coffee-and-random-thoughts-hackergame-2024-.html
/blog/2024/06/16/hackerrank-in-racket-part-3.html
/blog/2024/06/13/hackerrank-in-racket-part-2.html
/blog/2024/06/11/hackerrank-in-racket-part-1.html
/blog/2024/05/23/wireworld-simulator-using-the-raylib-part2.html
/blog/2024/05/20/wireworld-simulator-using-the-raylib-part1.html
/blog/2024/05/12/bad-apple-on-lemoncore.html
/blog/2024/04/07/search-engines-in-2024-will-spaces-still-matter.html
/blog/2024/02/25/i-need-a-done-list-not-a-todo-list.html
/blog/2025/11/06/using-djot-instead-of-markdown-in-mkdocs.html
/blog/2025/10/23/functional-pipeline-programming-in-python.html
/blog/2025/07/20/markdown-to-pdf-with-pandoc-and-typst.html
/blog/2025/05/28/refactor-my-quiz-app.html
/blog/2025/04/01/malware-analysis-powerghost.html
/blog/2025/01/30/cve-roulette-1-pngcheck.html
/blog/2026/02/28/saya-lang-loop-break-value.html
/blog/2026/01/23/hello-saya-lang.html

新博客的 URL 等同于 https://13m0n4de.pages.dev/posts/ + 文件名称,博客迁移时没修改过文件名,所以列出文件名,用 sed 替换再替换就可以得到新的 URL

$ ls posts/ | sed -E 's/\.(md|dj)$//' | sed 's#^#https://13m0n4de.pages.dev/posts/#'
https://13m0n4de.pages.dev/posts/bad_apple_on_lemon_core
https://13m0n4de.pages.dev/posts/cve_roulette_pngcheck
https://13m0n4de.pages.dev/posts/exploring_the_hare_part1
https://13m0n4de.pages.dev/posts/functional_pipeline_programming_in_python
https://13m0n4de.pages.dev/posts/hackergame2024
https://13m0n4de.pages.dev/posts/hackerrank_in_racket_part1
https://13m0n4de.pages.dev/posts/hackerrank_in_racket_part2
https://13m0n4de.pages.dev/posts/hackerrank_in_racket_part3
https://13m0n4de.pages.dev/posts/hello_saya_lang
https://13m0n4de.pages.dev/posts/i_need_a_done_list_not_a_todo_list
https://13m0n4de.pages.dev/posts/including_c_files_over_https
https://13m0n4de.pages.dev/posts/malware_analysis_powerghost
https://13m0n4de.pages.dev/posts/markdown_to_pdf_with_pandoc_and_typst
https://13m0n4de.pages.dev/posts/refactor_my_quiz_app
https://13m0n4de.pages.dev/posts/replacing_markdown_with_djot_in_mkdocs
https://13m0n4de.pages.dev/posts/ricing_my_arch_linux
https://13m0n4de.pages.dev/posts/saya_lang_loop_break_value
https://13m0n4de.pages.dev/posts/search_engines_in_2024_will_spaces_still_matter
https://13m0n4de.pages.dev/posts/wireworld_simulator_using_the_raylib_part1
https://13m0n4de.pages.dev/posts/wireworld_simulator_using_the_raylib_part2

甚至,还能把它们管道到 httpx 批量验证有效性:

$ ls posts/ | sed -E 's/\.(md|dj)$//' | sed 's#^#https://13m0n4de.pages.dev/posts/#' | httpx -sc
https://13m0n4de.pages.dev/posts/hackerrank_in_racket_part1 [200]
https://13m0n4de.pages.dev/posts/refactor_my_quiz_app [200]
https://13m0n4de.pages.dev/posts/wireworld_simulator_using_the_raylib_part1 [200]
https://13m0n4de.pages.dev/posts/hackergame2024 [200]
https://13m0n4de.pages.dev/posts/search_engines_in_2024_will_spaces_still_matter [200]
https://13m0n4de.pages.dev/posts/cve_roulette_pngcheck [200]
https://13m0n4de.pages.dev/posts/hackerrank_in_racket_part3 [200]
https://13m0n4de.pages.dev/posts/saya_lang_loop_break_value [200]
https://13m0n4de.pages.dev/posts/i_need_a_done_list_not_a_todo_list [200]
https://13m0n4de.pages.dev/posts/ricing_my_arch_linux [200]
https://13m0n4de.pages.dev/posts/bad_apple_on_lemon_core [200]
https://13m0n4de.pages.dev/posts/markdown_to_pdf_with_pandoc_and_typst [200]
https://13m0n4de.pages.dev/posts/hackerrank_in_racket_part2 [200]
https://13m0n4de.pages.dev/posts/wireworld_simulator_using_the_raylib_part2 [200]
https://13m0n4de.pages.dev/posts/functional_pipeline_programming_in_python [200]
https://13m0n4de.pages.dev/posts/hello_saya_lang [200]
https://13m0n4de.pages.dev/posts/exploring_the_hare_part1 [200]
https://13m0n4de.pages.dev/posts/replacing_markdown_with_djot_in_mkdocs [200]
https://13m0n4de.pages.dev/posts/malware_analysis_powerghost [200]
https://13m0n4de.pages.dev/posts/including_c_files_over_https [200]

有了两者的 URL,最后就是将它们对应起来。比如说,与 hackerrank-in-racket-part-1.html 最为相似的是 hackerrank_in_racket_part1,所以你应该去到这里,以肉眼判断的话。

shimejisim_43.webp
图2  难道这不是直接…… 《蘑菇拟态日常》贴纸包

我知道我知道……看上去就只是连字符和下划线区别而已,直接应用某些替换规则就能进行字符串比较了。但是,还会有更复杂的情况,直到某个例子逼着我重新写出 URL 生成逻辑,这就又绕回去了。

我想的办法是计算相似性,算法不重要(它们是比较“规整”的数据,各自区别也都足够大),只要足够自动化、放过我视疲劳的双眼就行了。

如果坚持命令行管道,那么 fzf 算是个办法,但命令最后肯定不是串联的,会复杂到需要写一个小脚本。

而如果非要写脚本,直接写出一个 Python 脚本生成 vercel.json 内容岂不更好:

import difflib
import json

with open("old.txt") as o, open("new.txt") as n:
    old = o.readlines()
    new = n.readlines()

redirects = []
for url in old:
    m = difflib.get_close_matches(url, new, n=1, cutoff=0.3)
    redirects.append(
        {"source": url.rstrip(), "destination": m[0].rstrip(), "permanent": True}
    )
redirects.append(
    {"source": "/(.*)", "destination": "https://13m0n4de.pages.dev/", "permanent": True}
)

vercel = {"$schema": "https://openapi.vercel.sh/vercel.json", "redirects": redirects}

print(json.dumps(vercel))

或者,还可以试一下纯列表推导式风格:

print(
    __import__("json").dumps(
        {
            "$schema": "https://openapi.vercel.sh/vercel.json",
            "redirects": [
                {
                    "source": url.rstrip(),
                    "destination": __import__("difflib")
                    .get_close_matches(url, new, n=1, cutoff=0.3)
                    .pop()
                    .rstrip(),
                    "permanent": True,
                }
                for old in [open("old.txt")]
                for new in [open("new.txt").readlines()]
                for url in old
            ]
            + [
                {
                    "source": "/(.*)",
                    "destination": "https://13m0n4de.pages.dev/",
                    "permanent": True,
                }
            ],
        }
    )
)
shimejisim_6.webp
图3  哇,好酷 《蘑菇拟态日常》贴纸包

好吧,也就一般酷,有人给我捧哏就会显得更酷一些。

最后的最后将 vercel.json 放在 docs/ 里,重新发布一次旧网站,Vercel 的重定向规则就生效了。

也许您从旧网站跳转而来,点开了这篇文章,现在您知道发生什么事了。

Copyright © 2024 13m0n4de · CC BY-NC 4.0