The contenus page loaded both lookup tables in full via HTMX on page load:
760 tag rows + 216 language rows, ~2.4MB of HTML including ~1960 inline
SVG icons and per-row CSRF forms. The DB queries were already fast (~6ms);
the cost was pure client-side payload and DOM.
- Add paged lookups: getTagsPage/getLanguagesPage + countTagsWithCount/
countLanguagesWithCount, and optional limit/offset on the existing
unpaged variants (backward compatible).
- Fragments serve 25 rows per request. A sentinel <tr> with
hx-trigger="intersect once root:#<wrap>" appends the next page when it
scrolls into the table's own scroll container (htmx 'revealed' only
checks the window viewport, not nested scrollers).
- Search forms still swap the whole wrapper and reset to offset 0.
- Style the load-more row; add PagedLanguagesTagsTest coverage.
Initial DOM for the page drops from ~2.4MB to ~160KB; scrolling reaches the
full totals (736 tags, 216 languages) and stops cleanly.
Replace every <img src="/assets/icons/..."> with <?= icon('name') ?>
across 26 template files. The PHP helper inlines the SVG markup into the
DOM so CSS color cascades naturally through fill="currentColor".
- Add src/icon.php helper: reads SVG file, sets width/height to 1em,
injects aria-hidden, supports optional CSS class
- Fix 12 icon SVGs that had hardcoded fill="#000000" or missing fill attr
- Replace search.svg with Phosphor fill-based magnifying glass
- Add explicit SVG sizes for admin header nav icons (16px/20px)
- Scope public search icon CSS to form[role=search]:not(.header-search-form)
to avoid breaking admin header layout; change stroke to fill
- Remove <img> filter: brightness(0) invert(1) hacks from admin.css