- Add skip-to-main-content link and main content ID target
- Add prefers-reduced-motion media queries for all animations
- Enhance visible focus indicators (2px offset, high-contrast ring)
- Replace ~160 text-surface-500 instances with text-surface-600/dark:text-surface-400
for 4.5:1+ contrast ratio compliance
- Add aria-hidden="true" to ~30+ decorative SVG icons across sidebars/widgets
- Convert facepile containers from div to semantic ul/li with role="list"
- Add aria-label to icon-only buttons (share, sort controls)
- Add sr-only labels to form inputs (webmention, search)
- Add aria-live="polite" to dynamically loaded webmentions
- Add aria-label with relative+absolute date to time-difference component
- Add keyboard handlers (Enter/Space) to custom interactive elements
- Add aria-label to nav landmarks (table of contents)
- Fix modal focus trap and dialog accessibility
- Fix lightbox keyboard navigation and screen reader announcements
Confab-Link: http://localhost:8080/sessions/edb1b7b0-da66-4486-bd9c-d1cfa7553b88
SVGs with only a viewBox and no width/height attributes use intrinsic
sizing that can override CSS width:100%. Adding width="100%" height="100%"
and preserveAspectRatio="none" on the SVG element itself ensures the
sparkline fills its container div.
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596
- YouTube embeds now use lite-youtube facade (loads iframe on click,
~800 KiB savings per page with embedded videos)
- Avatar resized from 1000x1000 to 400x400 (152 KiB → 39 KiB)
- lite-yt-embed.css max-width changed to 100% for responsive layout
- Removed unused Tailwind primary color palette from CSS bundle
Confab-Link: http://localhost:8080/sessions/edb1b7b0-da66-4486-bd9c-d1cfa7553b88
- Replace broken client-side type filter on /blog/ with navigation
pill links to dedicated collection pages (with post counts)
- Replace Load More with proper prev/next/page-number pagination
on Interactions inbound tab (20 per page, filter resets page)
- Add auto-unfurl transform for standalone external links in notes
- Exclude Digest and Categories pages from Pagefind search index
- Add Pagefind search filters for post type, year, and category
- Add Pagefind filter metadata to page.njk layout
Confab-Link: http://localhost:8080/sessions/956f4251-b4a9-4bc9-b214-53402ad1fe63
Adds a markdown-it inline rule that transforms #tag text into
links to /categories/tag/ on-site. Syndication targets (Bluesky,
Mastodon, Bridgy) continue to receive raw #tag text, which their
native facet/hashtag detection handles automatically.
Edge cases handled: headings, hex colors, URL fragments, code
blocks, pure numbers are all excluded from conversion.
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596
Add `featuredPosts` collection filtering posts with `featured: true`
frontmatter. New `featured-posts` section template with type-aware
rendering (articles, notes, photos, bookmarks, etc.) and star icon
header. Registered in homepage-section.njk dispatcher.
To feature a post, add `featured: true` to its frontmatter. Then add
a `{ "type": "featured-posts" }` section to the homepage config.
Confab-Link: http://localhost:8080/sessions/bd3f7012-c703-47e9-bfe2-2ad04ce1842d
Add an eleventy.after hook that triggers syndication immediately after
incremental rebuilds, cutting latency from ~2 min (poller) to ~5 sec.
Uses built-in crypto for HS256 JWT — no new dependencies.
Confab-Link: http://localhost:8080/sessions/d116ad5b-ef8a-424e-9ebe-76c06bef1df6
- Add teal accent color scale and activate Inter font via @font-face
- Neutralize nav/footer hovers from primary blue to surface neutrals
- Apply accent color to hero subtitle, FAB, CTA buttons, card hovers
- Fix reply post-type color from generic primary to distinctive sky blue
- Create centralized icon macro (icon.njk) with 24 reusable SVG icons
- Add per-widget-type icons and colored left-accent borders to all sidebars
- Update .p-category tags from blue to neutral surface with border
- Diversify color vocabulary: red (likes), amber (bookmarks/blogroll),
green (reposts), purple (funkwhale), sky (replies), orange (subscribe)
Confab-Link: http://localhost:8080/sessions/bd3f7012-c703-47e9-bfe2-2ad04ce1842d
Default "false" — adds eleventy:ignore to remote <img> tags via a
posthtml plugin (priority 1) that runs before eleventy-img (priority -1).
Sharp only processes local images, avoiding OOM from downloading and
decoding hundreds of external images.
Set PROCESS_REMOTE_IMAGES=true to restore previous behavior.
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596
- transformOnRequest: process images on-demand in watch mode instead
of all at once during rebuild (same pattern as zachleat.com)
- cacheOptions: cache remote image fetches to disk (1d build, 30d watch)
- concurrency: 4 (down from default ~10 based on CPU count) to limit
Sharp's native memory usage from parallel image decodes
Root cause: Sharp processes remote images outside V8 heap, so
--max-old-space-size doesn't cap total memory. Large remote images
(e.g. 3072px-wide) at concurrency 10 spike native memory enough
to exceed the 3GB cgroup limit.
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596
During watcher/incremental builds, .cache/og is in watchIgnores so
Eleventy's passthrough copy doesn't pick up newly generated OG images.
After OG generation, manually copy any new .png files from .cache/og/
to _site/og/ so they're immediately available to serve.
Confab-Link: http://localhost:8080/sessions/956f4251-b4a9-4bc9-b214-53402ad1fe63
5,137 starred repos in Nunjucks template + Pagefind indexing exceeded
the 2048MB Eleventy heap limit during build. Switched to Alpine.js
client-side rendering:
- _data/githubStarred.js: returns only buildDate (no API fetch)
- starred.njk: fetches /githubapi/api/starred/all via Alpine.js
- Added client-side text search (replaces separate Pagefind index)
- Removed pagefind-starred build step and --exclude-selectors flag
Confab-Link: http://localhost:8080/sessions/b130e9e5-4723-435d-8d5a-fc38113381c9
- New starred.njk page rendering all ~5k starred repos as searchable cards
- Separate Pagefind index (pagefind-starred) for starred-only search
- Alpine.js live updates section for stars added since last build
- Load More pagination (50 at a time, all in DOM)
- githubStarred.js data file fetching from plugin API (1d cache)
- Link from /github/ to /github/starred/
- Exclude starred cards from main site Pagefind index
Confab-Link: http://localhost:8080/sessions/b130e9e5-4723-435d-8d5a-fc38113381c9
When Micropub creates a post, the markdown file is written twice in quick
succession — first the initial content, then ~2s later a syndication update
adds syndication URLs. Without debouncing, the watcher rebuilds from the
first write and misses the second, causing "Also on" links to not appear.
- awaitWriteFinish (2s stability threshold): delays watcher events until
the file hasn't been written to for 2 seconds
- setWatchThrottleWaitTime (3s): groups all file changes within 3 seconds
into a single build
Confab-Link: http://localhost:8080/sessions/956f4251-b4a9-4bc9-b214-53402ad1fe63
The interactive/ directory contains self-contained HTML files with
JavaScript that Nunjucks incorrectly parses as template syntax. Add
to ignores so Eleventy only passthrough-copies without processing.
- Added weeklyDigests collection after recentPosts collection
- Groups published posts (excluding replies) by ISO 8601 week
- Supports both camelCase and underscore property names
- Includes byType grouping (articles, notes, photos, etc.)
- Calculates week start/end dates for display
- Excludes interactive directory from builds via .eleventyignore
Add a reusable fullwidth layout (layouts/fullwidth.njk) for rich HTML
content that needs the full container width without sidebar or prose
constraints. Add the interactive architecture explorer as a static
asset served via passthrough copy at /interactive/architecture.html.
- layouts/fullwidth.njk: site header + footer only, no sidebar
- interactive/architecture.html: tabbed architecture guide
- eleventy.config.js: passthrough copy for interactive/ directory
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.
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
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.
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).
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.
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.
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).
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.
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.
OG image generation writes to .cache/og/ which the watcher detects
as file changes, triggering another build that runs OG generation
again in an infinite loop.
Remove the incremental skip guard from OG generation. The manifest
caching already handles performance — only new posts without an
existing OG image get generated (~200ms each). Without this fix,
posts created via Micropub never got OG images until container restart.
Skip OG image generation, Pagefind indexing, and WebSub notification
during incremental rebuilds. These expensive operations only run on
full builds (container restart), not on every content change.
The @zachleat/table-saw component requires tables to be wrapped in
<table-saw> elements. Added an Eleventy transform to do this
automatically for all HTML output.
- Add time-difference web component for relative dates
- Add @zachleat/table-saw for responsive tables
- Add webmention facepile styling with bookmarks support
- Add OG image thumbnails to post navigation
- Add @11ty/is-land for lazy widget hydration
- Wrap sidebar widgets in is-land for deferred loading
- Lazy-load webmention avatars with is-land
- Add @zachleat/filter-container for blog archive filtering
- Add posting frequency sparkline to blog header
- Inline critical CSS and defer full stylesheet loading
Malformed HTML (e.g. unescaped quotes in iframe title attributes) caused
html-minifier-terser to throw a fatal parse error, killing the entire
Eleventy build. Now catches the error, logs a warning, and returns the
unminified content so the build completes.
Eleventy v3 parses YYYY-MM-DD- from filenames and removes it from
page.fileSlug. The OG generator was using the full filename (with
date prefix) causing a slug mismatch — hasOgImage filter checked
for 'slug.png' while the file was 'YYYY-MM-DD-slug.png'.
Also removes debug logging from hasOgImage filter.
Two fixes:
- extractFirstImage filter now skips <img> tags with the hidden attribute,
preventing the author avatar microformat from being used as og:image
- Clear NODE_OPTIONS in the OG subprocess env and pass --max-old-space-size
as a direct CLI flag to avoid conflict with parent process settings
The OG generation in the eleventy.before hook consumed too much memory
alongside Eleventy's data cascade, causing the Eleventy process to be
OOM-killed on Cloudron. Fix by running OG generation in a separate
child process with its own 768MB heap limit. Also write the manifest
incrementally (every 10 images) to preserve progress if interrupted.
- Add _textcasting extension to JSON feed with support/monetization config
- Add feedAttachments filter for photo/audio/video media in feed items
- Add content_text and date_modified fields to feed items
- Add protocol badges (ATmosphere, Fediverse, Web) on reply posts
- Add support configuration via environment variables in site data
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>