Hydrate the mint lists from the live API after paint.

/mints was a snapshot of whatever the API held when `astro build` ran, and stayed
that until the next build: a mint indexed at noon was reviewable at once — the 404
resolver saw to that — and simply had no card until 03:30. Every card's rating,
review count and status were as stale as the page.

The three index pages and the home page's three top-six strips now refetch
`GET /api/mints?type=…` once, after paint, and rebuild their grids. The
prerendered cards stay: they are the first paint, what a crawler indexes, and the
whole page without JavaScript. Hydration only ever replaces them with something
newer, and never with nothing — neither a failed fetch nor a well-formed empty
array touches a grid that has cards in it.

To make that affordable, the list payload grew the facts a chip is drawn from:
`nuts`, `capabilities`, and the two probed LNURL fields. /mints and /lnurl-mints
were fetching `GET /api/mints/:host` once per mint at build time to read two
booleans off each; that N+1 is gone from both, which takes the build from
fifty-six requests to one and is what makes the same read possible in a browser.
Additive: `MintDetail` already had all four.

web/src/lib/mint-cards.ts is MintCard.astro's parallel renderer, the same
relationship review-cards.ts has with the reviews panel. Same classes, same
data-* attributes — the sort, the search, the rank chips and the shared-element
view transitions all read the DOM — and the same i18n, through the page's own
inlined catalog rather than a build-time one.

Base.astro gained `clientNamespaces`, so the home page can inline the `home.`
catalog its strips need to rewrite "All 60 mints →" without putting 2KB of
marketing copy on 1,300 mint pages. check-i18n reads the prop off the page, so
the two cannot disagree.

Verified in Chromium against the built site: 60 prerendered cards become 61
including a mint inserted after the build; sort, search and hide-offline operate
on the new cards; /es/mints renders "En línea", "54 reseñas", "4,9" and "Solo
fundir"; JavaScript disabled still shows all 60; an aborted or empty API leaves
the grid alone; and a navigation away and back re-hydrates. 2016 pages build,
link and hreflang checks pass, 30 web tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
michilis
2026-08-25 16:27:33 +02:00
co-authored by Claude Opus 5
parent 060c7f1a59
commit 14548179a0
11 changed files with 670 additions and 60 deletions
+36
View File
@@ -1,5 +1,7 @@
/** Shapes returned by the API. The web app builds against these. */
import type { MintCapabilities } from './warnings.js';
/**
* Statuses a listed thing can be in.
*
@@ -35,6 +37,40 @@ export interface MintListItem {
score: number;
last_review_at: number | null;
version: string | null;
/* ---- card chips ----
*
* The three fields below exist so a card can be drawn from the list payload alone.
* Before them, /mints fetched `GET /api/mints/:host` once per mint at build time just
* to read two booleans off each one, which is fine for fifty-five mints on one build
* machine and is not fine for every visitor's browser once the list hydrates. They are
* facts, never rendered strings: the label a chip prints is decided by `mintChip` in
* the reader's own language, on whichever side is drawing the card.
*
* A federation has no counterpart and needs none — it publishes no capability list, so
* `mintChip` returns null for one and always will. See the Fedimint branch of
* `getMintWarnings`.
*/
/**
* NUT numbers this mint publishes, as strings: `["4", "5", "17"]`. Cashu only; empty
* for the other ecosystems and for a mint whose `/v1/info` has never been read.
*/
nuts: string[];
/**
* NUT-04 and NUT-05 switches, the Cashu chip's only input. null means nothing is
* cached for this mint, which is a different fact from "both are on" — see
* `readCapabilities`.
*/
capabilities: MintCapabilities | null;
/**
* LNURL: the advertised withdraw ceiling, millisatoshi. Optional rather than
* `| null`, so this stays exactly what `Partial<LnurlFields>` declares on `MintDetail`
* and the two do not have to be kept identical by hand.
*/
max_withdrawable_msat?: number | null;
/** LNURL: whether the last probe reached the mint's funding node. */
funding_available?: boolean | null;
}
export interface ProbeSample {