Ship LNURL mint pages, indexing UI, and reviews rewrite.

Add lnurl list/detail routes, OG fixtures, i18n strings, and the write/
index client flows so the site surfaces the new mint type end to end.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
michilis
2026-08-22 03:44:43 +02:00
co-authored by Cursor
parent 2a9444942b
commit c74c7fc187
66 changed files with 7603 additions and 1241 deletions
+96 -38
View File
@@ -1,5 +1,23 @@
---
/**
* The site footer: one component, every page, three zones.
*
* Zone one is four columns — who this is, then the two link groups people actually
* navigate to, then the language control. Zone two is a thin bar with the copyright
* and the credit, and nothing else: the moment anything else moves into that row it
* stops being a signature and starts being a fourth navigation surface.
*
* What used to be here was four flat rows, the last of which was ~20 raw language
* links wrapped over two lines. That wall is now the same switcher the header carries
* (see LanguageSwitcher.astro, `placement="footer"`), so there is one language control
* on the site in two places rather than two controls that happen to agree.
*
* Everything below is static per locale and prerenders. No hex, no npub, no counts,
* nothing fetched: the footer is the one region of every page that must be identical
* on the home page and on a mint page that failed to load.
*/
import LanguageSwitcher from './LanguageSwitcher.astro';
import Moai from './Moai.astro';
import { localePath, useI18n } from '../i18n';
import { pageLocale } from '../i18n/paths';
@@ -9,23 +27,29 @@ const GITHUB = 'https://github.com/Azzamo-net/cashumints.space';
const { locale } = pageLocale(Astro);
const t = useI18n(locale);
/**
* Two rows: the pages people navigate to, then the ones they go looking for. The
* disclaimer sits in the second row here and again at the bottom of every mint page,
* which is where the decision it is about actually gets made.
/*
* Route slugs stay English in every language and `localePath` adds the prefix, exactly
* as the header does. See src/i18n/routing.ts for why that trade was made.
*/
const nav = [
{ path: '/mints', label: t('footer.allMints') },
{ path: '/fedimints', label: t('footer.allFedimints') },
{ path: '/reviews', label: t('footer.allReviews') },
{ path: '/wallets', label: t('footer.wallets') },
{ path: '/about', label: t('footer.about') },
const explore = [
{ href: localePath('/mints', locale), label: t('footer.allMints') },
{ href: localePath('/fedimints', locale), label: t('footer.allFedimints') },
{ href: localePath('/lnurl-mints', locale), label: t('footer.allLnurlMints') },
{ href: localePath('/reviews', locale), label: t('footer.allReviews') },
{ href: localePath('/wallets', locale), label: t('footer.wallets') },
];
const legal = [
{ path: '/disclaimer', label: t('footer.disclaimer') },
{ path: '/terms', label: t('footer.terms') },
{ path: '/privacy', label: t('footer.privacy') },
/*
* GitHub sits in this column rather than beside the credit line because it is a place
* to go, not a signature. It is the only external link in the columns, so it carries
* `rel="noopener"` and nothing else needs to.
*/
const site = [
{ href: localePath('/about', locale), label: t('footer.about'), external: false },
{ href: GITHUB, label: 'GitHub', external: true },
{ href: localePath('/terms', locale), label: t('footer.terms'), external: false },
{ href: localePath('/privacy', locale), label: t('footer.privacy'), external: false },
{ href: localePath('/disclaimer', locale), label: t('footer.disclaimer'), external: false },
];
/*
@@ -39,31 +63,65 @@ const madeBy = t('footer.madeBy', {
});
---
<footer>
<div class="foot-inner">
<span>{t('footer.copyright', { year: String(year) })}</span>
<div class="links">
<a href={GITHUB} rel="noopener">GitHub</a>
{nav.map((link) => <a href={localePath(link.path, locale)}>{link.label}</a>)}
<footer class="site-footer" role="contentinfo">
<div class="foot-cols">
{/*
The brand column. The custodial warning lives here, under the description,
because it is the most honest sentence on the site and it was previously an
orphan floating at the bottom right of the last row. It is set in `--muted`
rather than `--faint`: quiet, but a step louder than the link columns, which is
the only typographic claim this footer makes about relative importance.
*/}
<div class="foot-brand">
<a class="foot-mark" href={localePath('/', locale)}>
<Moai size={20} />
<span>Cashumints.space</span>
</a>
<p class="foot-desc">{t('footer.tagline')}</p>
<p class="foot-warn">{t('footer.warning')}</p>
</div>
{/*
One nav landmark over both link columns, not one per column: a screen reader
listing landmarks should find "Footer", not "Footer" twice. The language control
carries its own nav, because it is a different kind of thing — it does not take
you to another page, it takes you to this page again.
*/}
<nav class="foot-nav" aria-label={t('footer.nav')}>
<div class="foot-col">
<h2 class="foot-head">{t('footer.col.explore')}</h2>
<ul>
{explore.map((link) => (
<li><a href={link.href}>{link.label}</a></li>
))}
</ul>
</div>
<div class="foot-col">
<h2 class="foot-head">{t('footer.col.site')}</h2>
<ul>
{site.map((link) => (
<li>
<a href={link.href} rel={link.external ? 'noopener' : undefined}>{link.label}</a>
</li>
))}
</ul>
</div>
</nav>
<div class="foot-col foot-lang">
<h2 class="foot-head">{t('lang.label')}</h2>
<LanguageSwitcher placement="footer" />
</div>
<span class="foot-right" set:html={madeBy} />
</div>
<div class="foot-inner foot-legal">
<div class="links">
{legal.map((link) => <a href={localePath(link.path, locale)}>{link.label}</a>)}
</div>
<LanguageSwitcher variant="inline" />
<span class="foot-right foot-warn">{t('footer.warning')}</span>
{/*
The bottom bar. Two items, and the discipline is that it stays two items: the
copyright and who made it. Everything else that was ever tempted into this row now
has a column above it.
*/}
<div class="foot-bar">
<span>{t('footer.copyright', { year: String(year) })}</span>
<span class="foot-credit" set:html={madeBy} />
</div>
</footer>
<style>
/* The second row is quieter than the first and has no top border of its own: it
reads as a continuation of the footer, not a second footer. */
.foot-legal { padding-top: 0; border-top: none; font-size: 12.5px; }
.foot-warn { color: var(--faint); }
@media (max-width: 640px) {
.foot-legal { padding-top: 4px; }
}
</style>