Mastodon's VerifyLinkService uses strict string comparison against
account.url (which includes trailing slash from the AP actor's url
field). The h-card self-link used SITE_URL without trailing slash,
causing the comparison to fail silently.
Rename getMastodonHandle() to getFediverseCreator() and prefer the
site's own ActivityPub handle (ACTIVITYPUB_HANDLE) over the external
Mastodon account for the fediverse:creator meta tag. The Mastodon
account is a syndication target, not the canonical identity.
Reuses the existing fediverseInteract Alpine.js component to let
visitors follow the site author from their own fediverse instance.
Registered in all three sidebar routers (homepage, blog listing,
blog post) as widget type "fediverse-follow".
Posts published between Jan 19 – Feb 6 2026 (pre-beta.37 preset) had
permalink: /content/TYPE/YYYY-MM-DD-SLUG/ in frontmatter. The data
cascade trusted these values, causing Eleventy to generate pages at
/content/ paths instead of canonical URLs. This left 86 posts (including
articles like collecteur-de-flux-rss) returning 404 at their canonical
URLs.
The markdown files were fixed on the server (permalink lines removed),
but this adds a safety net: any remaining or future /content/ permalinks
are auto-converted to /TYPE/YYYY/MM/DD/SLUG/ format.
- Change .avatar-row selector to .facepile to match build-time template
- Use facepile-avatar class for dynamically created avatar links
- Fix pluralization in updateCount (was only replacing the number,
now rebuilds the full "N Like/Likes" text correctly)
- Align ring color classes with build-time template
Conversations items are now included in build-time rendering via
conversationMentions data, so they no longer need a special exception
to bypass the timestamp filter. All items (webmention.io and
conversations) are now filtered equally by build timestamp.
The client-side webmentions.js was deduplicating by wm-id and source
URL, but conversations API and webmention.io use different ID formats
(string vs numeric). Add author URL + action type dedup to catch
cross-source duplicates (e.g., same Bluesky like reported by both).
webmention.io cache and conversations API can report the same
interaction (e.g. a like) with different wm-id formats. Deduplicate
by author URL + interaction type after URL filtering to prevent
the same like/reply appearing twice.
- 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
The regex matched x-bind:src="comment.author.photo" from the comments
component, causing the literal string to appear in og:image meta tags.
Every Mastodon instance fetching OG data hit /comment.author.photo → 404.
Require whitespace before src= so only actual HTML src attributes match.
Pass the self-hosted ActivityPub syndication URL to the modal instead
of the Eleventy HTML page URL. Mastodon's authorize_interaction needs
a resolvable AP URI, not the HTML page which lacks an activity+json
alternate link.
Replace @click.outside on modal panel with @click.stop — the backdrop
already handles closing. @click.outside fires from the same click event
that opens the modal via x-if, immediately setting showModal back to false.
nodes.fediverse.party doesn't send CORS headers, so the fetch fails
from the browser. Remove autocomplete entirely — users type their
instance once and localStorage remembers it.
Add a Fediverse button to the "Also on" footer for posts syndicated via
self-hosted ActivityPub. Clicking it redirects users to their own instance
via authorize_interaction so they can like/boost/reply natively. Instance
is stored in localStorage for repeat visits, with a modal for first-time
entry and Shift+click to change.
Also adds branded syndication buttons for LinkedIn and IndieNews, and
replaces the heuristic Mastodon URL detection with exact matching against
the configured MASTODON_INSTANCE.
Posts with `draft: true` frontmatter were included in every collection
(posts, notes, articles, feed, recentPosts, categories, etc.), making
them visible on the blog, homepage, RSS feed, and sidebar. Added an
isPublished filter to all 12 collections.
The --incremental CLI flag sets incremental=true in eleventy.after even
for the watcher's first full build, so pagefind was never running when
the initial build was OOM-killed. Replace the incremental guard with a
pagefindDone boolean that runs pagefind exactly once per process lifetime
— whichever build completes first (initial or watcher) gets indexed.
Pagefind indexing was moved to start.sh in f2cc855 but the shell-based
approach is fragile: the recovery mechanism timed out when the initial
build was OOM-killed, leaving no search index. The eleventy.after hook
is the natural place — Eleventy knows the correct output directory and
the incremental guard prevents re-indexing on watch rebuilds.
Timeout increased to 120s for the larger site (2194 pages).
Add <is-land on:visible> lazy-loading wrapper to every widget template
and the comments section for consistent deferred rendering. Widgets
that already had it (social-activity, github-repos, blogroll, feedland,
webmentions) are unchanged. Also wraps inline search and custom-html
widgets in all sidebar container files.
The <is-land> custom element defaults to display:inline, which breaks
margin spacing between adjacent widgets. Setting it to block ensures
widgets wrapped in is-land (blogroll, social-activity, github, etc.)
get proper mb-4 spacing from the .widget class.
Show 2 recent tracks from each source (4 total) instead of only
Funkwhale. Removed stats section — users can visit /listening/ for
full statistics. Now Playing indicator works from either source.
The search and custom-html widget types were using a non-existent
sidebar-widget class instead of the standard .widget class, causing
them to lack the spacing, border, and background styling that all
other widgets get. Updated all 4 sidebar containers.
Widget cards now have white bg, border, and shadow in light mode for
clear visual separation. Each widget has bottom margin for spacing.
Recent-comments widget updated to use standard .widget/.widget-title
classes instead of custom sidebar-widget class.
Alpine CDN uses queueMicrotask to auto-start, which fires between
defer scripts. comments.js must execute before Alpine so its
alpine:init listener is registered before Alpine.start() runs.
This is the Alpine-documented pattern: "Include [component scripts]
before Alpine's core JS file."
Convert commentsSection from a global function to Alpine.data()
registration via the alpine:init event. This is the proper Alpine.js
pattern for reusable components — the component is registered in
Alpine's internal registry before DOM processing begins, eliminating
script loading order issues.
Reverts the hacky approach of moving the script tag to <head>.
Add recent-comments widget case to data-driven routers in sidebar.njk
and homepage-sidebar.njk (was missing). Add to fallback defaults in
both sidebar.njk and blog-sidebar.njk so it shows without homepage
builder configuration.
- Comment area on post pages (IndieAuth sign-in, submit, display)
- Alpine.js client-side component for auth flow and comment CRUD
- Recent comments sidebar widget with build-time data fetching
- Include comments.js in base layout, comments.njk before webmentions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Conversations items (from Mastodon/Bluesky/ActivityPub) were filtered
out by the client-side timestamp check that prevents duplicating
build-time webmentions. Since conversations data is never in the
build-time cache, bypass the filter for items with a platform field.
Use two-strategy approach to work around async shortcode limitation
in deeply nested Nunjucks includes:
- blog.njk: async {% unfurl %} shortcode (top-level, works fine)
- recent-posts.njk: sync {{ url | unfurlCard | safe }} filter
(reads from pre-populated disk cache)
eleventy.before hook scans content files and pre-fetches all
interaction URLs before templates render, ensuring the sync filter
always has data — even on first build.
CV page now reads layout config from cv-page.json when available,
supporting single-column, two-column, and full-width hero layouts with
configurable sections, sidebar widgets, and footer columns. Falls back
to the previous hardcoded layout when no config exists.
Items without a type field (existing data before type feature was added)
were only appearing in "personal" filtered views. Since the /cv/ page uses
work-only variants, all existing untyped data was hidden. Changed filtering
so untyped items appear in both work and personal views.
Also guard cv.skills dictsort in cv.njk against undefined.
The eleventy.after hook was unreliable for pagefind because:
1. When the initial build is OOM-killed, the hook never fires
2. When the watcher starts with --incremental, the hook receives
incremental=true even for the first full build, skipping pagefind
Pagefind is now run explicitly by start.sh after the initial Eleventy
build succeeds, with a recovery process for OOM-killed builds.
The hook retains WebSub hub notification only.
Add thin-wrapper templates for work/personal filtering of CV sections:
- 8 new templates: cv-{experience,education,skills,interests}-{personal,work}.njk
- cv-languages.njk: standalone languages section (split from education)
- homepage-section.njk: 9 new routes for filtered variants
- cv.njk: uses work-only variants for the /cv/ page
- Base templates: filterType support in experience, education, skills, interests
- _data/cv.js: skillTypes and interestTypes fallback fields
Show rich link preview cards in bookmarks, likes, replies, reposts
collection pages and the homepage recent posts section. URLs are
fetched once and cached — the same cache serves all templates.
The default facebookexternalhit UA causes many sites to block or
redirect-loop, resulting in timeouts. Switch to a well-identified
bot UA that sites handle correctly.
- Increase timeout from 10s to 20s
- Cache failures for 1 day (avoids retrying every build)
- Add concurrency limiter (max 5 parallel requests)
- Refactor into renderCard/renderFallbackLink helpers
The target URL in likes, bookmarks, replies, and reposts now renders
as a rich OpenGraph card via the unfurl shortcode instead of a bare
link. The raw URL remains below the card for h-cite microformat
compatibility. Results are cached in .cache/unfurl/ for 1 week.
Adds {% unfurl "URL" %} shortcode that renders any URL as a rich card
with OpenGraph metadata (title, description, image, favicon). Uses
unfurl.js locally — no external API dependency. Results cached for 1
week in .cache/unfurl/. Also fixes Mastodon embed server config
(mstdn.social → indieweb.social).
- Add <noscript><style> in base.njk that unhides x-cloak/x-show content,
hides FAB and tab buttons when JS is disabled (content stacks instead)
- Add noscript message on search page with links to blog/categories
- Add noscript banner on interactions page explaining inbound tab needs JS
- Add ActivityPub/Fediverse platform badge (purple, network icon) to
interactions page alongside existing Mastodon and Bluesky badges
- Detect platform from Bridgy source URLs and author URLs for
webmention.io items that lack a platform field
- Filter self-referencing syndication URLs from "Also on" footer so
self-hosted AP posts don't show a redundant link back to the site
AI crawlers (GPTBot) were following edit/create links in the static
HTML, flooding the server with /posts/edit and /session/login requests.
Adding nofollow tells well-behaved crawlers to skip these admin-only links.
Fetch from both /webmentions/api/mentions and /conversations/api/mentions,
merge results with conversations items taking priority (richer metadata),
and display platform badges (Mastodon/Bluesky icons) on interaction cards.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The eleventy.after hook's dir.output reflects the config default (_site),
not the --output CLI flag used by start.sh. Use directories.output which
reflects the actual resolved output path.
Templates reference /pagefind/pagefind-ui.js but pagefind defaulted
to _pagefind/ (with underscore). Added --output-subdir pagefind flag
and updated ignores to match.
Eleventy 3.x renders Nunjucks templates in parallel, causing page.url
to return wrong values in {% set %} tags. This caused OG images to be
mismatched between pages (e.g., bookmark showed note's OG image).
Move ogSlug and hasOgImage computation to eleventyComputed, which runs
during the sequential data cascade phase before parallel rendering.
The computed values are then available as plain template variables.
Refs: https://github.com/11ty/eleventy/issues/3183
The Nunjucks race condition in Eleventy 3.x affects page.url too —
its value changes between {% set %} and {{ }} within the same
template render during parallel builds. Instead of trying to derive
slugs from page data, name OG images with the full filename
(including date prefix) to match URL path segments exactly.
page.fileSlug suffers from a race condition in Eleventy 3.x parallel
rendering where Nunjucks shares state across template compilations,
causing slugs to get mixed up between pages. page.url is always
correct, so derive the OG slug from it instead.