不必复杂,静态博客也可以做短链
立泉为什么需要短链
https://mudan.me/post/original/2022/05/17/不必复杂-静态博客也可以做短链.html
这是我博客里一篇文章的完整链接,除去协议和域名,后面一长串包含文章类型 “original”、写作时间 “20220517” 和文章标题“不必复杂-静态博客也可以做短链”,它们共同构成一个指向此 HTML 文档的 URL,很详细也很长。
这个博客站点是用 Jekyll 生成,它在转换 Markdown 文章到静态网页时支持设定输出的 URL 结构,确实能做到很短。只需给每个 Markdown 文章都指定一个自定义 Front Matter,然后用它做输出文件名,可以实现类似 https://mudan.me/lxzlysh.html
的短链。
# Jekyll 配置文件 _config.yml
# 指定 HTML 页面的输出目录格式,即文章链接的 URL 格式
# https://mudan.me/post/original/2022/05/17/不必复杂-静态博客也可以做短链.html
permalink: /post/:categories/:year/:month/:day/:title:output_ext
# 可以很短,比如 https://mudan.me/bfjbyzd.html
permalink: /:custom_front_matter:output_ext
这样的短链不包含任何文章相关信息,只有一串手动指定的字符,会不会碰撞是一个问题,另一个问题是permalink
实际上是 Jekyll 文章页面的输出目录。我的长链是把文章输出到/post/original/2022/05/17/
目录下按类型、日期分类存储,而这种短链显然把所有文章都输出到根目录堆成一团。如果硬要做“短结构”确实可以是/p/o/22/5/17/
,但结果既不如长链清晰也不再是通常意义上的“短链”。
理解为什么链接这么长之后回到一开始的问题,为什么需要短链?
Universal Resource Locator 即 URL 统一资源定位符,是一个能在茫茫网络中准确指向一个指定资源的字符串。当把一篇博文的 URL 分享出去,外部即可通过它访问到文章页面,就是这么“神奇”,而我则是遇到几个不太“神奇”的问题。
问题
不友好的中文解析
我的博文 URL 包含中文(文章标题),而执行网页跳转的 HTTP 协议并不允许 URL 中含有 ASCII 码表以外字符,所以浏览器在访问中文 URL 时会先用 URL Encoder 将其转换为合法 ASCII 字符,然后才能执行 HTTP 请求。另一方面,中文 URL 的分享传播并不需要转码,可以直接发在微博、朋友圈的帖子里,等点击的时候再转码跳转。
问题是很多平台解析链接会以非 ASCII 字符作为 URL 结束的标志,解析出的可点击链接只是中文 URL 的一部分,并不完整,比如微博:
# 原始 URL
https://mudan.me/post/original/2022/05/17/不必复杂-静态博客也可以做短链.html
# 解析出可以点击跳转的链接只是前半段英文部分
# 自然不能跳转到正确页面
https://mudan.me/post/original/2022/05/17/
如果使用 URL Encoder 把中文编码后再分享倒是能正确解析,但太长,每个汉字都被编码成十几个 ASCII 字符,容易触及帖子的字数限制。
# 原始 URL
https://mudan.me/post/original/2022/05/17/不必复杂-静态博客也可以做短链.html
# 编码 URL 明显变长
https://mudan.me/post/original/2021/06/10/%E4%B8%8D%E5%BF%85%E5%A4%8D%E6
%9D%82-%E9%9D%99%E6%80%81%E5%8D%9A%E5%AE%A2%E4%B9%9F%E5%8F%AF%E4%BB%A5%
E5%81%9A%E7%9F%AD%E9%93%BE.html
URL 变化需要重定向
使用短链的另一个原因也与博文 URL 里的文章标题有关,我有时修订文章可能修改标题,导致其 URL 随之变化,之前分享的链接会因此失效。虽然 Jekyll 的重定向插件jekyll-redirect-from
可以在原位置生成一个指向新地址的 HTML 文件进行跳转,但毕竟会创建多余目录和多余文件,不如直接维护一个短链与原始链的映射表简洁。无论文章 URL 如何变化,只需保证映射表里用于分享的短链始终指向最新地址即可。
<!-- jekyll-redirect-from 生成的重定向HTML -->
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>Redirecting…</title>
<link rel="canonical" href="http://mudan.me/target.html">
<script>location="http://mudan.me/target.html"</script>
<meta http-equiv="refresh" content="0; url=http://mudan.me/target.html">
<meta name="robots" content="noindex">
<h1>Redirecting…</h1>
<a href="http://mudan.me/target.html">Click here if you are not redirected.</a>
</html>
映射
短链服务的本质是查询映射表,从中找到对应长链然后跳转,这要求从长链生成短链的算法必须保证短链的唯一性,不能出现多个长链生成同一个短链的情况。
首先哈希算法是不合适的,虽然概率很低,但哈希碰撞不可避免。一种流行实现是多位 62 进制自增,比如定义短链为 5 个字符,每个字符都是 62 进制,那么 1 位可对应 62 个长链,5 位对应 62^5 = 916,132,832 个长链。短短 5 位的指数级增长已经接近 10 亿,足够应对大量短链映射。
为什么是 62 进制?因为 URL 必须是 ASCII 码表里的字符,常用的数字 [0-9] 和大小写字母 [a-z,A-Z] 加起来就是 62。当然如果把 +、- 这类合法字符算上,64 进制也可以。
一个生成短链的自增函数示例:
val table = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z')
/**
* 把传入的字符串按 [table] 数组的 62 个字符顺序自增,比如 0000Z,自增后为 00010
*/
fun selfIncreaseBase62(baseStr: String): String {
val sb = StringBuilder()
// 进位记录
var carry = 1
for (i in (baseStr.length - 1) downTo 0) {
var index = table.indexOf(baseStr[i])
if (index == -1) throw InvalidParameterException("invalid character")
index += carry
if (index >= table.size) {
// 进位
carry = 1
// index 归零
index = 0
} else {
// 不进位
carry = 0
}
sb.insert(0, table[index])
}
// 跳出循环依然有进位,则已经超出所能表示的范围
if (carry == 1) {
throw IndexOutOfBoundsException()
}
return sb.toString()
}
需要数据库吗
已经有生成映射表的方法,要如何存储并快速查询呢?数据库是能想到的首选方案,但对一个托管在 GitHub Pages 上区区数十网页的静态博客来说有些“重”,而且要租用服务器,并不合适。
不对外提供短链服务仅仅为博客一部分文章生成短链,所以更现实的方法是在api/
目录下创建一个保存映射表的 url-map.json 文件,手动添加映射,估摸着以自己的文章产出大概是不会超过百条的。
// api/url-map.json
// 短链只用 2 位 36 进制就足够,36^2 = 1296,千条容量,如果有幸真的不够,后期可以扩充
// 这里的 op、og、rp 是我的文章分类标志
// 最终的短链格式 https://mudan.me/op00
{
"map": [
{
"id": "op00",
"target": {
"path": "/post/opera/post_00.html",
"title": "title_00"
}
},
{
"id": "op01",
"target": {
"path": "/post/opera/post_01.html",
"title": "title_01"
}
},
{
"id": "og00",
"target": {
"path": "/post/original/post_00.html",
"title": "title_00"
}
},
{
"id": "og01",
"target": {
"path": "/post/original/post_01.html",
"title": "title_01"
}
},
{
"id": "rp00",
"target": {
"path": "/post/repost/post_00.html",
"title": "title_00"
}
}
]
}
跳转
GitHub Pages 托管的静态博客不能像自建 Nginx 服务器那样对请求灵活返回自定义内容,所谓“静态”是指每个网页都是已经存在的 HTML 文件,不能动态生成。Pages 只会返回请求 URL 的对应 HTML,但允许用户创建 URL 不存在时的 404 页面。它本质也是一个HTML
文件,是实现短链跳转的关键。
比如短链 https://mudan.me/op00 对应的 HTML 文件不存在,Pages 会返回自定义的 404 页面。在这个页面里可通过 JavaScript 获取 URL 中的短链 ID,随后下载含有映射表的 JSON 文件从中查找原链跳转。这就是静态博客实现短链的机制,不复杂,也不需要额外花费。
适当添加些好看的跳转动画,一切由开发者随意控制。