···
+
// Taken from Vale's 404 Guesser
+
// https://vale.rocks/assets/scripts/404-guesser.js
+
// which was based on Gwern's 404 Error Page URL Suggester
+
// https://gwern.net/static/js/404-guesser.js
+
const sitemapText = await this.fetchSitemap();
+
this.urls = this.parseUrls(sitemapText);
+
const currentPath = window.location.pathname;
+
if (!currentPath.endsWith("/404")) {
+
const suggestions = this.findSimilarUrls(currentPath);
+
this.injectSuggestions(currentPath, suggestions);
+
console.error("Error initializing URL suggester:", error);
+
const response = await fetch("/sitemap.xml");
+
return await response.text();
+
console.error("Error fetching sitemap:", error);
+
parseUrls(sitemapText) {
+
const parser = new DOMParser();
+
const xmlDoc = parser.parseFromString(sitemapText, "text/xml");
+
const urlNodes = xmlDoc.getElementsByTagName("url");
+
return Array.from(urlNodes).map(
+
new URL(node.getElementsByTagName("loc")[0].textContent).pathname,
+
boundedLevenshteinDistance(a, b, maxDistance) {
+
if (Math.abs(a.length - b.length) > maxDistance) return maxDistance + 1;
+
const matrix = Array(b.length + 1)
+
for (let j = 1; j <= a.length; j++) {
+
for (let i = 1; i <= b.length; i++) {
+
let minDistance = maxDistance + 1;
+
for (let j = 1; j <= a.length; j++) {
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
+
matrix[i][j] = matrix[i - 1][j - 1];
+
matrix[i][j] = Math.min(
+
matrix[i - 1][j - 1] + 1,
+
minDistance = Math.min(minDistance, matrix[i][j]);
+
if (minDistance > maxDistance) {
+
return maxDistance + 1;
+
return matrix[b.length][a.length];
+
findSimilarUrls(targetUrl) {
+
const targetPath = new URL(targetUrl, location.origin).pathname;
+
if (targetPath.startsWith("/posts/")) {
+
const exactMatch = this.urls.find((url) => url === targetPath);
+
return [location.origin + exactMatch];
+
const potentialMatches = this.urls.filter(
+
Math.abs(url.length - targetPath.length) <= this.maxDistance &&
+
!url.endsWith("/404.html"),
+
const similarUrls = potentialMatches
+
distance: this.boundedLevenshteinDistance(
+
.filter((item) => item.distance <= this.maxDistance)
+
.sort((a, b) => a.distance - b.distance);
+
const seenUrls = new Set();
+
const uniqueSimilarUrls = similarUrls
+
if (seenUrls.has(item.url)) return false;
+
seenUrls.add(item.url);
+
return uniqueSimilarUrls.map((item) => location.origin + item.url);
+
injectSuggestions(currentPath, suggestions) {
+
const app = document.querySelector("#suggestions");
+
if (suggestions.length > 0) {
+
const p = document.createElement("p");
+
p.innerHTML = "I did however find some URLs that might be relevant?";
+
for (const url of suggestions) {
+
const a = document.createElement("a");
+
const cleanUrl = url.replace(/\.html$/, "");
+
a.textContent = cleanUrl;
+
const endText = document.createElement("p");
+
app.appendChild(endText);
+
const p = document.createElement("p");
+
p.innerHTML = `Couldn't find any URLs similar to <code>${currentPath}</code>. I guess it's time to find something new`;
+
app.className = "url-suggestions";
+
document.addEventListener("DOMContentLoaded", () => {
+
new URLSuggester().initialize();