mirror of
https://github.com/svemagie/blog-eleventy-indiekit.git
synced 2026-04-02 16:44:56 +02:00
feat: eliminate URL dualism — computed permalinks, conversations support
- Add computed permalink in data cascade for existing posts without frontmatter permalink (converts file path to Indiekit URL) - Fix ogSlug filter and computed data for new 5-segment URL structure - Add conversations API as build-time data source - Merge conversations + webmentions in webmentionsForUrl filter with deduplication and legacy /content/ URL alias computation - Sidebar widget fetches from both webmention-io and conversations APIs - Update webmention-debug page with conversationMentions parameter
This commit is contained in:
14
_data/conversationMentions.js
Normal file
14
_data/conversationMentions.js
Normal file
@@ -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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,18 +16,55 @@ import { fileURLToPath } from "node:url";
|
|||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
ogSlug: (data) => {
|
eleventyComputed: {
|
||||||
const url = data.page?.url;
|
// Compute permalink from file path for posts without explicit frontmatter permalink.
|
||||||
if (!url) return "";
|
// Pattern: content/{type}/{yyyy}-{MM}-{dd}-{slug}.md → /{type}/{yyyy}/{MM}/{dd}/{slug}/
|
||||||
return url.replace(/\/$/, "").split("/").pop();
|
permalink: (data) => {
|
||||||
},
|
// If frontmatter already has permalink, use it (new posts from preset)
|
||||||
|
if (data.permalink) return data.permalink;
|
||||||
|
|
||||||
hasOgImage: (data) => {
|
// Only compute for files matching the dated post pattern
|
||||||
const url = data.page?.url;
|
const inputPath = data.page?.inputPath || "";
|
||||||
if (!url) return false;
|
const match = inputPath.match(
|
||||||
const slug = url.replace(/\/$/, "").split("/").pop();
|
/content\/([^/]+)\/(\d{4})-(\d{2})-(\d{2})-(.+)\.md$/
|
||||||
if (!slug) return false;
|
);
|
||||||
const ogPath = resolve(__dirname, "..", ".cache", "og", `${slug}.png`);
|
if (match) {
|
||||||
return existsSync(ogPath);
|
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);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{# Also checks legacy URLs from micro.blog and old blog for historical webmentions #}
|
{# Also checks legacy URLs from micro.blog and old blog for historical webmentions #}
|
||||||
{# Client-side JS supplements build-time data with real-time fetches #}
|
{# 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 absoluteUrl = site.url + page.url %}
|
||||||
{% set buildTimestamp = "" | timestamp %}
|
{% set buildTimestamp = "" | timestamp %}
|
||||||
|
|
||||||
|
|||||||
@@ -128,13 +128,29 @@ function webmentionsWidget() {
|
|||||||
async init() {
|
async init() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/webmentions/api/mentions?per-page=50&page=0');
|
const [wmRes, convRes] = await Promise.all([
|
||||||
if (!res.ok) {
|
fetch('/webmentions/api/mentions?per-page=50&page=0').catch(() => null),
|
||||||
if (res.status === 404) { this.error = null; return; }
|
fetch('/conversations/api/mentions?per-page=50&page=0').catch(() => null),
|
||||||
throw new Error('HTTP ' + res.status);
|
]);
|
||||||
|
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();
|
for (const item of (wmData.children || [])) {
|
||||||
this.mentions = (data.children || []).sort((a, b) => {
|
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);
|
return new Date(b.published || b['wm-received'] || 0) - new Date(a.published || a['wm-received'] || 0);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -380,7 +380,14 @@ export default function (eleventyConfig) {
|
|||||||
// OG images are named with the full date prefix to match URL segments exactly.
|
// OG images are named with the full date prefix to match URL segments exactly.
|
||||||
eleventyConfig.addFilter("ogSlug", (url) => {
|
eleventyConfig.addFilter("ogSlug", (url) => {
|
||||||
if (!url) return "";
|
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
|
// Check if a generated OG image exists for this slug
|
||||||
@@ -418,8 +425,33 @@ export default function (eleventyConfig) {
|
|||||||
|
|
||||||
// Webmention filters - with legacy URL support
|
// Webmention filters - with legacy URL support
|
||||||
// This filter checks both current URL and any legacy URLs from redirects
|
// This filter checks both current URL and any legacy URLs from redirects
|
||||||
eleventyConfig.addFilter("webmentionsForUrl", function (webmentions, url, urlAliases) {
|
// Merges webmentions + conversations with deduplication (conversations first)
|
||||||
if (!webmentions || !url) return [];
|
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)
|
// Build list of all URLs to check (current + legacy)
|
||||||
const urlsToCheck = new Set();
|
const urlsToCheck = new Set();
|
||||||
@@ -441,8 +473,18 @@ export default function (eleventyConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter webmentions matching any of our URLs
|
// Compute legacy /content/ URL from current URL for old webmention.io targets
|
||||||
return webmentions.filter((wm) => urlsToCheck.has(wm["wm-target"]));
|
// 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) {
|
eleventyConfig.addFilter("webmentionsByType", function (mentions, type) {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ pagefindIgnore: true
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-surface-200 dark:divide-surface-700">
|
<tbody class="divide-y divide-surface-200 dark:divide-surface-700">
|
||||||
{% for post in collections.posts | head(50) %}
|
{% 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) %}
|
{% set legacyUrls = urlAliases.getOldUrls(post.url) %}
|
||||||
{% if allMentions.length > 0 or legacyUrls.length > 0 %}
|
{% if allMentions.length > 0 or legacyUrls.length > 0 %}
|
||||||
<tr class="hover:bg-surface-50 dark:hover:bg-surface-800/50">
|
<tr class="hover:bg-surface-50 dark:hover:bg-surface-800/50">
|
||||||
|
|||||||
Reference in New Issue
Block a user