From a316d3148dea71a5f3c9b2dde1af36941e212b73 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Sat, 24 Jan 2026 14:57:29 +0100 Subject: [PATCH] feat: add legacy webmention recovery from old URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add urlAliases.js to parse redirect maps and build old→new URL mappings - Enhance webmentionsForUrl filter to check legacy URLs (micro.blog, Known/WP) - Update webmentions.njk to pass urlAliases data - Add webmention-debug.njk page at /debug/webmentions/ This recovers webmentions sent to old URL structures: - micro.blog: /YYYY/MM/DD/slug.html - Known/WordPress: /YYYY/slug Co-Authored-By: Claude Opus 4.5 --- _data/urlAliases.js | 139 +++++++++++++++++++++++++++ _includes/components/webmentions.njk | 3 +- eleventy.config.js | 36 +++++-- webmention-debug.njk | 123 ++++++++++++++++++++++++ 4 files changed, 290 insertions(+), 11 deletions(-) create mode 100644 _data/urlAliases.js create mode 100644 webmention-debug.njk diff --git a/_data/urlAliases.js b/_data/urlAliases.js new file mode 100644 index 0000000..0a44855 --- /dev/null +++ b/_data/urlAliases.js @@ -0,0 +1,139 @@ +/** + * URL Aliases for Webmention Recovery + * + * Maps new URLs to their old URLs so webmentions from previous + * URL structures can be displayed on current pages. + * + * Sources: + * - redirects.map.rmendes (micro.blog: /YYYY/MM/DD/slug.html → /notes/...) + * - old-blog-redirects.map.rmendes (Known/WP: /YYYY/slug → /content/...) + */ + +import { readFileSync, existsSync } from "fs"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const siteUrl = process.env.SITE_URL || "https://example.com"; + +/** + * Parse a redirect map file into URL mappings + * Format: old_path new_path; + */ +function parseRedirectMap(filePath) { + const aliases = {}; + + if (!existsSync(filePath)) { + console.log(`[urlAliases] File not found: ${filePath}`); + return aliases; + } + + try { + const content = readFileSync(filePath, "utf-8"); + const lines = content.split("\n").filter((line) => { + const trimmed = line.trim(); + return trimmed && !trimmed.startsWith("#"); + }); + + for (const line of lines) { + // Format: /old/path /new/path; + const match = line.match(/^(\S+)\s+(\S+);?$/); + if (match) { + const [, oldPath, newPath] = match; + // Normalize paths (remove trailing slashes, ensure leading slash) + const normalizedNew = newPath.replace(/;$/, "").replace(/\/$/, ""); + const normalizedOld = oldPath.replace(/\/$/, ""); + + // Map new URL → array of old URLs + if (!aliases[normalizedNew]) { + aliases[normalizedNew] = []; + } + aliases[normalizedNew].push(normalizedOld); + } + } + } catch (error) { + console.error(`[urlAliases] Error parsing ${filePath}:`, error.message); + } + + return aliases; +} + +/** + * Merge multiple alias maps + */ +function mergeAliases(...maps) { + const merged = {}; + for (const map of maps) { + for (const [newUrl, oldUrls] of Object.entries(map)) { + if (!merged[newUrl]) { + merged[newUrl] = []; + } + merged[newUrl].push(...oldUrls); + } + } + return merged; +} + +// Parse redirect maps from package root (one level up from _data) +const pkgRoot = resolve(__dirname, "../.."); + +// Try multiple possible locations +const mapLocations = [ + resolve(pkgRoot, "redirects.map.rmendes"), + resolve(pkgRoot, "old-blog-redirects.map.rmendes"), + // Fallback to template files if .rmendes versions don't exist + resolve(pkgRoot, "redirects.map"), + resolve(pkgRoot, "old-blog-redirects.map"), +]; + +const microblogAliases = parseRedirectMap(mapLocations[0]) || parseRedirectMap(mapLocations[2]); +const knownAliases = parseRedirectMap(mapLocations[1]) || parseRedirectMap(mapLocations[3]); + +const allAliases = mergeAliases(microblogAliases, knownAliases); + +// Log summary +const totalMappings = Object.keys(allAliases).length; +const totalOldUrls = Object.values(allAliases).reduce((sum, urls) => sum + urls.length, 0); +console.log(`[urlAliases] Loaded ${totalMappings} URL mappings with ${totalOldUrls} old URLs`); + +export default { + // The merged alias map: new URL → [old URLs] + aliases: allAliases, + + // Site URL for building absolute URLs + siteUrl, + + /** + * Get all URLs (old and new) that should be checked for webmentions + * @param {string} url - Current page URL (relative) + * @returns {string[]} - Array of absolute URLs to check + */ + getAllUrls(url) { + const normalizedUrl = url.replace(/\/$/, ""); + const urls = [ + `${siteUrl}${url}`, + `${siteUrl}${normalizedUrl}`, + ]; + + // Add old URL variations + const oldUrls = allAliases[normalizedUrl] || []; + for (const oldUrl of oldUrls) { + urls.push(`${siteUrl}${oldUrl}`); + // Also try with trailing slash + urls.push(`${siteUrl}${oldUrl}/`); + } + + // Deduplicate + return [...new Set(urls)]; + }, + + /** + * Get just the old URLs for a given new URL + * @param {string} url - Current page URL (relative) + * @returns {string[]} - Array of old relative URLs + */ + getOldUrls(url) { + const normalizedUrl = url.replace(/\/$/, ""); + return allAliases[normalizedUrl] || []; + }, +}; diff --git a/_includes/components/webmentions.njk b/_includes/components/webmentions.njk index 5af3f1e..bb2adb8 100644 --- a/_includes/components/webmentions.njk +++ b/_includes/components/webmentions.njk @@ -1,7 +1,8 @@ {# Webmentions Component #} {# Displays likes, reposts, and replies for a post #} +{# Also checks legacy URLs from micro.blog and old blog for historical webmentions #} -{% set mentions = webmentions | webmentionsForUrl(page.url) %} +{% set mentions = webmentions | webmentionsForUrl(page.url, urlAliases) %} {% if mentions.length %}
diff --git a/eleventy.config.js b/eleventy.config.js index caffb7b..47ec18b 100644 --- a/eleventy.config.js +++ b/eleventy.config.js @@ -235,17 +235,33 @@ export default function (eleventyConfig) { return date.toLocaleDateString("en-US", options); }); - // Webmention filters - eleventyConfig.addFilter("webmentionsForUrl", function (webmentions, url) { + // Webmention filters - with legacy URL support + // This filter checks both current URL and any legacy URLs from redirects + eleventyConfig.addFilter("webmentionsForUrl", function (webmentions, url, urlAliases) { if (!webmentions || !url) return []; - const absoluteUrl = url.startsWith("http") - ? url - : `${siteUrl}${url}`; - return webmentions.filter( - (wm) => - wm["wm-target"] === absoluteUrl || - wm["wm-target"] === absoluteUrl.replace(/\/$/, "") - ); + + // Build list of all URLs to check (current + legacy) + const urlsToCheck = new Set(); + + // Add current URL variations + const absoluteUrl = url.startsWith("http") ? url : `${siteUrl}${url}`; + urlsToCheck.add(absoluteUrl); + urlsToCheck.add(absoluteUrl.replace(/\/$/, "")); + urlsToCheck.add(absoluteUrl.endsWith("/") ? absoluteUrl : `${absoluteUrl}/`); + + // Add legacy URLs from aliases (if provided) + if (urlAliases?.aliases) { + const normalizedUrl = url.replace(/\/$/, ""); + const oldUrls = urlAliases.aliases[normalizedUrl] || []; + for (const oldUrl of oldUrls) { + urlsToCheck.add(`${siteUrl}${oldUrl}`); + urlsToCheck.add(`${siteUrl}${oldUrl}/`); + urlsToCheck.add(`${siteUrl}${oldUrl}`.replace(/\/$/, "")); + } + } + + // Filter webmentions matching any of our URLs + return webmentions.filter((wm) => urlsToCheck.has(wm["wm-target"])); }); eleventyConfig.addFilter("webmentionsByType", function (mentions, type) { diff --git a/webmention-debug.njk b/webmention-debug.njk new file mode 100644 index 0000000..238c77b --- /dev/null +++ b/webmention-debug.njk @@ -0,0 +1,123 @@ +--- +layout: layouts/base.njk +title: Webmention Debug +permalink: /debug/webmentions/ +eleventyExcludeFromCollections: true +--- + + +
+ {# Summary #} +
+

Summary

+
+
+
Total URL Mappings
+
{{ urlAliases.aliases | length }}
+
+
+
Total Webmentions
+
{{ webmentions | length }}
+
+
+
+ + {# Recent Posts with Webmentions #} +
+

Posts with Webmentions

+
+ + + + + + + + + + {% for post in collections.posts | head(50) %} + {% set allMentions = webmentions | webmentionsForUrl(post.url, urlAliases) %} + {% set legacyUrls = urlAliases.getOldUrls(post.url) %} + {% if allMentions.length > 0 or legacyUrls.length > 0 %} + + + + + + {% endif %} + {% endfor %} + +
Current URLLegacy URLsWebmentions
+ + {{ post.url }} + + + {% if legacyUrls.length %} + {% for legacyUrl in legacyUrls %} +
{{ legacyUrl }}
+ {% endfor %} + {% else %} + - + {% endif %} +
+ {% if allMentions.length %} + + {{ allMentions.length }} + + {% else %} + 0 + {% endif %} +
+
+
+ + {# URL Alias Sample #} +
+

URL Alias Sample (first 20)

+
+ + + + + + + + + {% set aliasEntries = urlAliases.aliases | dictsort %} + {% for newUrl, oldUrls in aliasEntries | head(20) %} + + + + + {% endfor %} + +
New URLOld URL(s)
{{ newUrl }} + {% for oldUrl in oldUrls %} +
{{ oldUrl }}
+ {% endfor %} +
+
+
+ + {# Raw Webmention Targets (for debugging) #} +
+

Recent Webmention Targets

+

+ Shows which URLs webmentions were sent to (useful for verifying legacy URL matches). +

+
    + {% for wm in webmentions | head(30) %} +
  • + {{ wm["wm-property"] }}: + {{ wm["wm-target"] }} +
  • + {% endfor %} +
+
+