- Remove self-mention filter (siteOrigin, isSelfMention) from webmentions.js
- Remove build-time self-mention filter from eleventy.config.js
- processWebmentions() now separates is_owner items and threads them
under parent interaction cards via threadOwnerReplies()
- owner:detected handler reduced to wireReplyButtons() only
- Remove loadOwnerReplies() and Alpine.store replies from comments.js
- Owner replies now come from conversations API with parent_url metadata
Confab-Link: http://localhost:8080/sessions/184584f4-67e1-485a-aba8-02ac34a600fe
Owner replies sent via webmention-sender appear as webmentions on
the owner's own posts, showing the reply as a top-level entry instead
of threaded. Filter out any webmention whose source URL starts with
the site URL, in both build-time (eleventy.config.js) and client-side
(webmentions.js) rendering paths.
Confab-Link: http://localhost:8080/sessions/184584f4-67e1-485a-aba8-02ac34a600fe
Replace unbounded Promise.all on 545 interaction URLs with batches
of 50. Add GC calls after the markdown walk and between batches to
free parsed content and resolved promise data before the render phase.
Logs RSS every 5th batch for memory monitoring.
Same pattern as the OG image batch spawning fix.
Confab-Link: http://localhost:8080/sessions/0b241cd6-aff2-4fec-853c-2b5a61e61946
Full OG regeneration (2,350 images) OOM-kills when the Eleventy watcher
is running (~1.8 GB RSS), leaving only ~1.2 GB headroom in the 3 GB
container. WASM native memory from Satori/Resvg grows beyond what GC
can reclaim within a single process.
Solution: spawn og-cli in batches of 100 images. Each invocation exits
after its batch, fully releasing all WASM native memory. Exit code 2
signals "more work remains" and the spawner re-loops. Peak memory per
batch stays under ~500 MB regardless of total image count.
Also seed newManifest from existing manifest so unscanned entries survive
batch writes.
Confab-Link: http://localhost:8080/sessions/edb1b7b0-da66-4486-bd9c-d1cfa7553b88
Full OG regeneration (2,350 images) was OOM-killed because WASM native
memory from Satori/Resvg accumulated between GC cycles. The previous
GC interval of 50 images allowed ~2.5-5 GB of native allocations before
reclamation. Reduce to every 5 images to keep peak RSS under ~400 MB.
Also reduce --max-old-space-size from 768 to 512 MB (V8 heap only uses
~22 MB) and add peak RSS tracking to the completion log.
Confab-Link: http://localhost:8080/sessions/edb1b7b0-da66-4486-bd9c-d1cfa7553b88
Satori (Yoga WASM) and Resvg (Rust WASM) allocate native memory that
V8 doesn't track against the heap limit. Without periodic GC, the JS
wrappers accumulate and native RSS grows to ~2 GB during OG image
generation for 3400+ posts.
- Add --expose-gc to og-cli spawn
- Call global.gc() every 50 images to reclaim native memory
- Log final RSS/heap for monitoring
Confab-Link: http://localhost:8080/sessions/edb1b7b0-da66-4486-bd9c-d1cfa7553b88
After GC, logs heap space breakdown (old_space, large_object_space, etc.)
to help identify memory consumers. Supports HEAP_SNAPSHOT=1 env var to
write a heap snapshot to /tmp for detailed analysis.
Confab-Link: http://localhost:8080/sessions/edb1b7b0-da66-4486-bd9c-d1cfa7553b88
Add global.gc() call in eleventy.after handler to release unused heap
pages back to the OS. Without this, V8 keeps ~2 GB of build-time
allocations resident in watch mode because there's no allocation
pressure to trigger GC naturally. Requires --expose-gc in NODE_OPTIONS
(set in start.sh for the watcher process).
Confab-Link: http://localhost:8080/sessions/edb1b7b0-da66-4486-bd9c-d1cfa7553b88
- 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.