···
1
+
// Taken from Vale's 404 Guesser
2
+
// https://vale.rocks/assets/scripts/404-guesser.js
3
+
// which was based on Gwern's 404 Error Page URL Suggester
4
+
// https://gwern.net/static/js/404-guesser.js
8
+
this.maxDistance = 8;
12
+
async initialize() {
14
+
const sitemapText = await this.fetchSitemap();
16
+
this.urls = this.parseUrls(sitemapText);
17
+
const currentPath = window.location.pathname;
18
+
if (!currentPath.endsWith("/404")) {
19
+
const suggestions = this.findSimilarUrls(currentPath);
20
+
this.injectSuggestions(currentPath, suggestions);
24
+
console.error("Error initializing URL suggester:", error);
28
+
async fetchSitemap() {
30
+
const response = await fetch("/sitemap.xml");
31
+
return await response.text();
33
+
console.error("Error fetching sitemap:", error);
38
+
parseUrls(sitemapText) {
39
+
const parser = new DOMParser();
40
+
const xmlDoc = parser.parseFromString(sitemapText, "text/xml");
41
+
const urlNodes = xmlDoc.getElementsByTagName("url");
42
+
return Array.from(urlNodes).map(
44
+
new URL(node.getElementsByTagName("loc")[0].textContent).pathname,
48
+
boundedLevenshteinDistance(a, b, maxDistance) {
49
+
if (Math.abs(a.length - b.length) > maxDistance) return maxDistance + 1;
50
+
const matrix = Array(b.length + 1)
52
+
.map((_, i) => [i]);
53
+
for (let j = 1; j <= a.length; j++) {
56
+
for (let i = 1; i <= b.length; i++) {
57
+
let minDistance = maxDistance + 1;
58
+
for (let j = 1; j <= a.length; j++) {
59
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
60
+
matrix[i][j] = matrix[i - 1][j - 1];
62
+
matrix[i][j] = Math.min(
63
+
matrix[i - 1][j - 1] + 1,
64
+
matrix[i][j - 1] + 1,
65
+
matrix[i - 1][j] + 1,
68
+
minDistance = Math.min(minDistance, matrix[i][j]);
70
+
if (minDistance > maxDistance) {
71
+
return maxDistance + 1;
74
+
return matrix[b.length][a.length];
77
+
findSimilarUrls(targetUrl) {
78
+
const targetPath = new URL(targetUrl, location.origin).pathname;
80
+
if (targetPath.startsWith("/posts/")) {
81
+
const exactMatch = this.urls.find((url) => url === targetPath);
83
+
return [location.origin + exactMatch];
87
+
const potentialMatches = this.urls.filter(
89
+
Math.abs(url.length - targetPath.length) <= this.maxDistance &&
90
+
!url.endsWith("/404.html"),
93
+
const similarUrls = potentialMatches
96
+
distance: this.boundedLevenshteinDistance(
102
+
.filter((item) => item.distance <= this.maxDistance)
103
+
.sort((a, b) => a.distance - b.distance);
105
+
const seenUrls = new Set();
106
+
const uniqueSimilarUrls = similarUrls
107
+
.filter((item) => {
108
+
if (seenUrls.has(item.url)) return false;
109
+
seenUrls.add(item.url);
114
+
return uniqueSimilarUrls.map((item) => location.origin + item.url);
117
+
injectSuggestions(currentPath, suggestions) {
118
+
const app = document.querySelector("#suggestions");
121
+
if (suggestions.length > 0) {
122
+
const p = document.createElement("p");
124
+
p.innerHTML = "I did however find some URLs that might be relevant?";
125
+
app.appendChild(p);
127
+
for (const url of suggestions) {
128
+
const a = document.createElement("a");
129
+
const cleanUrl = url.replace(/\.html$/, "");
131
+
a.textContent = cleanUrl;
132
+
app.appendChild(a);
135
+
const endText = document.createElement("p");
136
+
app.appendChild(endText);
138
+
const p = document.createElement("p");
139
+
p.innerHTML = `Couldn't find any URLs similar to <code>${currentPath}</code>. I guess it's time to find something new`;
140
+
app.appendChild(p);
143
+
app.className = "url-suggestions";
147
+
document.addEventListener("DOMContentLoaded", () => {
148
+
new URLSuggester().initialize();