97 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
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
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
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
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
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
6ff40c8317 perf: address PageSpeed Insights issues (CLS, contrast, touch targets, JS minification)
- Reserve sidebar min-height on desktop to prevent CLS from Alpine.js hydration
- Defer lite-yt-embed.css with media="print" onload pattern
- Add terser JS minification in eleventy.after build hook
- Increase touch target sizing for category pills, facepile avatars, nav items
- Fix text-surface-400 contrast ratio (3.05:1 → 6.23:1) across 20 instances

Confab-Link: http://localhost:8080/sessions/edb1b7b0-da66-4486-bd9c-d1cfa7553b88
2026-03-07 20:13:45 +01:00
Ricardo
e236b4bf65 a11y: comprehensive WCAG 2.1 Level AA accessibility audit
- 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
2026-03-07 18:58:08 +01:00
Ricardo
95a532b8c1 fix: add explicit width/height attributes to sparkline SVG
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
2026-03-07 17:28:29 +01:00
Ricardo
9a9cd2d251 perf: enable lite-yt-embed, optimize avatar, add cache headers
- 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
2026-03-07 17:26:40 +01:00
Ricardo
8baec25b2c feat: blog filter nav, interactions pagination, note unfurl, pagefind improvements
- 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
2026-03-06 10:45:55 +01:00
Ricardo
14dcfba50a feat: fix sparkline calculation, redesign with domain colors, add post-graph
- Fix sparkline downward trend by extrapolating partial current month
- Redesign sparkline SVG with gradient fill and responsive sizing
- Apply domain-specific colors (amber/rose/emerald/purple) via currentColor
- Add eleventy-plugin-post-graph for GitHub-style contribution grids
- Homepage: posting activity graph in Tier 2 default layout
- AI page (/ai/): stats dashboard + AI-involved posts graph injected via layout
- New filters: aiPosts (filter by AI level), aiStats (total/count/percentage)

Confab-Link: http://localhost:8080/sessions/956f4251-b4a9-4bc9-b214-53402ad1fe63
2026-03-05 14:21:27 +01:00
Ricardo
91c0816303 feat: convert #hashtags in post content to category links
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
2026-03-05 13:46:40 +01:00
Ricardo
59fe05ae47 fix: use pinned property for featuredPosts collection
Renamed from "featured" to "pinned" to avoid conflict with the
"featured" hero image property in MF2/Micropub. Handles both boolean
and string "true" values from YAML frontmatter.

Confab-Link: http://localhost:8080/sessions/bd3f7012-c703-47e9-bfe2-2ad04ce1842d
2026-03-04 19:49:01 +01:00
Ricardo
8cd3c86bfa feat: add featured posts section for homepage builder
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
2026-03-04 19:26:18 +01:00
Ricardo
b811b43bc2 feat: syndication webhook on incremental builds
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
2026-03-04 17:33:18 +01:00
Ricardo
2ca3e047a4 feat: UI overhaul — accent color, Inter font, widget icons, diversified colors
- 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
2026-03-04 11:21:39 +01:00
Ricardo
99c9d7e73d fix: make remote image processing optional via PROCESS_REMOTE_IMAGES
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
2026-03-03 12:23:51 +01:00
Ricardo
d8d1dbfcec fix: prevent watcher OOM by tuning eleventy-img plugin
- 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
2026-03-03 12:06:46 +01:00
Ricardo
4fb7e2e92e fix: sync new OG images to output during incremental builds
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
2026-03-02 16:04:31 +01:00
Ricardo
a39b20375d fix: make starred page client-side rendered to avoid OOM
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
2026-03-02 13:40:33 +01:00
Ricardo
9d29f24d93 feat: add /github/starred/ page with Pagefind search and live updates
- 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
2026-03-02 13:15:12 +01:00
Ricardo
c3d64afa23 fix: add watcher debounce for rapid successive file changes
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
2026-03-01 21:27:54 +01:00
Ricardo
99ae0853ff fix: ignore interactive/ directory from Nunjucks processing
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.
2026-02-25 17:34:09 +01:00
Ricardo
cd4967f939 feat: add weeklyDigests collection for digest feature
- 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
2026-02-25 17:31:43 +01:00
Ricardo
abc0404816 feat: add fullwidth layout and interactive architecture page
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
2026-02-25 16:48:06 +01:00
Ricardo
de043020ac feat: add RSS per-category feed template, discovery links, and WebSub notifications
- Create category-feed.njk (RSS 2.0 pagination template)
- Add conditional <link rel="alternate"> tags for category pages in base.njk
- Extend WebSub hub notifications to include per-category feed URLs
2026-02-24 22:45:14 +01:00
Ricardo
22c151bb02 feat: add categoryFeeds collection for per-category RSS/JSON feeds 2026-02-24 22:39:49 +01:00
Ricardo
338bd3cc64 fix: correct alternate link URL for markdown-agents
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.
2026-02-24 20:53:04 +01:00
Ricardo
c1cd837845 fix: move markdown-agents generation from pagination template to eleventy.after hook
Eleventy 3.x's async internals crash when pagination templates access
collection item properties, triggering the frontMatter getter. Replace
the article-markdown.njk pagination template with post-build file
generation using gray-matter to read source files directly.
2026-02-24 20:46:23 +01:00
Ricardo
356d074700 fix: use rawMarkdownBody filter instead of template.frontMatter.content
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.
2026-02-24 20:32:46 +01:00
Ricardo
e56e2c67a5 fix: use Eleventy transform to resolve OG images from outputPath
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
2026-02-24 20:31:24 +01:00
Ricardo
a7bc472e87 fix: deduplicate cross-source webmentions
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.
2026-02-23 10:06:19 +01:00
Ricardo
e1f4b2cd90 feat: eliminate URL dualism — computed permalinks, conversations support
- 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
2026-02-23 09:38:30 +01:00