曾经我遇到过这样一件事情:网站在经历重构后,搜索引擎上的索引页面和网站的路径不一样。比方说,我希望新的网站使用 /friends/ 作为友链路径,但索引在搜索引擎上的旧网站链接却是 /links/,它们有个共同点就是都叫“友链”,但访客点进来后却一头雾水。还有一件事:网站从 Vercel 换到 Cloudflare Pages 后,因为本地生成文章的时区和搜索引擎上的不一样,所以路径也不一样。比方说,我有一篇文章从 4 月 9 日变成了 4 月 10 日,但之前的路径还是 4 月 9 日。
路径纠错实际上是一个很狭窄、但是关键时刻又急需的功能。有了它,就可以解决以上的问题,做到不浪费搜索引擎的 SEO 索引。流程是这样的:当你访问进入错误的路径时,浏览器会保持那个 URL,同时页面被替换为我们预设好的 /404.html;如果检测到当前是 404 页面,而不是主动访问的 /404 又或者 /404.html 的 404,则触发路径纠错功能;获取访客当前访问错误的地址,通过浏览器自带的语义分析方法,比对网站的 sitemap.txt 列表,选择可能性最高的那个路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
| (async function () { const container = document.querySelector('.error-content'); if (!container) return;
const currentPath = window.location.pathname; if (currentPath === '/404' || currentPath === '/404.html') return;
await tryRoute(currentPath); })();
async function tryRoute(targetPath) { try { const response = await fetch('/sitemap.txt'); if (!response.ok) return;
const text = await response.text(); const urls = text.split('\n') .map(line => line.trim()) .filter(Boolean);
if (urls.length === 0) return;
const sitemapPaths = urls.map(url => { try { return new URL(url).pathname; } catch { return url; } });
const bestPath = findBestMatch(targetPath, sitemapPaths); if (!bestPath) return;
const bestIndex = sitemapPaths.indexOf(bestPath); let bestUrl = urls[bestIndex];
if (bestUrl.endsWith('/index.html')) { bestUrl = bestUrl.slice(0, -'index.html'.length); }
btf.snackbarShow('未找到页面,五秒后跳转至正确路径……');
setTimeout(() => { window.location.href = bestUrl; }, 5000); } catch (e) {} }
window.error_location = async function (wrongPath) { if (!wrongPath || typeof wrongPath !== 'string') return; await tryRoute(wrongPath); };
function findBestMatch(target, candidates) { const targetSegs = decodeURIComponent(target) .toLowerCase() .split('/') .filter(Boolean);
let bestScore = Infinity; let bestCandidate = candidates[0];
for (const candidate of candidates) { const candSegs = decodeURIComponent(candidate) .toLowerCase() .split('/') .filter(Boolean);
const minLen = Math.min(targetSegs.length, candSegs.length); let totalDist = 0; let totalLen = 0;
for (let i = 0; i < minLen; i++) { const d = levenshtein(targetSegs[i], candSegs[i]); totalDist += d; totalLen += Math.max(targetSegs[i].length, candSegs[i].length); }
const extraSegs = Math.abs(targetSegs.length - candSegs.length); const penalty = extraSegs * 3; totalDist += penalty; totalLen += penalty;
const score = totalLen > 0 ? totalDist / totalLen : 0;
if (score < bestScore) { bestScore = score; bestCandidate = candidate; } }
return bestCandidate; }
function levenshtein(a, b) { const m = a.length; const n = b.length;
if (m === 0) return n; if (n === 0) return m;
let prev = Array.from({ length: n + 1 }, (_, j) => j); let curr = new Array(n + 1);
for (let i = 1; i <= m; i++) { curr[0] = i; for (let j = 1; j <= n; j++) { const cost = a[i - 1] === b[j - 1] ? 0 : 1; curr[j] = Math.min( curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost ); } [prev, curr] = [curr, prev]; }
return prev[n]; }
|
以上就是最终的代码,我说一下我是怎么想到的,以及其中要避开的坑。一开始我只是在 DeepSeek TUI 里要求只需实现上述功能即可,但是我很快发现,在本地浏览模式,我无法触发路径 404 功能。索性我就让 DeepSeek 写了一个调试函数 error_location(),参数为字符串,专门用于调试功能。第一次调试就出了问题,当我的期望是通过字符串 /tg 进入 /tags/ 时,结果返回却是 /tags/梦/。后来我让 AI 加了路径层级检测,才解决了这个问题。
随之而来的又是一个新问题:Hexo 的 _config.yml 默认设置了路径统一为不需要 /index.html 的版本,但有意思的是,hexo-generator-sitemap 在生成网站地图的时候,会生成带 index.html 版本的。这意味着如果我们的寻址方式是通过 sitemap.txt 进行的,则需要在路径里判断是否减去 index.html,否则访客会跳转到一个和预期页面差不多,但是没有评论、数据、记录的页面,因为 Waline 评论系统默认记录的是路径,而非文章标题。
需要注意的是,Butterfly 的 404 页面的容器为 .error-content 而非 #error-content。如果写错了样式匹配,则脚本不会生效。脚本生效之后,访客如果通过搜索引擎访问到错误的页面,会被前端算法自动比对,在五秒之内为访客匹配到正确页面——得益于浏览器的语义匹配功能。举一个相对离谱一点的例子,如果搜索引擎上的路经为 /links/,访客点进来会被索引到 /friends/;如果搜索引擎上的路径为 4 月 9 号,点进来会被索引到 4 月 10 号。
以至于如果你改过标题,这个脚本都能选取相对正确的标题路径,为访客进行跳转,可谓是相当贴心了。上文说到 sitemap.txt 中的文章路径为 /index.html 版本的,而这个 404 跳转请求的脚本仅仅对 404 页面生效,那么我们就需要一个单独的脚本,来完成这件事情。我们来探讨一下算法:当访客从搜索引擎点击、进入这篇文章之后,脚本会检查浏览器路径中是否有 index.html;如存在,则使用 fetch() 检查是否有无 index.html 路径,有则跳转:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| (async function () { const currentPath = window.location.pathname;
if (!currentPath.endsWith('/index.html')) return;
const cleanPath = currentPath.slice(0, -'index.html'.length) || '/';
try { const response = await fetch(cleanPath, { method: 'HEAD' }); if (response.ok) { window.location.replace(cleanPath); } } catch (e) {} })();
|
值得一提的是,这两个 JS 脚本不需要单独为其适配 PJAX(如果你开启了 PJAX 的话),因为它们的运行情况被预设在访问网站的时候,此时的浏览器会加载并运行 JS 脚本(当然,你肯定不会为了偷懒,把可点击的导航栏路径,改成这种错误路径)。接下来,我就需要把这些 JS 脚本,全部引入 _config.butterfly.yml 的 inject 块段,确保这些脚本的功能可以正常运行(我想起来以前我没有导入这些文件,导致代码写完了还抱怨 AI 给的代码不能运行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| inject: head: - <link rel="stylesheet" href="/assets/css/transparent.css"> - <link rel="stylesheet" href="/assets/css/fonts.css"> - <link rel="stylesheet" href="/assets/css/svg.css"> - <link rel="stylesheet" href="/assets/css/pagination.css"> - <link rel="stylesheet" href="/assets/css/social.css"> - <link rel="stylesheet" href="/assets/css/align.css"> - <link rel="stylesheet" href="/assets/css/snackbar.css"> - <link rel="stylesheet" href="/assets/css/meta.css"> - <link rel="stylesheet" href="/assets/css/spacing.css"> - <link rel="stylesheet" href="/assets/css/paragraph.css"> - <link rel="stylesheet" href="/assets/css/proportion.css"> bottom: - <script src="/assets/js/stroll.js"></script> - <script src="/assets/js/blindbox.js"></script> - <script src="/assets/js/mourn.js"></script> - <script src="/assets/js/reminder.js"></script> - <script src="/assets/js/friends.js"></script> - <script src="/assets/js/avatar.js"></script> - <script src="/assets/js/music.js"></script> - <script src="/assets/js/height.js"></script> - <script src="/assets/js/routing.js"></script> - <script src="/assets/js/fix.js"></script>
|