Add thin-wrapper templates for work/personal filtering of CV sections:
- 8 new templates: cv-{experience,education,skills,interests}-{personal,work}.njk
- cv-languages.njk: standalone languages section (split from education)
- homepage-section.njk: 9 new routes for filtered variants
- cv.njk: uses work-only variants for the /cv/ page
- Base templates: filterType support in experience, education, skills, interests
- _data/cv.js: skillTypes and interestTypes fallback fields
Show rich link preview cards in bookmarks, likes, replies, reposts
collection pages and the homepage recent posts section. URLs are
fetched once and cached — the same cache serves all templates.
The default facebookexternalhit UA causes many sites to block or
redirect-loop, resulting in timeouts. Switch to a well-identified
bot UA that sites handle correctly.
- Increase timeout from 10s to 20s
- Cache failures for 1 day (avoids retrying every build)
- Add concurrency limiter (max 5 parallel requests)
- Refactor into renderCard/renderFallbackLink helpers
The target URL in likes, bookmarks, replies, and reposts now renders
as a rich OpenGraph card via the unfurl shortcode instead of a bare
link. The raw URL remains below the card for h-cite microformat
compatibility. Results are cached in .cache/unfurl/ for 1 week.
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).
- Add <noscript><style> in base.njk that unhides x-cloak/x-show content,
hides FAB and tab buttons when JS is disabled (content stacks instead)
- Add noscript message on search page with links to blog/categories
- Add noscript banner on interactions page explaining inbound tab needs JS
- Add ActivityPub/Fediverse platform badge (purple, network icon) to
interactions page alongside existing Mastodon and Bluesky badges
- Detect platform from Bridgy source URLs and author URLs for
webmention.io items that lack a platform field
- Filter self-referencing syndication URLs from "Also on" footer so
self-hosted AP posts don't show a redundant link back to the site
AI crawlers (GPTBot) were following edit/create links in the static
HTML, flooding the server with /posts/edit and /session/login requests.
Adding nofollow tells well-behaved crawlers to skip these admin-only links.
Fetch from both /webmentions/api/mentions and /conversations/api/mentions,
merge results with conversations items taking priority (richer metadata),
and display platform badges (Mastodon/Bluesky icons) on interaction cards.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.
Eleventy 3.x renders Nunjucks templates in parallel, causing page.url
to return wrong values in {% set %} tags. This caused OG images to be
mismatched between pages (e.g., bookmark showed note's OG image).
Move ogSlug and hasOgImage computation to eleventyComputed, which runs
during the sequential data cascade phase before parallel rendering.
The computed values are then available as plain template variables.
Refs: https://github.com/11ty/eleventy/issues/3183
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 webmentions sidebar widget uses Alpine.js + fetch API but was
missing the is-land on:visible wrapper that all other JS-dependent
sidebar widgets have. This defers its initialization until scrolled
into view, consistent with social-activity, github-repos, funkwhale,
blogroll, and feedland widgets.
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.
Each collection page (articles, notes, photos, bookmarks, likes,
replies, reposts) now shows its own sparkline next to the heading,
showing that specific post type's frequency over the last 12 months.
- 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>
Uses Satori + @resvg/resvg-js to create branded 1200x630 social
preview cards at build time. Cards show post title, type badge,
date, and site name on a dark background with blue accent.
Generated images are cached in .cache/og/ (persistent on Cloudron)
and passthrough-copied to the output. Posts with photos continue
using their own images. Untitled posts (notes) use body text.
Uses markdown-it-anchor to generate linkable IDs on h2-h4 headings.
Headings become clickable links with a subtle # indicator on hover.
Includes scroll-margin-top so anchored headings don't hide behind
the sticky header.
Add row selection and expand/collapse behavior matching Dave Winer's
blogroll.js: first click selects, second click (or caret click) expands
to show up to 5 recent items fetched from blogroll API. Items cached
after first fetch. Added dark mode styles for expanded items.
Replicates Dave Winer's blogroll.js visual rendering (240px bordered
container, Ubuntu/Rancho fonts, Title/When sort, caret wedges, truncated
titles, relative timestamps, "Powered by FeedLand" footer) using Alpine.js
and the blogroll API instead of jQuery + external scripts.
Registered in all three sidebar types (homepage, blog listing, blog post)
and in the fallback sidebar.
Sidebar widget now fetches blogs sorted by recently updated and groups
them into tabs by source type (Microsub/FeedLand). Tabs only appear
when multiple source types exist. Each tab shows up to 8 blogs.
When viewing a post that has mpUrl in its frontmatter, the FAB menu
shows an "Edit this post" link at the top that redirects to the
Indiekit admin edit form via /posts/edit?url=<mpUrl>.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- cv-projects-personal.njk: filters projects with projectType=personal (or unset)
- cv-projects-work.njk: filters projects with projectType=work
- CV page now only shows work projects
- Homepage section dispatcher routes both new types
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The defer + DOMContentLoaded queue approach failed silently when
pagefind-ui.js couldn't load. Moving the script to end of body
ensures all DOM elements exist and processes the queue immediately
after the script loads.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>