字词数:4768   ✧   阅读量:   ✧  

以下按钮可供你方便地引用本文。URL 中即使域名变化,其后的路径也将始终指向原本的内容。
https://css.celestialy.top/p/feda224a0
[浅谈网站 URL 管理与预防 404 | clsty 的网络空间站](https://css.celestialy.top/p/feda224a0)
[[https://css.celestialy.top/p/feda224a0][浅谈网站 URL 管理与预防 404 | clsty 的网络空间站]]

互联网的世界,一刻亿变。而有些 URL,变着变着就成为了 404。

如何防止个人网站因即技术或结构变更而导致同一页面的旧 URL 失效而 404?本文对此进行简单讨论。

注:本文仅以 Hugo 作为具体实现的例子,而各方案的设计理念则并不局限于 Hugo。

404 是最常见的 HTTP 状态码之一,它表示“Not Found”,通常意味着链接对应的页面不(再)存在。

笔者在阅读文章时,常会翻阅其中的参考链接,偶尔会遇到 404 报错。

  • 常见原因例如,网站关停,域名变更,页面被移除,等等。
  • 但也有一种本应当能避免的 404:网站正常运行,域名没有变化,文章依然存活,但是页面的 URL 路径更新了。
    • 这种情形常见于个人网站,且年代越是久远则越容易发生。
    • 而博客园、知乎等内容平台上文章的 URL 则相对稳定,404 的原因主要是文章被删除1

因此,在搭建网络空间站时,笔者就在考虑解决这个问题的方案。也即,我希望自己的文章不会因为名称变化2、结构变化3等原因,使得别人引用本文的链接变成 404。

方案对比

slug

在网络(web)上,slug4 是页面 URL 中按 / 划分的最后一个部分,它通常用小写字母或数字以及连字符组成,比如 https://example.com/articles/latest/how-to-keep-fit 其中的 how-to-keep-fit 就是 slug。

Hugo 可以为每个页面单独指定 slug。

比如下面示例导言区所生成的页面,就具有形如 https://example.com/articles/latest/emacs-org-better-latex 的 URL:

1---
2title: 为 Emacs 的 Text Mode 优化 LaTeX 工作流
3slug: emacs-org-better-latex
4---

这样做的所谓的“好处”之一是,URL 友好易读,不需要网页的标题就能得知所链接的页面的主题。

不过,尽管 slug 在很多资料中被高度认可(尤其是英语网站),我们需要认识到,它的这种好处是存疑的。

  • 首先,不靠网页标题就得知链接页面主题,并不算是一个很有必要的特性。相反,假设某个笔记软件限制人们为链接添加标题或注释性文本,通常只能说明它的这个功能非常有局限性。
  • 其次,slug 只能使用英语或拉丁语,对于非英语使用者可能根本算不上易读。

而 slug 与本文主题相关的优点是,它与标题相互独立 —  — 你在变动标题时,不会导致路径也发生变动,所以用 slug 定义的路径是相对稳定的。

但是,这种稳定性非常有限,因此这个方案仍然会导致以下的一些问题。

与标题类似,slug 仍然是对页面内容的概括。

假设你写了一篇文章来说明在 Emacs Org-mode 中优化 LaTeX 工作流的一种方法,这个 slug 就可以叫做 emacs-org-better-latex 。而这样一个含义过于具体的 URL 路径,可能随文章更新而与实际内容脱节。为了解除这种脱节,就又需要更新 slug,这就导致路径变动,使得旧的链接返回 404。

比如对于上面假设的那篇文章,你又发现了它稍加拓展就能对 Emacs 中的各种 Text Mode 都适用,比如 AucTeX、Markdown 等等,于是你将 slug 更新为 emacs-text-better-latex —  — 此时旧的 slug 即 emacs-org-better-latex 就失效了。

slug 本身仅针对 URL 路径的最后一部分。

如果你的文档形成了某种组织结构,每一篇文档都是“路径树”的某个分支下的一片叶子,一旦你需要调整这篇叶子的位置,或者调整这棵树本身,都会导致 URL 的路径变动5(旧的链接从此 404),对此 slug 也是无能为力的。

在计算机领域有句话叫做“面向接口编程,而非面向实现编程”6。 slug 所存在的问题本质上亦与之类似:过于具体,而不够抽象。

URL 与编号

就 slug 存在的问题去思考,我们可以很直接地得到解决方法:不再使用 slug,而是使用与内容毫无关联的“编号”作为 URL 路径的最后一部分,而整个站点中 URL 的前面所有部分都可以统一,比如 https://example.com/posts/编号

只要是与页面的后续变化解耦、不会重复、可以自动生成的字符串,都适合用作编号,比如:

  • 随机字符串
  • 首次发布时间
  • 从 1 开始的递增数

Hugo 也支持为每个页面单独指定 URL,例如为了让某个页面具有形如 https://example.com/posts/2022-02-22.22.22.22 的 URL,则其导言区可以是:

1---
2title: 为 Emacs 的 Org Mode 优化 LaTeX 工作流
3url: /posts/2022-02-22.22.22.22
4---

这个方案可以轻松解决 slug 带来的问题。

  • 每个 URL 在表面的含义上均与页面内容无关,所以也不必随内容的主题改变而变动,更不会导致 404。
  • 这种固定结构的 URL 并不妨碍你用文档树来组织页面,因为在 Hugo 中,除非明确指定 URL,否则 URL 是由文档树结构所“决定”7的,而并不是由 URL 反过来决定文档树结构。
    • 那么决定文档树的组织是什么呢?答案是导言区的 menu 参数。
    • 每个父母节点都具有参数 menu.identifier,而要将某个页面作为它的子节点则只需要让这个页面的 menu.parent 的值与父母节点的 menu.identifier 的值相同。
    • 通过这种机制,就能在目录里形成树状的组织结构,它并不影响我们显式地指定页面的 URL。

编号作为 slug 的补充

为 URL 启用编号已经是相当方便且稳定的解决方案了;但有一些人可能还是喜欢 slug 的一些特性,想着“我全都要”。

对此,Hugo 的 alias 提供了一种解决方案。

  • 每个页面都可以在导言区配置多个 alias 路径,置于参数 aliases 中。
  • 当浏览器访问任一个 alias 链接时,都会跳转(被重定向)到页面本身的固有地址。8

如下示例导言区所生成的页面,具有形如 https://example.com/articles/latest/emacs-org-better-latex 的固有地址,也同时具有会被重定向到固有地址的跳转链接 https://example.com/posts/2022-02-22.22.22.22

1---
2title: 为 Emacs 的 Text Mode 优化 LaTeX 工作流
3slug: emacs-text-better-latex
4aliases:
5- /posts/2022-02-22.22.22.22
6---

这样一来,不仅固有地址使用 slug 变得“可读”了起来,也可以在页面开头提醒用户引用跳转链接来防止 404。

但是,这样做表面上两全其美,实际上会引出一些更麻烦的问题:

  • 为了让用户确实地引用那个跳转链接,而不是显示在浏览器地址栏里的固有地址,如前面所提到的,你需要非常醒目地在页面中告知用户“请使用跳转链接来引用/收藏”,还需要写出理由 —  — 这无疑降低了交互体验。
  • 浏览器的收藏功能以及部分插件会默认使用地址栏里的 URL 即固有地址,而用户现在只能手动将它改为你的跳转链接了。在大多数情况下,这非常麻烦。
  • 当用户在其他网站上发贴时,所粘贴的跳转链接可能会被网站提供的在线编辑器自动重定向到固有地址。
  • 最为关键的是,在地址栏之外,slug 的优点仅在用户放弃 URL 稳定性、接受 404 风险、进而选择了固有地址的前提下才能有所体现 —  — 这真的是“两全其美”吗?9

编号与 slug 的真正结合

前面在由编号作为 slug 补充的方案中,我们只是简单地添加含编号 URL 到 aliases 中,而固有地址仍采用 slug,并且导致了不少问题。

思路

下面我们对这种方案进行改进,让编号与 slug 的优点更紧密地结合起来。10

  • 将这样一个同时包含编号与 slug 的 URL 作为固有地址: https://example.com/posts/2022-02-22.22.22.22/emacs-org-better-latex
  • 当 slug 内容与固定地址的不一致时(比如变成 https://example.com/posts/2022-02-22.22.22.22/emacs-text-better-latex 时),始终跳转到固定地址。

这样一来,也就能同时实现可读性,以及预防 404 的稳定性了。

假如能为 alias 使用通配符

思路俱备,只欠实现。

假如我们能为 alias 使用通配符的话,可以这样写导言区:

1---
2title: 为 Emacs 的 Text Mode 优化 LaTeX 工作流
3url: /posts/2022-02-22.22.22.22/emacs-text-better-latex
4aliases:
5- /posts/2022-02-22.22.22.22
6- /posts/2022-02-22.22.22.22/*
7---

但实际上 Hugo 不支持 alias 通配符,因为 alias 的实现原理是在指定路径生成一个 HTML 文件,并在文件中实现跳转。对于通配符来说,需要生成的 HTML 文件数量几乎是无限个11,这是不切实际的。

那么为什么……?

说到这里,你可能会意识到,本方案可能无法单方面地从静态站点或者说 Hugo 这一侧来解决,而需要考虑配置后端的软件(nginx、apache 等)或平台(GitHub Pages 等),因为利用它们才能实现作用于无数种可能性之上的跳转规则。

但是,如果你对静态站点的 404 配置有一定经验,你可能会意识到一个奇怪的事实:

既然如此,为什么我们可以自定义 404 页面呢? 要知道,404 本身只是由 HTTP 协议所返回的状态码,它并不必然意味着任何跳转。这里的跳转必然对应着某种规则,而它显然不是由 Hugo 用类似于 alias 的机制实现的,而只能是 —  — 

是的,这正是由后端软件或平台所实现的跳转规则。

  • 例如,若需要让 nginx 使用自定义 404 页面,则需添加配置 error_page 404 = /404.html;
  • 而大部分的静态站点托管平台(如 GitHub Pages)则默认启用类似的规则,所以不需要你手动配置。

具体实现

这就给了我们一个思路:我们可以在 404 页面用 Javascript 来处理链接,实现跳转规则。

以 Hugo 为例,首先编辑你在 /content 目录下所有文章的导言区,12

  • 确保每篇文章的 URL 路径都为 /posts/编号/slug
  • 且列表 aliases 至少含有一个元素 /posts/编号
  • 当然,你需要根据文章将上面所说的“编号”和“slug”替换为正确的值。

例如:

1---
2title: 为 Emacs 的 Text Mode 优化 LaTeX 工作流
3url: /posts/2022-02-22.22.22.22/emacs-text-better-latex
4aliases:
5- /posts/2022-02-22.22.22.22
6---

请仔细检查,确保链接已正确工作(比如试着访问 https://example.com/posts/2022-02-22.22.22.22 看看会不会跳转到固有地址 https://example.com/posts/2022-02-22.22.22.22/emacs-text-better-latex),再继续以下步骤。

新建 /layouts/404.html ,内容如下:

 1<!DOCTYPE html>
 2<html lang="{{ .Site.LanguageCode }}">
 3  <head>
 4    {{- partial "head.html" . -}}
 5    <meta charset=utf-8>
 6    <meta name=robots content="noindex">
 7    <script>
 8      var currentPath = window.location.pathname;
 9      var regex = /\/posts\/([^\/]+)\/([^\/]+)\/?/;
10      if (regex.test(currentPath)) {
11        var matches = currentPath.match(regex);
12        window.location.replace("/posts/" + matches[1]);
13//    } else { window.location.href = "/404-error/";
14      }
15    </script>
16  </head>
17  <body>
18    <div>
19      404 Not Found.
20    </div>
21  </body>
22</html>

最后,如果你已在 /content 目录下自定义了 404 页面,则它优先于 /layouts/404.html ,会使之失效。为了解决这个问题,请依次执行以下步骤:

  • 去掉 /layouts/404.html} else { window.location.href = "/404-error/"; 这一行开头的注释符号13
  • 将自定义 404 页面在 content 目录下的路径从 /content/404/ 改为 /content/404-error/,或从 /content/404.md 改为 /content/404-error.md
  • 在自定义 404 页面的 Markdown 源文件的导言区,指定参数 url 的值为 /404-error,并且确保没有多余的 alias 占用 /404.html/404

这样就完成了。比如访问 https://example.com/posts/2022-02-22.22.22.22/emacs-org-better-latex 时应该会跳转到固有地址 https://example.com/posts/2022-02-22.22.22.22/emacs-text-better-latex

另一种方法

这里再给出一种不依赖 Javascript 但有局限性的折衷方法(不推荐):每次更新 slug 时,都手动将旧的 URL 路径添加到 aliases 中。

例如对于前面提到过的例子(其中 emacs-org-better-latex 是旧的 slug)则可以这样写导言区:

1---
2title: 为 Emacs 的 Text Mode 优化 LaTeX 工作流
3url: /posts/2022-02-22.22.22.22/emacs-text-better-latex
4aliases:
5- /posts/2022-02-22.22.22.22
6- /posts/2022-02-22.22.22.22/emacs-org-better-latex
7---

这种方法不涉及对服务端软件/平台的配置,但很难被自动化;而如果每次都要手动操作,则可能遗漏步骤。

对于个人博客网站,目前流行的方案仍然是 slug。含有 slug 的链接与标题相独立,也有一定的可读性,但却常常导致 404。

为了预防 404,最简单且足够稳定的方法是,为每个页面使用单独的编号,并指定它的 URL 为 https://example.com/posts/编号 或类似格式。这种方案也被广泛地应用于各大内容平台。14

本文也另外提出了一种利用编号来补足 slug 不稳定之处的方案,但缺点较多。

最后提到的结合编号与 slug 的方案则兼具各方优点,也是笔者最为推荐的,只是当前采用此方案的网站相对少见一些。15


  1. “你似乎来到了没有知识存在的荒原” ↩︎

  2. 比如假设有路径为 /blog/latex-in-hugo 的一篇文章,因添加了更多内容,更改标题之后路径变为 /blog/latex-in-web 。 ↩︎

  3. 比如从散文区转移到文档区,或从文档区的一个分类转到另一个分类。 ↩︎

  4. slug 的原意为“铅字条”,即一种刻有文字的短小的金属条,出现于旧时代的印刷技术中。 ↩︎

  5. 比如,从 docs/A/foo-foo 变为 docs/B/foo-foo ↩︎

  6. 这句话亦有常用的英文表述:Program to an interface, not an implementation. ↩︎

  7. 实际上在未明确指定 URL 时,真正决定 URL 的是 md 文件在 content 目录中的位置;但为了管理方便,一般会使文档树的结构与 content 目录中的结构保持一致。 ↩︎

  8. 固有地址可由 slug 或 url 配置。 ↩︎

  9. 至少相比单纯的 slug 方案,这确实给了用户更多选择,但无论哪种选择都不够好。 ↩︎

  10. 这并非我的独创。比如 Stack Overflow 就已经在使用这种方案了。 ↩︎

  11. 几乎”的意思是,理论上来说,所需 HTML 文件的数量取决于 URL 路径的长度限制以及可用的字符种数,所以是一个非常巨大但仍然有限的整数值。 ↩︎

  12. 这里的 /content 是相对于你的 Hugo 工程的根目录而言的,而不是相对于你所使用的操作系统的 /;后文的 /layouts 等也是同理,不再赘述。 ↩︎

  13. 在 Javascript 中,注释符号是 //。 ↩︎

  14. 目前的例子有博客园、知乎等。 ↩︎

  15. 目前的例子有本站、Stack Overflow 等;基于 Discourse 的 Emacs China 当前也采用类似方案。 ↩︎