Add stripTrailingSlash filter and use it in the link tag so the
alternate URL is /articles/.../slug.md (matching nginx routing)
instead of /articles/.../slug/index.md.
The extractFirstImage filter picks up <img> tags from the full rendered
page content, including sidebar widgets (like recent post thumbnails).
This caused og:image to reference sidebar OG images from OTHER posts
instead of falling through to the __OG_IMAGE_PLACEHOLDER__ that the
og-fix transform resolves from outputPath.
Only ogPhoto and image (from frontmatter) are now used as explicit
image sources. All other cases use the placeholder resolved by the
og-fix transform.
Eleventy 3.x no longer allows synchronous access to template internals
from pagination templates. Replace article.template.frontMatter.content
with a custom filter that reads the source file via gray-matter.
The Eleventy 3.x parallel rendering race condition (#3183) makes
page.url unreliable in templates — it changes between lines during
concurrent processing. All previous approaches (eleventyComputed,
capturing page.url early with {% set %}) failed because the page
object is shared and mutated by parallel renders.
The transform approach works because outputPath is passed as a
function parameter (not read from a shared object) and IS correct
since files are written to the right location. The transform:
- Derives the OG slug from outputPath pattern matching
- Replaces __OG_IMAGE_PLACEHOLDER__ with the correct OG image URL
- Replaces __TWITTER_CARD_PLACEHOLDER__ with the correct card type
- Fixes og:url and canonical URL from outputPath
Generate index.md alongside index.html for /articles/ at build time.
Agents can access clean Markdown via .md URL extension or Accept:
text/markdown content negotiation. Includes configurable content-signal
policy (ai-train, search, ai-input) and a master on/off toggle via
MARKDOWN_AGENTS_ENABLED env var.
Pages with permalink:false (like about.njk) have page.url as false,
which crashes inline string operations. Use the ogSlug filter with
(page.url or "") guard to handle falsy values safely. Also removes
debug comment from previous debugging session.
permalink is set by eleventyComputed which cross-contaminates return
values across pages during Eleventy 3.x parallel rendering. page.url
is set by Eleventy's internal pipeline and is correct in templates
(verified via og:url meta tag which always shows the right URL).
Both page.url AND page.inputPath are unreliable in eleventyComputed due to
Eleventy 3.x parallel rendering (issue #3183). They return values from OTHER
pages being processed concurrently, causing og:image meta tags to reference
wrong OG images.
Fix: compute ogSlug directly in base.njk from the permalink data value using
existing Nunjucks filters (ogSlug, hasOgImage). permalink comes from frontmatter
(per-file data) and is immune to cross-page contamination.
page.url in eleventyComputed returns URLs from other pages being
processed concurrently in Eleventy 3.x parallel rendering. This caused
OG images to show wrong post types and titles (e.g., a note showing
"Reply" badge from a completely different post).
Fix: use page.inputPath (physical file path) which is always correct,
matching the approach already used by the permalink computation.
The .post-list li rule used border-surface-200 which sets border-color
for ALL sides (shorthand). Combined with its higher specificity (0-1-1
vs 0-1-0), this overrode the border-l-{color} utility classes on
.post-card elements. Changing to border-b-surface-200 restricts the
color to only the bottom separator border, allowing the left border
color utilities to apply correctly.
Add color-coded left borders to post cards on all blog listing and
category pages, and make sidebar widgets collapsible with localStorage
persistence on both listing and single-post sidebars.
The widget used bare 'recentPosts' variable which doesn't exist in
the homepage context (it's a collection). Add fallback to
collections.recentPosts so the widget works on all pages.
site.url had a trailing slash (added for Mastodon rel=me verification),
which caused double slashes in all URL constructions like
{{ site.url }}/auth → https://rmendes.net//auth
This broke IndieAuth login — indielogin.com read the authorization_endpoint
link tag with //auth and redirected users there, which 404'd in nginx.
Split into site.url (no slash, for URL construction) and site.me /
site.author.url (with slash, for Mastodon rel=me strict matching).
Also fixed twitter:image meta tags to use smart slash logic matching
the og:image pattern (check if path starts with / before prepending one).
The homepage uses cv-projects-personal.njk, not cv-projects.njk.
Apply the same collapsible accordion pattern to both personal
and work project section variants.
Sidebar widgets are now wrapped in a collapsible Alpine.js container
with a title + chevron toggle. First 3 widgets open by default, rest
collapsed. State persists in localStorage across page loads. Inner
widget titles hidden by CSS to avoid duplication with wrapper titles.
Project cards now show a compact summary row (name, status badge, date
range, chevron) that expands on click to reveal description and tech
tags. Uses Alpine.js with independent toggles and smooth transitions.
Each post card in the recent-posts section now has a 3px left border
colored by post type: red for likes, amber for bookmarks, green for
reposts, blue for replies, purple for photos, neutral for articles/notes.
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>.