--- import Base from '../../layouts/Base.astro'; import MintCard from '../../components/MintCard.astro'; import ReviewByUrl from '../../components/ReviewByUrl.astro'; import type { LnurlDetail } from '@cashumints/shared'; import { fetchLnurlMint, fetchLnurlMints } from '../../lib/api'; import { localePath, useI18n, type Locale } from '../../i18n'; import { itemListNode } from '../../lib/schema'; import { isIndexableMint } from '../../lib/seo'; import { localePaths } from '../../i18n/paths'; /** * The LNURL mint index. /mints' twin, and built from the same parts for the same reason * /fedimints is: same container, same card, same search, same result line, same * leave-and-return animation. * * Unlike /fedimints it keeps the full sort set including "recently seen online". That * page drops it because a third of its rows have no `last_online` at all — nothing * checks those federations, so ordering on the column is a shuffle rather than a sort. * An LNURL mint is genuinely probed over HTTPS, exactly as a Cashu mint is, so every row * here has a real answer and the sort means what it says. */ /** One page per locale: `/lnurl-mints`, `/es/lnurl-mints`, `/nl/lnurl-mints`. */ export const getStaticPaths = localePaths; interface Props { locale: Locale; } const { locale } = Astro.props; // `t` formats the numbers inside its own strings, so no separate formatter is needed. const t = useI18n(locale); const mints = await fetchLnurlMints(); /* The list payload carries no LNURL fields, so the "no withdrawals" and "no mint / melt" chips need each mint's detail. One fetch per mint, at build time, against the API on the same machine — exactly what /mints does for its NUT switches. A mint whose detail cannot be read simply gets no chip. */ const details = await Promise.all( mints.map((mint) => fetchLnurlMint(mint.host).catch(() => null)), ); const chipSources = details.map((detail: LnurlDetail | null) => detail ? { max_withdrawable_msat: detail.max_withdrawable_msat ?? null, funding_available: detail.funding_available ?? null, } : null, ); const online = mints.filter((m) => m.status === 'online').length; const offline = mints.filter((m) => m.status === 'offline').length; const description = t('lnurlMints.description', { total: mints.length, online, offline }); /* * The index, as a list a crawler can read in order. * * Only the mints whose own pages are indexable: listing a page here while telling a * crawler not to index it is a contradiction. The order is the server-rendered one, * which is the score order; the sort control rewrites the DOM but not this markup, * so `ItemListOrderDescending` describes what shipped. */ const canonical = new URL(localePath('/lnurl-mints', locale), Astro.site).href; const schema = [ itemListNode({ canonical, fragment: 'lnurl-mints', name: t('lnurlMints.heading'), items: mints.filter(isIndexableMint).map((mint) => ({ name: mint.name ?? mint.host, url: new URL(localePath(`/lnurl-mint/${mint.host}`, locale), Astro.site).href, })), }), ]; --- {/* The same .page container the home page and the mint pages use, so the heading, the controls, the count line and the grid all share one left edge and one width. */}

{t('lnurlMints.heading')}

{t('lnurlMints.lede')}

{/* The way in for something this site has never seen. Every other route to writing a review starts from a page that already exists, which is exactly the case a reader with a brand new mint does not have. */}
{/* A pressed pill rather than a native checkbox: it is the same control the review filters use, and it is a real button, so it keeps the focus ring. */}

{t('lnurlMints.showing', { total: mints.length, online, offline })}

{mints.map((mint, i) => )}
{mints.length === 0 &&

{t('lnurlMints.emptyIndex')}

}
{/* Two steps in one dialog: identify the mint, then the ordinary review form. */}