把文章和页面的 URL 收短了一层:
| 改动前 | 改动后 |
|---|---|
https://xiezitai.cn/article/why-self-host |
https://xiezitai.cn/why-self-host |
https://xiezitai.cn/page/links |
https://xiezitai.cn/links |
/article/、/page/ 这两个前缀对读者和搜索引擎都不携带任何信息,它只是当年「文章 / 页面」两张表各占一个路由的副产品。
URL 结构变了,但外面已经流转过的链接、搜索引擎里已有的收录不能失效。所以 /article/{slug} 与 /page/{slug} 保留为路由,命中后返回 301 而不是 302 —— 301 是「永久搬家」,权重会跟着走到新地址。
根级 slug 本质上是一条兜底路由 /{slug}。而 Spring MVC 里 @RequestMapping 的优先级(order 0)高于静态资源处理器(LOWEST_PRECEDENCE - 1),也就是说这条兜底路由会把 /admin.html、/favicon.svg 一并吃掉 —— 表现为「找不到文章 → 回首页」,整个后台直接打不开。
解法是在路径变量的正则里加负向断言,显式放行根级文件:
@GetMapping("/{slug:(?!admin\\.html$|index\\.html$|favicon\\.svg$|favicon\\.ico$|robots\\.txt$|sitemap\\.xml$|error$).+}")
这里还踩到第二个坑:PathPattern 的变量正则里不能出现 / —— 会被当成路径分隔符,直接抛 PatternParseException。所以只能写 .+;好在 PathPattern 本身就保证变量只吃一个路径段,/a/b 这种多段路径压根不会命中这条路由。
站内拼链接的地方有五六处(前台模板、分发正文尾注、开放 API 的 url 字段、sitemap、通知消息、后台列表),原来各写各的。这次统一到一个 SlugUtil.publicPath():
| 输入 | 输出 | 原因 |
|---|---|---|
why-self-host |
/why-self-host |
普通 ASCII 原样输出 |
你好 |
/%E4%BD%A0%E5%A5%BD |
真中文逐字节编码;空格是 %20 而不是 +(+ 在路径里是字面量,只有 query 里才代表空格) |
%e4%bd%a0%e5%a5%bd-legacy |
/%e4%bd%a0%e5%a5%bd-legacy |
已经是编码形态的老数据绝不再编一层 —— 再编会得到 %25e4…,被 Spring 的 StrictHttpFirewall 直接拦成 400 |
canonical 与 og:url,指向根级规范地址sitemap.xml 同步输出根级地址Location、/admin.html 与 /favicon.svg 未被兜底路由吞掉、sitemap 不再含 /article/、多段路径不被截胡、未命中的根级路径回首页/why-self-host、/links 返回 200;/article/why-self-host、/page/links 返回 301 且 Location 指向根级;/admin.html 返回 200迁移提示:如果你有自定义的跳转规则,或者哪里硬编码过
/article/前缀,跟着换成根级即可;老地址会一直 301 过去,不用急着改。