---
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,
})),
}),
];
---
{t('lnurlMints.lede')}
{t('lnurlMints.showing', { total: mints.length, online, offline })}
{t('lnurlMints.noResults')}
{mints.length === 0 &&{t('lnurlMints.emptyIndex')}
}