diff --git a/_data/conversationMentions.js b/_data/conversationMentions.js new file mode 100644 index 0000000..1652f37 --- /dev/null +++ b/_data/conversationMentions.js @@ -0,0 +1,14 @@ +import EleventyFetch from "@11ty/eleventy-fetch"; + +export default async function () { + try { + const data = await EleventyFetch( + "http://127.0.0.1:8080/conversations/api/mentions?per-page=10000", + { duration: "15m", type: "json" } + ); + return data.children || []; + } catch (e) { + console.log(`[conversationMentions] API unavailable: ${e.message}`); + return []; + } +} diff --git a/_data/eleventyComputed.js b/_data/eleventyComputed.js index 2b58219..d895107 100644 --- a/_data/eleventyComputed.js +++ b/_data/eleventyComputed.js @@ -16,18 +16,55 @@ import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); export default { - ogSlug: (data) => { - const url = data.page?.url; - if (!url) return ""; - return url.replace(/\/$/, "").split("/").pop(); - }, + eleventyComputed: { + // Compute permalink from file path for posts without explicit frontmatter permalink. + // Pattern: content/{type}/{yyyy}-{MM}-{dd}-{slug}.md → /{type}/{yyyy}/{MM}/{dd}/{slug}/ + permalink: (data) => { + // If frontmatter already has permalink, use it (new posts from preset) + if (data.permalink) return data.permalink; - hasOgImage: (data) => { - const url = data.page?.url; - if (!url) return false; - const slug = url.replace(/\/$/, "").split("/").pop(); - if (!slug) return false; - const ogPath = resolve(__dirname, "..", ".cache", "og", `${slug}.png`); - return existsSync(ogPath); + // Only compute for files matching the dated post pattern + const inputPath = data.page?.inputPath || ""; + const match = inputPath.match( + /content\/([^/]+)\/(\d{4})-(\d{2})-(\d{2})-(.+)\.md$/ + ); + if (match) { + const [, type, year, month, day, slug] = match; + return `/${type}/${year}/${month}/${day}/${slug}/`; + } + + // For non-matching files (pages, root files), preserve existing permalink or let Eleventy decide + return data.permalink; + }, + + // OG image slug — must reconstruct date-prefixed filename from URL segments. + // OG images are generated as {yyyy}-{MM}-{dd}-{slug}.png by lib/og.js. + // With new URL structure /type/yyyy/MM/dd/slug/, we reconstruct the filename. + ogSlug: (data) => { + const url = data.page?.url || ""; + const segments = url.split("/").filter(Boolean); + // Date-based URL: /type/yyyy/MM/dd/slug/ → 5 segments + if (segments.length === 5) { + const [, year, month, day, slug] = segments; + return `${year}-${month}-${day}-${slug}`; + } + // Fallback: last segment (for pages, legacy URLs) + return segments[segments.length - 1] || ""; + }, + + hasOgImage: (data) => { + const url = data.page?.url || ""; + const segments = url.split("/").filter(Boolean); + let slug; + if (segments.length === 5) { + const [, year, month, day, s] = segments; + slug = `${year}-${month}-${day}-${s}`; + } else { + slug = segments[segments.length - 1] || ""; + } + if (!slug) return false; + const ogPath = resolve(__dirname, "..", ".cache", "og", `${slug}.png`); + return existsSync(ogPath); + }, }, }; diff --git a/_includes/components/webmentions.njk b/_includes/components/webmentions.njk index e012c5d..c8de85e 100644 --- a/_includes/components/webmentions.njk +++ b/_includes/components/webmentions.njk @@ -3,7 +3,7 @@ {# Also checks legacy URLs from micro.blog and old blog for historical webmentions #} {# Client-side JS supplements build-time data with real-time fetches #} -{% set mentions = webmentions | webmentionsForUrl(page.url, urlAliases) %} +{% set mentions = webmentions | webmentionsForUrl(page.url, urlAliases, conversationMentions) %} {% set absoluteUrl = site.url + page.url %} {% set buildTimestamp = "" | timestamp %} diff --git a/_includes/components/widgets/webmentions.njk b/_includes/components/widgets/webmentions.njk index 9b1b472..d3637f3 100644 --- a/_includes/components/widgets/webmentions.njk +++ b/_includes/components/widgets/webmentions.njk @@ -128,13 +128,29 @@ function webmentionsWidget() { async init() { this.loading = true; try { - const res = await fetch('/webmentions/api/mentions?per-page=50&page=0'); - if (!res.ok) { - if (res.status === 404) { this.error = null; return; } - throw new Error('HTTP ' + res.status); + const [wmRes, convRes] = await Promise.all([ + fetch('/webmentions/api/mentions?per-page=50&page=0').catch(() => null), + fetch('/conversations/api/mentions?per-page=50&page=0').catch(() => null), + ]); + const wmData = wmRes?.ok ? await wmRes.json() : { children: [] }; + const convData = convRes?.ok ? await convRes.json() : { children: [] }; + + // Merge: conversations items first (richer metadata), then webmentions + const seen = new Set(); + const merged = []; + for (const item of (convData.children || [])) { + const key = item['wm-id'] || item.url; + if (key && !seen.has(key)) { seen.add(key); merged.push(item); } } - const data = await res.json(); - this.mentions = (data.children || []).sort((a, b) => { + for (const item of (wmData.children || [])) { + const key = item['wm-id']; + if (!key || seen.has(key)) continue; + if (item.url && seen.has(item.url)) continue; + seen.add(key); + merged.push(item); + } + + this.mentions = merged.sort((a, b) => { return new Date(b.published || b['wm-received'] || 0) - new Date(a.published || a['wm-received'] || 0); }); } catch (e) { diff --git a/eleventy.config.js b/eleventy.config.js index 385dfa4..dd1555c 100644 --- a/eleventy.config.js +++ b/eleventy.config.js @@ -380,7 +380,14 @@ export default function (eleventyConfig) { // OG images are named with the full date prefix to match URL segments exactly. eleventyConfig.addFilter("ogSlug", (url) => { if (!url) return ""; - return url.replace(/\/$/, "").split("/").pop(); + const segments = url.split("/").filter(Boolean); + // Date-based URL: /type/yyyy/MM/dd/slug/ → 5 segments → "yyyy-MM-dd-slug" + if (segments.length === 5) { + const [, year, month, day, slug] = segments; + return `${year}-${month}-${day}-${slug}`; + } + // Fallback: last segment (for pages, legacy URLs) + return segments[segments.length - 1] || ""; }); // Check if a generated OG image exists for this slug @@ -418,8 +425,33 @@ export default function (eleventyConfig) { // 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 []; + // Merges webmentions + conversations with deduplication (conversations first) + eleventyConfig.addFilter("webmentionsForUrl", function (webmentions, url, urlAliases, conversationMentions = []) { + if (!url) return []; + + // Merge conversations + webmentions with deduplication + const seen = new Set(); + const merged = []; + + // Add conversations first (richer metadata) + for (const item of conversationMentions) { + const key = item['wm-id'] || item.url; + if (key && !seen.has(key)) { + seen.add(key); + merged.push(item); + } + } + + // Add webmentions (skip duplicates) + if (webmentions) { + for (const item of webmentions) { + const key = item['wm-id']; + if (!key || seen.has(key)) continue; + if (item.url && seen.has(item.url)) continue; + seen.add(key); + merged.push(item); + } + } // Build list of all URLs to check (current + legacy) const urlsToCheck = new Set(); @@ -441,8 +473,18 @@ export default function (eleventyConfig) { } } - // Filter webmentions matching any of our URLs - return webmentions.filter((wm) => urlsToCheck.has(wm["wm-target"])); + // Compute legacy /content/ URL from current URL for old webmention.io targets + // Pattern: /type/yyyy/MM/dd/slug/ → /content/type/yyyy-MM-dd-slug/ + const pathSegments = url.replace(/\/$/, "").split("/").filter(Boolean); + if (pathSegments.length === 5) { + const [type, year, month, day, slug] = pathSegments; + const contentUrl = `/content/${type}/${year}-${month}-${day}-${slug}/`; + urlsToCheck.add(`${siteUrl}${contentUrl}`); + urlsToCheck.add(`${siteUrl}${contentUrl}`.replace(/\/$/, "")); + } + + // Filter merged data matching any of our URLs + return merged.filter((wm) => urlsToCheck.has(wm["wm-target"])); }); eleventyConfig.addFilter("webmentionsByType", function (mentions, type) { diff --git a/webmention-debug.njk b/webmention-debug.njk index a8fbf1d..43246bd 100644 --- a/webmention-debug.njk +++ b/webmention-debug.njk @@ -43,7 +43,7 @@ pagefindIgnore: true {% for post in collections.posts | head(50) %} - {% set allMentions = webmentions | webmentionsForUrl(post.url, urlAliases) %} + {% set allMentions = webmentions | webmentionsForUrl(post.url, urlAliases, conversationMentions) %} {% set legacyUrls = urlAliases.getOldUrls(post.url) %} {% if allMentions.length > 0 or legacyUrls.length > 0 %}