detectPlatform() now checks item.platform first (set by conversations
API via NodeInfo) before falling back to URL heuristics. Mastodon gets
its own badge, Bluesky gets its own, all other fediverse software
shows the Fediverse badge, and webmention.io data uses Bridgy URL
heuristics as fallback.
Confab-Link: http://localhost:8080/sessions/184584f4-67e1-485a-aba8-02ac34a600fe
Add Reply-to-Interactions section to README with architecture diagram,
threading mechanism, reply routing table, and plugin dependencies.
Update CLAUDE.md with interaction API sources and reply architecture.
Add conversations and comments plugins to the plugin integration table.
Confab-Link: http://localhost:8080/sessions/184584f4-67e1-485a-aba8-02ac34a600fe
- 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
Sending mp-syndicate-to: [] caused a server-side crash in jf2.js
where syndicateTo?.includes() received a non-iterable after
normalization. Only include the property when a target exists.
Confab-Link: http://localhost:8080/sessions/184584f4-67e1-485a-aba8-02ac34a600fe
Reply buttons on webmention interactions (Bluesky, Mastodon, IndieWeb)
now show an inline reply form directly under the card instead of
delegating to the hidden Comments section. The form posts via Micropub
with optional syndication targeting.
Confab-Link: http://localhost:8080/sessions/184584f4-67e1-485a-aba8-02ac34a600fe
- Add .wm-reply-btn button and .wm-owner-reply-slot to dynamically
created reply elements (parity with build-time Nunjucks template)
- Extract wireReplyButtons() so buttons are wired both on owner:detected
and after dynamic replies are appended (fixes timing gap)
- Use data-wired attribute to prevent double-wiring
- Show comment form for site owner (isOwner) not just IndieAuth users
- Fix "Signed in as" display to use ownerProfile when user is null
Confab-Link: http://localhost:8080/sessions/184584f4-67e1-485a-aba8-02ac34a600fe
Two bugs fixed:
1. Reply buttons stayed hidden despite owner being detected. The
alpine:initialized event fires before the async checkOwner() fetch
resolves, so isOwner was always false when the handler ran. Fix:
dispatch custom owner:detected event from init() after both owner
check and owner replies are loaded.
2. Client-side webmentions not rendering on pages with zero build-time
webmentions. createWebmentionsSection() looked for .webmention-form
but the <details> element lacked that class, so the insertion point
was never found. Fix: add webmention-form class to the details element.
Confab-Link: http://localhost:8080/sessions/184584f4-67e1-485a-aba8-02ac34a600fe
The previous approach filtered client-side webmentions by timestamp
(only show items received after buildTime). This missed webmentions
that existed in the API but weren't included in the build-time cache
(e.g., Bluesky interactions via Bridgy that webmention.io stored but
the Eleventy cache plugin didn't fetch).
Now scans the DOM for actually-rendered items: author URLs in facepiles
for likes/reposts/bookmarks, and wm-url on reply cards. Only appends
webmentions not already visible, regardless of when they were received.
Confab-Link: http://localhost:8080/sessions/184584f4-67e1-485a-aba8-02ac34a600fe
- Owner detection via Alpine.js global store (shared across components)
- Inline reply form for native comments with threaded display
- Micropub reply support for social/webmention interactions
- Provenance badges (Mastodon/Bluesky/ActivityPub/IndieWeb) on webmentions
- detectPlatform() for both build-time and client-side webmentions
- Reply buttons on webmention cards (owner only)
- Threaded owner reply display under matching webmentions
- Auto-expand comments section when comments exist
- Hide IndieAuth sign-in when admin session detected
- Author badge on owner comments and replies
Confab-Link: http://localhost:8080/sessions/184584f4-67e1-485a-aba8-02ac34a600fe
Vertically center all content (badge, title, description, avatar) to
eliminate dead space. Move accent bar to top, inline badge+date row,
site name bottom-right. Bump DESIGN_VERSION to 3 for full regeneration.
Confab-Link: http://localhost:8080/sessions/5565387e-4eb5-4441-89fb-2c6347de8e0c
- Light background, clean typography hierarchy, avatar, metadata row, accent bar
- extractBodyText → extractFirstParagraph (stops at first paragraph break)
- Articles with fm.title get body text as description; notes show first paragraph as title
- DESIGN_VERSION bump forces full regeneration without manual cache clearing
- sanitize() strips non-renderable chars to prevent Satori NO GLYPH artifacts
Confab-Link: http://localhost:8080/sessions/5565387e-4eb5-4441-89fb-2c6347de8e0c
extractBodyText() was too naive - markdown tables (|...|), heading anchors
({#id}), list numbering, and HTML tags leaked into OG image titles for
notes without explicit titles. Characters outside Inter font coverage
caused Satori to render "NO GLYPH" vertically on the card.
Confab-Link: http://localhost:8080/sessions/5565387e-4eb5-4441-89fb-2c6347de8e0c
Introduce shared cachedFetch helper (lib/data-fetch.js) wrapping
EleventyFetch with two protections:
- 10-second hard timeout via AbortController on every network request,
preventing slow or unresponsive APIs from hanging the build
- 4-hour cache TTL in watch/serve mode (vs 5-15 min originals), so
incremental rebuilds serve from disk cache instead of re-fetching
APIs every time a markdown file changes
All 13 network _data files updated to use cachedFetch. Production
builds keep original short TTLs for fresh data.
Targets the "Data File" benchmark (12,169ms / 32% of incremental
rebuild) — the largest remaining bottleneck after filter memoization.
Confab-Link: http://localhost:8080/sessions/0b241cd6-aff2-4fec-853c-2b5a61e61946
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
Replace the server-side toc.njk placeholder (which never rendered because
no code populated the `toc` variable) with a client-side Alpine.js component
that scans .e-content headings at page load, builds a dynamic table of
contents, and highlights the current section via IntersectionObserver.
- Only appears on articles/notes with 3+ headings (h2-h4)
- Excluded at build time for bookmarks, likes, and reposts
- Scroll spy activates heading in top 30% of viewport
Confab-Link: http://localhost:8080/sessions/cc343b15-8d10-43cd-a48f-ca912eb79b83
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
The previous commit fixed the Tier 2 default hero avatar (home.njk),
but production uses the homepage builder (Tier 1) which renders
hero.njk instead. Same issue: HTML width/height 96x96 but CSS sets
sm:w-32/h-32 (128px) on desktop, causing CLS on resize.
Confab-Link: http://localhost:8080/sessions/edb1b7b0-da66-4486-bd9c-d1cfa7553b88
Three root causes identified via PageSpeed layout shift culprits:
1. Grid mismatch (CLS 0.495): Critical CSS used `2fr 1fr` but Tailwind
compiles to `repeat(3, minmax(0, 1fr))` with `grid-column: span 2`.
Updated critical CSS to match Tailwind's exact output.
2. Font swap FOUT (CLS 0.074): @font-face declarations were only in the
deferred stylesheet. Moved to critical CSS with font-display:optional
and added <link rel="preload"> for weights 400/600/700. Changed all
font-display from swap to optional in tailwind.css source.
3. Avatar resize: HTML width/height was 96x96 but CSS sets sm:w-32/h-32
(128px) on desktop. Updated attributes to 128x128.
Confab-Link: http://localhost:8080/sessions/edb1b7b0-da66-4486-bd9c-d1cfa7553b88
The skeleton-to-content swap was the root cause of extreme CLS scores.
Critical CSS already provides correct first-paint layout, making the
skeleton unnecessary. Removes html.loading class, skeleton div,
page-content wrapper, and all skeleton CSS rules.
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
The @keydown.tab handler in fediverse-modal.njk contained complex
inline JS with arrow functions, querySelector strings with escaped
quotes, and comparison operators — all of which confused
html-minifier-terser's HTML parser, causing parse errors on every
page that includes the modal (i.e., nearly every page).
Moved the focus-trap logic to a trapFocus() method on the Alpine
component where it belongs.
Confab-Link: http://localhost:8080/sessions/edb1b7b0-da66-4486-bd9c-d1cfa7553b88
The accessibility change from <span> to <ul> caused categories to stack
vertically. Add flex utilities to maintain horizontal flow while keeping
the semantic list markup and ARIA attributes.
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596
- 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
Articles, notes, and bookmarks were all sharing amber, which
defeats the purpose of per-type color identity.
New complete color map (7 unique colors):
- Articles: indigo (long-form writing)
- Notes: teal (short posts)
- Bookmarks: amber (saved links)
- Likes: red (heart)
- Replies: sky (conversation)
- Reposts: green (sharing)
- Photos: purple (visual)
Updated across collection pages, blog.njk mixed view,
recent-posts widget (now shows type-specific icons and colors
for all 7 types), and design system documentation.
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596
Each post type now has a unique, consistent color applied across
sparkline, card border, icon, label, hover state, and permalink:
- Articles/Notes/Bookmarks: amber
- Likes: red (was rose)
- Replies: sky (was rose)
- Reposts: green (was rose)
- Photos: purple
Replaces generic accent/rose colors with type-specific colors in
blog.njk mixed-type view and all individual collection pages.
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596
Match sparkline wrapper colors to the post type icon colors used in the
recent posts widget: replies=sky, reposts=green, likes=red, photos=purple.
Articles, notes, bookmarks, and blog keep amber (writing domain).
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596
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
The sparkline SVG containers were using <span> elements as flex items.
While a span's outer display is blockified in flex context, its inner
display remains inline, causing SVG width:100% to resolve against the
inline content width (~22px) instead of the flex-allocated width (~670px).
Switching to <div> provides block inner display, allowing the SVG to
fill the available space correctly.
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596
Replace unstyled content flash with pulsing gray placeholder shapes
while the deferred Tailwind stylesheet loads. Uses a 'loading' class
on <html> that critical CSS uses to show skeleton / hide content,
removed by the stylesheet's onload handler.
Includes noscript fallback to bypass skeleton when JS is disabled.
Confab-Link: http://localhost:8080/sessions/edb1b7b0-da66-4486-bd9c-d1cfa7553b88
- Sparkline: change from fixed 120/180px to fluid width (flex-1 min-w-0)
filling the content area next to page titles across all 8 post type pages
- Blog filter: fix dark mode active pill contrast (dark:bg-accent-700)
- Interactions: replace wrong share icon with correct ActivityPub logo
- Interactions: add IndieWeb webmention provenance badge (globe icon)
- Interactions: improve platform detection (Bridgy Fed, more Fediverse instances)
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596