379 Commits

Author SHA1 Message Date
Ricardo
e0913fb976 diag: persist memory profiling to file (log buffer gets flushed) 2026-03-30 17:52:30 +02:00
Ricardo
a96787bef4 diag: add memory profiling at build phases 2026-03-30 17:35:49 +02:00
Ricardo
d5b9579d90 fix: exclude .interface-design from sitemap 2026-03-29 11:19:07 +02:00
Ricardo
56ab15e439 fix: regenerate sitemap.xml on incremental builds too 2026-03-29 11:06:09 +02:00
Ricardo
7f26d7398b fix: generate sitemap.xml in eleventy.after hook instead of template
The @quasibit/eleventy-plugin-sitemap shortcode triggers
TemplateContentPrematureUseError on paginated templates in Eleventy 3.x.
Move sitemap generation to eleventy.after hook which scans the output
directory for index.html files after all templates are rendered. Remove
the plugin import/registration and the sitemap.njk template.
2026-03-29 11:05:42 +02:00
Ricardo
489c467a3f feat: add sitemap.xml generation with URL pattern exclusions
Wire up @quasibit/eleventy-plugin-sitemap (already installed) with a
sitemap.njk template and excludeFromSitemap filter. Excludes replies,
feeds, categories, digest, debug pages, admin pages, and 404.
2026-03-29 10:59:33 +02:00
Ricardo
79d072ac59 feat: create startup-gate readiness signal after successful build 2026-03-28 22:57:21 +01:00
Ricardo
c6ea35d28a docs: add note about getFilteredByGlob cache behavior 2026-03-28 19:57:48 +01:00
Ricardo
5ca8f83873 feat: add /updated.xml feed for recently edited posts
New RSS feed at /updated.xml that surfaces posts where the updated
date is newer than the published date. Complements /feed.xml (new
posts) with a dedicated feed for edits.

- recentlyUpdated collection: filters posts with updated > published,
  sorted by update date, limited to 20
- Unique guid per edit (url#updated-date) so feed readers treat
  updates as new entries
- Auto-discovery link in <head> and footer link
2026-03-28 18:46:06 +01:00
Ricardo
d3146bf0c1 fix: show Fediverse button from mpSyndicateTo before syndication runs
The Fediverse button only appeared after the syndication endpoint wrote
the syndication array to frontmatter. If the Eleventy watcher missed
the second file update (timing race), the button never showed.

Now falls back to mpSyndicateTo — which is written at post creation
time — to render the button on the first build. Once syndication
populates the syndication array, it takes over.
2026-03-27 18:59:24 +01:00
Ricardo
8af3cc329d fix: pagination scrambling and scroll + feat: excludePostTypes filter
- Fix duplicate x-for keys causing scrambled pagination numbers on
  interactions page (two '…' entries shared the same key)
- Fix scroll target in goToPage — was using dead closest('[x-show]')
  selector, now scrolls to #webmentions-list
- Add flex-wrap to pagination-links for mobile overflow
- Add excludePostTypes Eleventy filter to exclude post types from
  collections by detecting type from frontmatter properties
- Wire excludePostTypes into recent-posts section via sectionConfig
- Add error/stale data banner to changelog page
2026-03-26 16:25:36 +01:00
Ricardo
a3cb1c1f55 docs: document replyTargets architecture in CLAUDE.md 2026-03-26 15:42:00 +01:00
Ricardo
34fdab4b85 feat: use replyTargets config for platform-to-syndicator mapping
- Frontend now reads replyTargets from isOwner API to resolve which
  syndicator handles replies for each platform
- Build-time reply buttons get platform from URL heuristics as fallback
- enrichBuildTimeBadges upgrades to NodeInfo-resolved platform at runtime
2026-03-26 08:21:54 +01:00
Ricardo
dcd73b1897 fix: frontend reply syndication mapping and empty array cleanup
- Map platform to syndicator using service.name keys from isOwner API
- Add activitypub platform mapping (was completely missing)
- Remove empty mp-syndicate-to array for webmention replies (caused
  permanent no-op entries in syndication queue)
2026-03-25 22:41:14 +01:00
Ricardo
9dc02102ad fix: deduplicate interactions by author+type+target, not just wm-id
Same interaction arriving from webmention.io and conversations API had
different wm-id values but same author/type/target. Now normalizes URLs
and deduplicates by semantic identity.
2026-03-24 11:47:15 +01:00
Ricardo
c6165bd7af feat: add view mode toggle for changelog page
Add a "Group by: Repository | Change Type" segmented toggle to the
changelog page. Users can now switch between repo-based tabs (Core,
Endpoints, Syndicators, etc.) and commit-type tabs (Features, Fixes,
Refactor, etc.) with reactive Alpine.js computed getters.

Both views share the same commit data — only the grouping dimension
and color scheme change. Tab counts and badge labels update
reactively when switching view modes.

Confab-Link: http://localhost:8080/sessions/5767023f-100b-4b9c-85fc-12d7e1ab248a
2026-03-16 18:43:13 +01:00
Ricardo
03f28f5efd chore: move plans to centralized documentation-central
Plans relocated to ~/code/indiekit-dev/documentation-central/plans/

Confab-Link: http://localhost:8080/sessions/d6567f44-c576-4acd-9c8c-454aa58fbde9
2026-03-16 15:23:48 +01:00
Ricardo
d4984f43bf fix: enrich build-time reply badges with conversations API platform data
Build-time reply cards rendered from webmention.io cache lack platform
provenance — they fall through URL heuristics to "webmention" (IndieWeb badge).
After conversations API data arrives with NodeInfo-resolved platform names,
enrichBuildTimeBadges() upgrades matching build-time cards with correct badges.

Also adds wm-provenance-badge class to all badge variants for reliable
DOM selection during enrichment.

Confab-Link: http://localhost:8080/sessions/184584f4-67e1-485a-aba8-02ac34a600fe
2026-03-15 16:11:39 +01:00
Ricardo
6046eceaac feat: use NodeInfo-resolved platform for provenance badges
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
2026-03-15 15:16:40 +01:00
Ricardo
ddf272dac9 docs: document reply-to-interactions feature
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
2026-03-15 14:07:31 +01:00
Ricardo
c7c0f4e0a4 refactor: unified owner reply threading via conversations API
- 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
2026-03-15 12:45:55 +01:00
Ricardo
55927722cc fix: filter out self-mentions from webmentions display
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
2026-03-15 09:23:41 +01:00
Ricardo
3eacba1672 fix: don't send empty mp-syndicate-to in Micropub reply
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
2026-03-14 23:40:43 +01:00
Ricardo
395750da9b fix: show inline reply form under webmention cards
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
2026-03-14 22:49:15 +01:00
Ricardo
02546950bf fix: use Alpine.js v3 API for reply button click handler
Alpine v3 uses Alpine.$data(el) instead of el.__x.$data.
The old v2 pattern silently failed, making Reply buttons non-functional.

Confab-Link: http://localhost:8080/sessions/184584f4-67e1-485a-aba8-02ac34a600fe
2026-03-14 21:43:33 +01:00
Ricardo
0fe6ab0195 fix: reply buttons on dynamic webmentions + owner comment form
- 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
2026-03-14 20:12:06 +01:00
Ricardo
a9b4300d7b fix: reply buttons hidden + missing webmentions on pages without build-time data
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
2026-03-14 19:19:11 +01:00
Ricardo
58e3695d68 fix: use DOM-based dedup instead of timestamp for client-side webmentions
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
2026-03-14 18:30:04 +01:00
Ricardo
39351c4728 feat: reply-to-interactions frontend
- 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
2026-03-14 16:34:56 +01:00
Ricardo
61db75bd76 fix: hide TOC widget wrapper when no headings found
When tocScanner finds no .e-content or fewer than 3 headings,
hide the parent .widget-collapsible container so the empty
collapsible chrome (header button, border) doesn't show.

Confab-Link: http://localhost:8080/sessions/cc343b15-8d10-43cd-a48f-ca912eb79b83
2026-03-11 14:13:57 +01:00
Ricardo
e1aa8cb762 feat(og): v3 centered card layout with improved visual balance
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
2026-03-10 20:53:27 +01:00
Ricardo
4adb8f0afd feat(og): GitHub-inspired card design + first-paragraph-only extraction
- 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
2026-03-10 20:15:05 +01:00
Ricardo
b3b65bf891 fix(og): strip markdown tables, lists, and non-renderable chars from body text
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
2026-03-10 19:51:50 +01:00
Ricardo
0fe99ee5b1 perf: add timeout and watch-mode cache extension to all data files
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
2026-03-10 17:11:24 +01:00
Ricardo
f7d452fc30 perf: memoize all date filters and optimize youtube pre-check
dateDisplay: 16,935 calls → ~2,350 unique dates cached (Map + eleventy.before clear)
date: 33,025 calls → ~2,350 unique date+format combos cached
isoDate: 9,696 calls → same memoization pattern

youtube-link-to-embed: single includes("youtu") replaces two separate
substring scans on 15-50KB HTML per page.

Confab-Link: http://localhost:8080/sessions/0b241cd6-aff2-4fec-853c-2b5a61e61946
2026-03-10 16:38:00 +01:00
Ricardo
1cdc4b89a7 fix: handle non-string outputPath in html-transformer override
outputPath can be `false` for pages without output. Optional chaining
(?.) only guards null/undefined, not booleans — use typeof check instead.

Confab-Link: http://localhost:8080/sessions/0b241cd6-aff2-4fec-853c-2b5a61e61946
2026-03-10 16:19:59 +01:00
Ricardo
ea7433852d perf: memoize aiPosts/aiStats filters and skip PostHTML for imageless pages
aiPosts/aiStats: cache filter results per build — 694 calls × 2,350 posts
= 1.6M iterations reduced to 1. Saves ~2.2s per incremental rebuild.

html-transformer: override default transform with content pre-check that
skips PostHTML parse/serialize (~3ms/page) for pages without <img> tags.
Both registered PostHTML plugins only target <img> elements.

Confab-Link: http://localhost:8080/sessions/0b241cd6-aff2-4fec-853c-2b5a61e61946
2026-03-10 16:13:54 +01:00
Ricardo
8a7e45cea7 perf: memoize hash filter and optimize transforms
- Cache hash filter results per build (55,332 → 16 file reads)
- Cache OG directory listing for og-fix transform (3,426 → 1 readdirSync)
- Early-exit youtube-link-to-embed on pages without YouTube links
- All caches clear on eleventy.before for correct incremental rebuilds

Confab-Link: http://localhost:8080/sessions/0b241cd6-aff2-4fec-853c-2b5a61e61946
2026-03-10 15:36:20 +01:00
Ricardo
129e0720af perf: batch unfurl pre-fetch to reduce peak memory
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
2026-03-10 14:44:51 +01:00
Ricardo
48160a5b13 feat: client-side TOC widget with Alpine.js scroll spy
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
2026-03-10 13:01:53 +01:00
Ricardo
508ddf03ca docs: document OG batch spawning architecture in CLAUDE.md
Confab-Link: http://localhost:8080/sessions/edb1b7b0-da66-4486-bd9c-d1cfa7553b88
2026-03-09 18:35:09 +01:00
Ricardo
db10d9cfbf fix(og): batch spawning to prevent OOM during watcher rebuilds
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
2026-03-09 17:37:17 +01:00
Ricardo
6dd8f03214 fix(og): aggressive GC to prevent OOM in constrained containers
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
2026-03-09 17:25:05 +01:00
Ricardo
bfd885cbc9 perf: add periodic GC to og-cli to reclaim WASM native memory
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
2026-03-09 17:03:44 +01:00
Ricardo
8753e73709 feat: add V8 heap space diagnostics to post-build GC hook
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
2026-03-09 16:44:33 +01:00
Ricardo
9f591ca2fb fix: update avatar dimensions in hero section (Tier 1) to match CSS
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
2026-03-09 00:31:48 +01:00
Ricardo
17c21b2b8f perf: fix desktop CLS (0.57) — grid match, font-display optional, avatar sizing
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
2026-03-09 00:19:20 +01:00
Ricardo
9e8f0f139a perf: remove skeleton loader to fix CLS (0.916 mobile / 1.004 desktop)
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
2026-03-08 15:23:27 +01:00
Ricardo
229f770cbb perf: force V8 garbage collection after Eleventy builds
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
2026-03-08 12:52:42 +01:00
Ricardo
254d5069f7 fix: move focus-trap logic from inline attribute to JS method
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
2026-03-07 20:35:32 +01:00