Dev #1
@@ -38,6 +38,11 @@ PUBLIC_API_URL=
|
|||||||
# Canonical origin for canonical links, OpenGraph tags and the sitemap.
|
# Canonical origin for canonical links, OpenGraph tags and the sitemap.
|
||||||
SITE_URL=https://cashumints.space
|
SITE_URL=https://cashumints.space
|
||||||
|
|
||||||
|
# Whether a rated mint's JSON-LD also carries the review-snippet-eligible Product
|
||||||
|
# type next to Service (web/src/lib/schema.ts explains the trade). On by default;
|
||||||
|
# set to 0 to ship the plain Service node on the next build, no code change needed.
|
||||||
|
SEO_PRODUCT_JSONLD=1
|
||||||
|
|
||||||
# ─── API storage ─────────────────────────────────────────────────────────────
|
# ─── API storage ─────────────────────────────────────────────────────────────
|
||||||
# The API serves everything from its own database and nothing from a mint or a relay at
|
# The API serves everything from its own database and nothing from a mint or a relay at
|
||||||
# request time, so a restart loses nothing: mint metadata, cached /v1/info payloads,
|
# request time, so a restart loses nothing: mint metadata, cached /v1/info payloads,
|
||||||
|
|||||||
@@ -282,6 +282,32 @@ OpenGraph and Twitter tags, a social card, a self-referencing canonical, a full
|
|||||||
set and a JSON-LD graph. `sitemap.xml` lists every indexable one with its `xhtml:link`
|
set and a JSON-LD graph. `sitemap.xml` lists every indexable one with its `xhtml:link`
|
||||||
alternates, and `robots.txt` sits beside it.
|
alternates, and `robots.txt` sits beside it.
|
||||||
|
|
||||||
|
### Social cards
|
||||||
|
|
||||||
|
`pnpm build` starts with `pnpm og` (`web/scripts/og/build-og.mjs`), which renders one
|
||||||
|
1200x630 PNG per mint and federation into `web/public/og/` from the same API data the
|
||||||
|
pages use — satori lays the card out and turns the text into glyph paths (the six
|
||||||
|
static TTFs under `web/scripts/og/fonts/` are the same faces the site uses),
|
||||||
|
`@resvg/resvg-js` rasterises it. No browser involved; a full run of ~70 cards takes a
|
||||||
|
few seconds, and a rerun with unchanged data renders nothing: each card's inputs are
|
||||||
|
hashed into its filename (`mint.example.com.a1b2c3d4e5.png`) and
|
||||||
|
`web/src/generated/og-manifest.json` records what is already on disk. The hashed name
|
||||||
|
is also the cache-busting story — link-preview scrapers cache an `og:image` by URL, so
|
||||||
|
a mint whose rating moved or that went offline gets a new URL on the next scheduled
|
||||||
|
rebuild, while the stable-named `default.png` (brand plus network stats, used by every
|
||||||
|
non-mint page) is served with a one-hour cache instead (see the nginx block below).
|
||||||
|
|
||||||
|
The images are one English render shared by every locale; titles, descriptions and alt
|
||||||
|
text translate per page. Relative times stay out of the PNGs on purpose — a "3d ago"
|
||||||
|
would go stale inside a static file — so the cards carry absolute month/year stamps,
|
||||||
|
and the one exception, the "Offline {n}d" chip, is derived from a day-granular clock so
|
||||||
|
it regenerates at most once a day.
|
||||||
|
|
||||||
|
`pnpm og:fixtures` renders the six edge cases in `web/scripts/og/fixtures.mjs`
|
||||||
|
(30-character name, no icon, zero reviews, offline, melt only, announced-only
|
||||||
|
federation) into `web/og-fixtures/` at a pinned timestamp; those snapshots are
|
||||||
|
committed, so eyeball them after any template change.
|
||||||
|
|
||||||
### What is indexed, and what is not
|
### What is indexed, and what is not
|
||||||
|
|
||||||
One rule, in `web/src/lib/seo.ts`, decides it, and it covers both ecosystems: a page is
|
One rule, in `web/src/lib/seo.ts`, decides it, and it covers both ecosystems: a page is
|
||||||
@@ -739,14 +765,27 @@ server {
|
|||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
}
|
}
|
||||||
|
|
||||||
# The social card, the icons and the manifest. Not fingerprinted, since their names
|
# The favicons and the manifest. Not fingerprinted, since their names are referenced
|
||||||
# are referenced from outside the site, so a week with revalidation rather than a
|
# from outside the site, so a week with revalidation rather than a year of immutability.
|
||||||
# year of immutability.
|
|
||||||
location ~* ^/(og\.png|favicon\.(ico|svg)|apple-touch-icon\.png|icon-\d+\.png|site\.webmanifest)$ {
|
location ~* ^/(og\.png|favicon\.(ico|svg)|apple-touch-icon\.png|icon-\d+\.png|site\.webmanifest)$ {
|
||||||
expires 7d;
|
expires 7d;
|
||||||
add_header Cache-Control "public";
|
add_header Cache-Control "public";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The generated social cards (`pnpm og`). The default card keeps a stable name and
|
||||||
|
# changes with the site's stats, so it gets an hour; every per-mint card carries a
|
||||||
|
# content hash in its filename and a stale hash is never referenced again, so those
|
||||||
|
# are immutable. (The hash is also what actually refreshes link previews: the big
|
||||||
|
# scrapers cache an og:image by URL and ignore these headers.)
|
||||||
|
location = /og/default.png {
|
||||||
|
add_header Cache-Control "public, max-age=3600";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /og/ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
# Same-origin API and icons, matching the default empty PUBLIC_API_URL. Drop these two
|
# Same-origin API and icons, matching the default empty PUBLIC_API_URL. Drop these two
|
||||||
# blocks only if you build with PUBLIC_API_URL pointing at a separate API host.
|
# blocks only if you build with PUBLIC_API_URL pointing at a separate API host.
|
||||||
location /api/ {
|
location /api/ {
|
||||||
|
|||||||
+1
-12
@@ -114,12 +114,6 @@
|
|||||||
"card.reviewed": "reviewed {when}",
|
"card.reviewed": "reviewed {when}",
|
||||||
"card.noReviews": "no reviews yet",
|
"card.noReviews": "no reviews yet",
|
||||||
"mint.title": "{name} · Cashu mint reviews",
|
"mint.title": "{name} · Cashu mint reviews",
|
||||||
"mint.description.fallback": "{name} is a Cashu mint at {domain}.",
|
|
||||||
"mint.description.rated.one": "Rated {rating} out of 5 from {n} review signed on Nostr.",
|
|
||||||
"mint.description.rated.other": "Rated {rating} out of 5 from {n} reviews signed on Nostr.",
|
|
||||||
"mint.description.noReviews": "No community reviews yet.",
|
|
||||||
"mint.description.online": "Currently online.",
|
|
||||||
"mint.description.unreachable": "Currently unreachable.",
|
|
||||||
"mint.breadcrumb": "Breadcrumb",
|
"mint.breadcrumb": "Breadcrumb",
|
||||||
"mint.crumbMints": "Cashu mints",
|
"mint.crumbMints": "Cashu mints",
|
||||||
"mint.copyUrl": "Copy mint URL",
|
"mint.copyUrl": "Copy mint URL",
|
||||||
@@ -546,6 +540,7 @@
|
|||||||
"seo.mint.titleNoReviews": "{name}: Cashu mint info and reviews | Cashumints.space",
|
"seo.mint.titleNoReviews": "{name}: Cashu mint info and reviews | Cashumints.space",
|
||||||
"seo.fedimint.titleNoReviews": "{name}: Fedimint federation info and reviews | Cashumints.space",
|
"seo.fedimint.titleNoReviews": "{name}: Fedimint federation info and reviews | Cashumints.space",
|
||||||
"seo.desc.fallback": "Community reviews for {name} ({domain}).",
|
"seo.desc.fallback": "Community reviews for {name} ({domain}).",
|
||||||
|
"seo.desc.fallbackDomain": "Community reviews for the Cashu mint at {domain}.",
|
||||||
"seo.desc.rated.one": "Rated {rating}/5 from {n} signed Nostr review.",
|
"seo.desc.rated.one": "Rated {rating}/5 from {n} signed Nostr review.",
|
||||||
"seo.desc.rated.other": "Rated {rating}/5 from {n} signed Nostr reviews.",
|
"seo.desc.rated.other": "Rated {rating}/5 from {n} signed Nostr reviews.",
|
||||||
"seo.desc.noReviews": "No reviews yet.",
|
"seo.desc.noReviews": "No reviews yet.",
|
||||||
@@ -557,7 +552,6 @@
|
|||||||
"seo.mint.ogAlt.other": "{name}: {rating} stars from {n} reviews on Cashumints.space",
|
"seo.mint.ogAlt.other": "{name}: {rating} stars from {n} reviews on Cashumints.space",
|
||||||
"seo.mint.ogAltNoReviews": "{name} on Cashumints.space",
|
"seo.mint.ogAltNoReviews": "{name} on Cashumints.space",
|
||||||
"nav.home": "Home",
|
"nav.home": "Home",
|
||||||
"mint.description.fallbackDomain": "A Cashu mint running at {domain}.",
|
|
||||||
"mint.more.title": "More mints",
|
"mint.more.title": "More mints",
|
||||||
"mint.more.all": "See all mints",
|
"mint.more.all": "See all mints",
|
||||||
"nav.fedimints": "Fedimints",
|
"nav.fedimints": "Fedimints",
|
||||||
@@ -578,7 +572,6 @@
|
|||||||
"fedimints.announcedNote.other": "{n} of these were announced on Nostr and nothing has confirmed since that they are running. They are listed as announced rather than as online or offline, because neither would be true.",
|
"fedimints.announcedNote.other": "{n} of these were announced on Nostr and nothing has confirmed since that they are running. They are listed as announced rather than as online or offline, because neither would be true.",
|
||||||
"fedimints.noResults": "No federations match. Try a name, or paste a federation id.",
|
"fedimints.noResults": "No federations match. Try a name, or paste a federation id.",
|
||||||
"fedimints.emptyIndex": "No federations have been announced on the relays this site reads. The list fills itself as announcements arrive.",
|
"fedimints.emptyIndex": "No federations have been announced on the relays this site reads. The list fills itself as announcements arrive.",
|
||||||
"fedimint.title": "{name} · Fedimint federation reviews",
|
|
||||||
"fedimint.crumb": "Fedimints",
|
"fedimint.crumb": "Fedimints",
|
||||||
"fedimint.breadcrumb": "Breadcrumb",
|
"fedimint.breadcrumb": "Breadcrumb",
|
||||||
"fedimint.unnamed": "Unnamed federation",
|
"fedimint.unnamed": "Unnamed federation",
|
||||||
@@ -611,10 +604,6 @@
|
|||||||
"fedimint.more.title": "More federations",
|
"fedimint.more.title": "More federations",
|
||||||
"fedimint.more.all": "See all federations",
|
"fedimint.more.all": "See all federations",
|
||||||
"fedimint.disclaimer": "Joining a federation means trusting its guardians with your money. Ratings are community opinions, and confirmed running means reachable, not trustworthy.",
|
"fedimint.disclaimer": "Joining a federation means trusting its guardians with your money. Ratings are community opinions, and confirmed running means reachable, not trustworthy.",
|
||||||
"fedimint.description.online": "A check confirmed it running.",
|
|
||||||
"fedimint.description.announced": "No check has confirmed it running.",
|
|
||||||
"fedimint.description.fallback": "{name} is a Fedimint federation announced on Nostr.",
|
|
||||||
"fedimint.description.fallbackId": "A Fedimint federation announced on Nostr as {id}.",
|
|
||||||
"fedimint.module.lightning": "Lightning",
|
"fedimint.module.lightning": "Lightning",
|
||||||
"fedimint.module.mint": "Ecash mint",
|
"fedimint.module.mint": "Ecash mint",
|
||||||
"fedimint.module.wallet": "On-chain wallet",
|
"fedimint.module.wallet": "On-chain wallet",
|
||||||
|
|||||||
+1
-12
@@ -118,12 +118,6 @@
|
|||||||
"card.reviewed": "reseñado {when}",
|
"card.reviewed": "reseñado {when}",
|
||||||
"card.noReviews": "sin reseñas aún",
|
"card.noReviews": "sin reseñas aún",
|
||||||
"mint.title": "{name} · Reseñas del mint Cashu",
|
"mint.title": "{name} · Reseñas del mint Cashu",
|
||||||
"mint.description.fallback": "{name} es un mint Cashu en {domain}.",
|
|
||||||
"mint.description.rated.one": "Valorado con {rating} sobre 5 a partir de {n} reseña firmada en Nostr.",
|
|
||||||
"mint.description.rated.other": "Valorado con {rating} sobre 5 a partir de {n} reseñas firmadas en Nostr.",
|
|
||||||
"mint.description.noReviews": "Todavía no hay reseñas de la comunidad.",
|
|
||||||
"mint.description.online": "Ahora mismo en línea.",
|
|
||||||
"mint.description.unreachable": "Ahora mismo no responde.",
|
|
||||||
"mint.breadcrumb": "Ruta de navegación",
|
"mint.breadcrumb": "Ruta de navegación",
|
||||||
"mint.crumbMints": "Mints Cashu",
|
"mint.crumbMints": "Mints Cashu",
|
||||||
"mint.copyUrl": "Copiar la URL del mint",
|
"mint.copyUrl": "Copiar la URL del mint",
|
||||||
@@ -550,6 +544,7 @@
|
|||||||
"seo.mint.titleNoReviews": "{name}: información y reseñas del mint Cashu | Cashumints.space",
|
"seo.mint.titleNoReviews": "{name}: información y reseñas del mint Cashu | Cashumints.space",
|
||||||
"seo.fedimint.titleNoReviews": "{name}: información y reseñas de la federación Fedimint | Cashumints.space",
|
"seo.fedimint.titleNoReviews": "{name}: información y reseñas de la federación Fedimint | Cashumints.space",
|
||||||
"seo.desc.fallback": "Reseñas de la comunidad sobre {name} ({domain}).",
|
"seo.desc.fallback": "Reseñas de la comunidad sobre {name} ({domain}).",
|
||||||
|
"seo.desc.fallbackDomain": "Reseñas de la comunidad sobre el mint Cashu en {domain}.",
|
||||||
"seo.desc.rated.one": "Valorado {rating}/5 en {n} reseña firmada en Nostr.",
|
"seo.desc.rated.one": "Valorado {rating}/5 en {n} reseña firmada en Nostr.",
|
||||||
"seo.desc.rated.other": "Valorado {rating}/5 en {n} reseñas firmadas en Nostr.",
|
"seo.desc.rated.other": "Valorado {rating}/5 en {n} reseñas firmadas en Nostr.",
|
||||||
"seo.desc.noReviews": "Aún sin reseñas.",
|
"seo.desc.noReviews": "Aún sin reseñas.",
|
||||||
@@ -561,7 +556,6 @@
|
|||||||
"seo.mint.ogAlt.other": "{name}: {rating} estrellas de {n} reseñas en Cashumints.space",
|
"seo.mint.ogAlt.other": "{name}: {rating} estrellas de {n} reseñas en Cashumints.space",
|
||||||
"seo.mint.ogAltNoReviews": "{name} en Cashumints.space",
|
"seo.mint.ogAltNoReviews": "{name} en Cashumints.space",
|
||||||
"nav.home": "Inicio",
|
"nav.home": "Inicio",
|
||||||
"mint.description.fallbackDomain": "Un mint Cashu que funciona en {domain}.",
|
|
||||||
"mint.more.title": "Más mints",
|
"mint.more.title": "Más mints",
|
||||||
"mint.more.all": "Ver todos los mints",
|
"mint.more.all": "Ver todos los mints",
|
||||||
"nav.fedimints": "Fedimints",
|
"nav.fedimints": "Fedimints",
|
||||||
@@ -582,7 +576,6 @@
|
|||||||
"fedimints.announcedNote.other": "{n} de estas se anunciaron en Nostr y nada ha confirmado desde entonces que estén funcionando. Aparecen como anunciadas, no como en línea ni sin conexión, porque ninguna de las dos sería cierta.",
|
"fedimints.announcedNote.other": "{n} de estas se anunciaron en Nostr y nada ha confirmado desde entonces que estén funcionando. Aparecen como anunciadas, no como en línea ni sin conexión, porque ninguna de las dos sería cierta.",
|
||||||
"fedimints.noResults": "Ninguna federación coincide. Prueba con un nombre, o pega un id de federación.",
|
"fedimints.noResults": "Ninguna federación coincide. Prueba con un nombre, o pega un id de federación.",
|
||||||
"fedimints.emptyIndex": "No se ha anunciado ninguna federación en los relays que lee este sitio. La lista se llena sola según van llegando anuncios.",
|
"fedimints.emptyIndex": "No se ha anunciado ninguna federación en los relays que lee este sitio. La lista se llena sola según van llegando anuncios.",
|
||||||
"fedimint.title": "{name} · Reseñas de la federación Fedimint",
|
|
||||||
"fedimint.crumb": "Fedimints",
|
"fedimint.crumb": "Fedimints",
|
||||||
"fedimint.breadcrumb": "Ruta de navegación",
|
"fedimint.breadcrumb": "Ruta de navegación",
|
||||||
"fedimint.unnamed": "Federación sin nombre",
|
"fedimint.unnamed": "Federación sin nombre",
|
||||||
@@ -615,10 +608,6 @@
|
|||||||
"fedimint.more.title": "Más federaciones",
|
"fedimint.more.title": "Más federaciones",
|
||||||
"fedimint.more.all": "Ver todas las federaciones",
|
"fedimint.more.all": "Ver todas las federaciones",
|
||||||
"fedimint.disclaimer": "Unirte a una federación significa confiar tu dinero a sus guardianes. Las valoraciones son opiniones de la comunidad, y confirmada en funcionamiento significa accesible, no fiable.",
|
"fedimint.disclaimer": "Unirte a una federación significa confiar tu dinero a sus guardianes. Las valoraciones son opiniones de la comunidad, y confirmada en funcionamiento significa accesible, no fiable.",
|
||||||
"fedimint.description.online": "Una comprobación confirmó que está funcionando.",
|
|
||||||
"fedimint.description.announced": "Ninguna comprobación ha confirmado que esté funcionando.",
|
|
||||||
"fedimint.description.fallback": "{name} es una federación Fedimint anunciada en Nostr.",
|
|
||||||
"fedimint.description.fallbackId": "Una federación Fedimint anunciada en Nostr como {id}.",
|
|
||||||
"fedimint.module.lightning": "Lightning",
|
"fedimint.module.lightning": "Lightning",
|
||||||
"fedimint.module.mint": "Mint de ecash",
|
"fedimint.module.mint": "Mint de ecash",
|
||||||
"fedimint.module.wallet": "Monedero on-chain",
|
"fedimint.module.wallet": "Monedero on-chain",
|
||||||
|
|||||||
+1
-12
@@ -114,12 +114,6 @@
|
|||||||
"card.reviewed": "laatste review {when}",
|
"card.reviewed": "laatste review {when}",
|
||||||
"card.noReviews": "nog geen reviews",
|
"card.noReviews": "nog geen reviews",
|
||||||
"mint.title": "{name} · Reviews van deze Cashu mint",
|
"mint.title": "{name} · Reviews van deze Cashu mint",
|
||||||
"mint.description.fallback": "{name} is een Cashu mint op {domain}.",
|
|
||||||
"mint.description.rated.one": "Beoordeeld met {rating} van de 5 op basis van {n} review, ondertekend op Nostr.",
|
|
||||||
"mint.description.rated.other": "Beoordeeld met {rating} van de 5 op basis van {n} reviews, ondertekend op Nostr.",
|
|
||||||
"mint.description.noReviews": "Nog geen reviews uit de community.",
|
|
||||||
"mint.description.online": "Nu online.",
|
|
||||||
"mint.description.unreachable": "Nu onbereikbaar.",
|
|
||||||
"mint.breadcrumb": "Kruimelpad",
|
"mint.breadcrumb": "Kruimelpad",
|
||||||
"mint.crumbMints": "Cashu mints",
|
"mint.crumbMints": "Cashu mints",
|
||||||
"mint.copyUrl": "Mint-URL kopiëren",
|
"mint.copyUrl": "Mint-URL kopiëren",
|
||||||
@@ -546,6 +540,7 @@
|
|||||||
"seo.mint.titleNoReviews": "{name}: info en reviews over deze Cashu-mint | Cashumints.space",
|
"seo.mint.titleNoReviews": "{name}: info en reviews over deze Cashu-mint | Cashumints.space",
|
||||||
"seo.fedimint.titleNoReviews": "{name}: info en reviews over deze Fedimint-federatie | Cashumints.space",
|
"seo.fedimint.titleNoReviews": "{name}: info en reviews over deze Fedimint-federatie | Cashumints.space",
|
||||||
"seo.desc.fallback": "Reviews uit de gemeenschap over {name} ({domain}).",
|
"seo.desc.fallback": "Reviews uit de gemeenschap over {name} ({domain}).",
|
||||||
|
"seo.desc.fallbackDomain": "Reviews uit de gemeenschap over de Cashu-mint op {domain}.",
|
||||||
"seo.desc.rated.one": "Beoordeeld met {rating}/5 uit {n} op Nostr ondertekende review.",
|
"seo.desc.rated.one": "Beoordeeld met {rating}/5 uit {n} op Nostr ondertekende review.",
|
||||||
"seo.desc.rated.other": "Beoordeeld met {rating}/5 uit {n} op Nostr ondertekende reviews.",
|
"seo.desc.rated.other": "Beoordeeld met {rating}/5 uit {n} op Nostr ondertekende reviews.",
|
||||||
"seo.desc.noReviews": "Nog geen reviews.",
|
"seo.desc.noReviews": "Nog geen reviews.",
|
||||||
@@ -557,7 +552,6 @@
|
|||||||
"seo.mint.ogAlt.other": "{name}: {rating} sterren uit {n} reviews op Cashumints.space",
|
"seo.mint.ogAlt.other": "{name}: {rating} sterren uit {n} reviews op Cashumints.space",
|
||||||
"seo.mint.ogAltNoReviews": "{name} op Cashumints.space",
|
"seo.mint.ogAltNoReviews": "{name} op Cashumints.space",
|
||||||
"nav.home": "Home",
|
"nav.home": "Home",
|
||||||
"mint.description.fallbackDomain": "Een Cashu-mint die draait op {domain}.",
|
|
||||||
"mint.more.title": "Meer mints",
|
"mint.more.title": "Meer mints",
|
||||||
"mint.more.all": "Bekijk alle mints",
|
"mint.more.all": "Bekijk alle mints",
|
||||||
"nav.fedimints": "Fedimints",
|
"nav.fedimints": "Fedimints",
|
||||||
@@ -578,7 +572,6 @@
|
|||||||
"fedimints.announcedNote.other": "{n} hiervan zijn op Nostr aangekondigd en niets heeft sindsdien bevestigd dat ze draaien. Ze staan er als aangekondigd, niet als online of offline, want geen van beide zou kloppen.",
|
"fedimints.announcedNote.other": "{n} hiervan zijn op Nostr aangekondigd en niets heeft sindsdien bevestigd dat ze draaien. Ze staan er als aangekondigd, niet als online of offline, want geen van beide zou kloppen.",
|
||||||
"fedimints.noResults": "Geen enkele federatie komt overeen. Probeer een naam, of plak een federatie-id.",
|
"fedimints.noResults": "Geen enkele federatie komt overeen. Probeer een naam, of plak een federatie-id.",
|
||||||
"fedimints.emptyIndex": "Er is geen enkele federatie aangekondigd op de relays die deze site leest. De lijst vult zichzelf zodra er aankondigingen binnenkomen.",
|
"fedimints.emptyIndex": "Er is geen enkele federatie aangekondigd op de relays die deze site leest. De lijst vult zichzelf zodra er aankondigingen binnenkomen.",
|
||||||
"fedimint.title": "{name} · Reviews van deze Fedimint-federatie",
|
|
||||||
"fedimint.crumb": "Fedimints",
|
"fedimint.crumb": "Fedimints",
|
||||||
"fedimint.breadcrumb": "Kruimelpad",
|
"fedimint.breadcrumb": "Kruimelpad",
|
||||||
"fedimint.unnamed": "Naamloze federatie",
|
"fedimint.unnamed": "Naamloze federatie",
|
||||||
@@ -611,10 +604,6 @@
|
|||||||
"fedimint.more.title": "Meer federaties",
|
"fedimint.more.title": "Meer federaties",
|
||||||
"fedimint.more.all": "Alle federaties bekijken",
|
"fedimint.more.all": "Alle federaties bekijken",
|
||||||
"fedimint.disclaimer": "Meedoen met een federatie betekent dat je jouw geld toevertrouwt aan haar guardians. Beoordelingen zijn meningen uit de community, en bevestigd draaiend betekent bereikbaar, niet betrouwbaar.",
|
"fedimint.disclaimer": "Meedoen met een federatie betekent dat je jouw geld toevertrouwt aan haar guardians. Beoordelingen zijn meningen uit de community, en bevestigd draaiend betekent bereikbaar, niet betrouwbaar.",
|
||||||
"fedimint.description.online": "Een controle heeft bevestigd dat hij draait.",
|
|
||||||
"fedimint.description.announced": "Geen enkele controle heeft bevestigd dat hij draait.",
|
|
||||||
"fedimint.description.fallback": "{name} is een Fedimint-federatie die op Nostr is aangekondigd.",
|
|
||||||
"fedimint.description.fallbackId": "Een Fedimint-federatie die op Nostr is aangekondigd als {id}.",
|
|
||||||
"fedimint.module.lightning": "Lightning",
|
"fedimint.module.lightning": "Lightning",
|
||||||
"fedimint.module.mint": "Ecash mint",
|
"fedimint.module.mint": "Ecash mint",
|
||||||
"fedimint.module.wallet": "On-chain wallet",
|
"fedimint.module.wallet": "On-chain wallet",
|
||||||
|
|||||||
@@ -57,14 +57,20 @@ interface Props {
|
|||||||
schema?: SchemaNode[];
|
schema?: SchemaNode[];
|
||||||
/**
|
/**
|
||||||
* Social card. A site-relative path, resolved to the absolute URL `og:image` requires.
|
* Social card. A site-relative path, resolved to the absolute URL `og:image` requires.
|
||||||
* Defaults to the shared 1200x630 card.
|
* Defaults to the shared 1200x630 card that `pnpm og` generates.
|
||||||
*/
|
*/
|
||||||
image?: string;
|
image?: string;
|
||||||
|
/**
|
||||||
|
* Alt text for the card. Mint pages pass their own ("{name}: 4.9 stars from 54
|
||||||
|
* reviews…"); everything else gets the sitewide line, matching the default card.
|
||||||
|
*/
|
||||||
|
imageAlt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
title, description, current, mintCount, ogType = 'website',
|
title, description, current, mintCount, ogType = 'website',
|
||||||
noindex = false, offGraph = false, schema = [], image = OG_IMAGE,
|
noindex = false, offGraph = false, schema = [], image = OG_IMAGE,
|
||||||
|
imageAlt,
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -217,7 +223,7 @@ const noscriptMotion =
|
|||||||
<meta property="og:image" content={imageUrl} />
|
<meta property="og:image" content={imageUrl} />
|
||||||
<meta property="og:image:width" content={String(OG_IMAGE_WIDTH)} />
|
<meta property="og:image:width" content={String(OG_IMAGE_WIDTH)} />
|
||||||
<meta property="og:image:height" content={String(OG_IMAGE_HEIGHT)} />
|
<meta property="og:image:height" content={String(OG_IMAGE_HEIGHT)} />
|
||||||
<meta property="og:image:alt" content={t('seo.ogImageAlt')} />
|
<meta property="og:image:alt" content={imageAlt ?? t('seo.ogImageAlt')} />
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
`summary_large_image`, now that there is an image worth the space. With no
|
`summary_large_image`, now that there is an image worth the space. With no
|
||||||
@@ -227,7 +233,7 @@ const noscriptMotion =
|
|||||||
<meta name="twitter:title" content={title} />
|
<meta name="twitter:title" content={title} />
|
||||||
<meta name="twitter:description" content={description} />
|
<meta name="twitter:description" content={description} />
|
||||||
<meta name="twitter:image" content={imageUrl} />
|
<meta name="twitter:image" content={imageUrl} />
|
||||||
<meta name="twitter:image:alt" content={t('seo.ogImageAlt')} />
|
<meta name="twitter:image:alt" content={imageAlt ?? t('seo.ogImageAlt')} />
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
Two faces, and only two: Inter 400 is every paragraph on the site and Space
|
Two faces, and only two: Inter 400 is every paragraph on the site and Space
|
||||||
|
|||||||
+34
-14
@@ -15,14 +15,15 @@
|
|||||||
* with the visible page is worse than none: it teaches a crawler that neither can be
|
* with the visible page is worse than none: it teaches a crawler that neither can be
|
||||||
* trusted.
|
* trusted.
|
||||||
*
|
*
|
||||||
* Two absences are deliberate:
|
* One absence is deliberate:
|
||||||
*
|
*
|
||||||
* - **No `SearchAction` on `WebSite`.** The search on /mints is an island filtering
|
|
||||||
* markup already in the page. There is no `?q=` URL to hand a crawler, and declaring
|
|
||||||
* one would promise a query endpoint that does not exist.
|
|
||||||
* - **No `Review` nodes.** Review bodies are fetched from Nostr relays in the browser,
|
* - **No `Review` nodes.** Review bodies are fetched from Nostr relays in the browser,
|
||||||
* so they are not in the prerendered HTML. Marking up text a crawler cannot see in
|
* so they are not in the prerendered HTML. Marking up text a crawler cannot see in
|
||||||
* the page is the same mismatch.
|
* the page is the same mismatch.
|
||||||
|
*
|
||||||
|
* The `SearchAction` on `WebSite` exists because /mints honours `?q=`: the search
|
||||||
|
* island reads the parameter on load (it is how the home page's quick filters deep
|
||||||
|
* link), so the URL the graph hands a crawler really does open a filtered list.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** A node in the graph. Loose by design: schema.org shapes vary per type. */
|
/** A node in the graph. Loose by design: schema.org shapes vary per type. */
|
||||||
@@ -118,6 +119,19 @@ export function siteNodes(o: {
|
|||||||
url: `${o.origin}/`,
|
url: `${o.origin}/`,
|
||||||
inLanguage: o.locale,
|
inLanguage: o.locale,
|
||||||
publisher: { '@id': orgId(o.origin) },
|
publisher: { '@id': orgId(o.origin) },
|
||||||
|
/*
|
||||||
|
* The English /mints URL for every locale: hreflang on the target page routes a
|
||||||
|
* visitor on from there, and one canonical action beats three that differ only
|
||||||
|
* by prefix.
|
||||||
|
*/
|
||||||
|
potentialAction: {
|
||||||
|
'@type': 'SearchAction',
|
||||||
|
target: {
|
||||||
|
'@type': 'EntryPoint',
|
||||||
|
urlTemplate: `${o.origin}/mints?q={search_term_string}`,
|
||||||
|
},
|
||||||
|
'query-input': 'required name=search_term_string',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -195,16 +209,20 @@ export function itemListNode(o: {
|
|||||||
/**
|
/**
|
||||||
* A mint, as the thing its page is about.
|
* A mint, as the thing its page is about.
|
||||||
*
|
*
|
||||||
* `Service` with an explicit `serviceType`, which is what a Cashu mint is: something a
|
* `Service` is the honest base type: something a third party runs and you can use.
|
||||||
* third party runs and you can use. Not a `Product` (nothing is sold here), not an
|
* Not an `Organization` — the operator is usually anonymous, this site knows nothing
|
||||||
* `Organization` (the operator is usually anonymous and this site knows nothing about
|
* about them beyond a URL, and Google shows no review snippets for reviews an
|
||||||
* them beyond a URL), not a `SoftwareApplication` (the software is named in `version`,
|
* Organization node carries about itself. Not a `SoftwareApplication` — the software
|
||||||
* but this page is about one deployment, not about cdk-mintd in general).
|
* is named in `version`, but this page is about one deployment.
|
||||||
*
|
*
|
||||||
* `Service` is not on Google's review-snippet list, so this will not draw stars in a
|
* `Service` alone is not on Google's review-snippet list, so a rated mint *also*
|
||||||
* search result. That is the accepted trade: `Product` would be eligible and would also
|
* carries `Product` (a dual-typed node), which is the eligible type. The reviews
|
||||||
* be a lie about what a custodial mint is, on a site whose whole premise is not
|
* behind the `AggregateRating` are third-party Nostr events about someone else's
|
||||||
* overclaiming.
|
* mint, not the site reviewing itself, so the self-serving-review policy is not the
|
||||||
|
* problem here — but review-snippet eligibility remains entirely at Google's
|
||||||
|
* discretion, which is why the `Product` half sits behind `SEO_PRODUCT_JSONLD`
|
||||||
|
* (`site.ts`) and can be switched off without touching a template. With the flag off,
|
||||||
|
* or with no ratings to carry, the node is exactly the plain `Service` it always was.
|
||||||
*/
|
*/
|
||||||
export function mintServiceNode(o: {
|
export function mintServiceNode(o: {
|
||||||
canonical: string;
|
canonical: string;
|
||||||
@@ -213,9 +231,11 @@ export function mintServiceNode(o: {
|
|||||||
mintUrl: string;
|
mintUrl: string;
|
||||||
icon?: string | null;
|
icon?: string | null;
|
||||||
rating: { value: number; ratingCount: number; reviewCount: number } | null;
|
rating: { value: number; ratingCount: number; reviewCount: number } | null;
|
||||||
|
/** Add the review-snippet-eligible `Product` type. Only honoured when rated. */
|
||||||
|
product?: boolean;
|
||||||
}): SchemaNode {
|
}): SchemaNode {
|
||||||
const node: SchemaNode = {
|
const node: SchemaNode = {
|
||||||
'@type': 'Service',
|
'@type': o.product && o.rating ? ['Product', 'Service'] : 'Service',
|
||||||
'@id': `${o.canonical}#mint`,
|
'@id': `${o.canonical}#mint`,
|
||||||
name: text(o.name, 120),
|
name: text(o.name, 120),
|
||||||
description: text(o.description, 300),
|
description: text(o.description, 300),
|
||||||
|
|||||||
+15
-2
@@ -17,11 +17,24 @@ export const GITHUB_URL = 'https://github.com/Azzamo-net/cashumints.space';
|
|||||||
export const AZZAMO_URL = 'https://azzamo.net';
|
export const AZZAMO_URL = 'https://azzamo.net';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default social card, served from `public/`.
|
* The default social card: brand, tagline and the network's stats, rendered by
|
||||||
|
* `pnpm og` into `public/og/` alongside the per-mint cards. Home, the list pages and
|
||||||
|
* any mint whose own card failed to render all point here.
|
||||||
*
|
*
|
||||||
* Relative on purpose: `Base.astro` resolves it against `Astro.site`, because an
|
* Relative on purpose: `Base.astro` resolves it against `Astro.site`, because an
|
||||||
* `og:image` that is not absolute is not a weaker signal, it is an ignored one.
|
* `og:image` that is not absolute is not a weaker signal, it is an ignored one.
|
||||||
|
* Stable filename, unlike the hashed per-mint cards, so it is served with
|
||||||
|
* `Cache-Control: max-age=3600` rather than immutable (see README).
|
||||||
*/
|
*/
|
||||||
export const OG_IMAGE = '/og.png';
|
export const OG_IMAGE = '/og/default.png';
|
||||||
export const OG_IMAGE_WIDTH = 1200;
|
export const OG_IMAGE_WIDTH = 1200;
|
||||||
export const OG_IMAGE_HEIGHT = 630;
|
export const OG_IMAGE_HEIGHT = 630;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a rated mint's JSON-LD also carries the review-snippet-eligible `Product`
|
||||||
|
* type (see `mintServiceNode` in schema.ts for the reasoning). On by default; set
|
||||||
|
* `SEO_PRODUCT_JSONLD=0` to fall back to the plain `Service` node on the next build.
|
||||||
|
* An env flag rather than a source edit, so the call can be reversed without a code
|
||||||
|
* change if Google's treatment of self-collected review markup shifts.
|
||||||
|
*/
|
||||||
|
export const PRODUCT_JSONLD = (process.env['SEO_PRODUCT_JSONLD'] ?? '1') !== '0';
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { ogImageFor } from '../../../lib/og';
|
|||||||
import { fedimintSubject } from '../../../lib/review-subject';
|
import { fedimintSubject } from '../../../lib/review-subject';
|
||||||
import { breadcrumbNode, mintServiceNode } from '../../../lib/schema';
|
import { breadcrumbNode, mintServiceNode } from '../../../lib/schema';
|
||||||
import { clampWords, cleanMintDescription, isIndexableMint, nameCollisions, seoName } from '../../../lib/seo';
|
import { clampWords, cleanMintDescription, isIndexableMint, nameCollisions, seoName } from '../../../lib/seo';
|
||||||
|
import { PRODUCT_JSONLD } from '../../../lib/site';
|
||||||
import { bannerHtml } from '../../../lib/mint-state';
|
import { bannerHtml } from '../../../lib/mint-state';
|
||||||
import { localModuleName, localNetworkName, moduleRowsHtml, otherModules } from '../../../lib/fedimint-ui';
|
import { localModuleName, localNetworkName, moduleRowsHtml, otherModules } from '../../../lib/fedimint-ui';
|
||||||
import { iconGradient, initials, transitionName, truncateMiddle } from '../../../lib/format';
|
import { iconGradient, initials, transitionName, truncateMiddle } from '../../../lib/format';
|
||||||
@@ -197,6 +198,7 @@ const schema = [
|
|||||||
canonical,
|
canonical,
|
||||||
name: titleName,
|
name: titleName,
|
||||||
description,
|
description,
|
||||||
|
product: PRODUCT_JSONLD,
|
||||||
/*
|
/*
|
||||||
* The invite code stands in for the service's address, because it is the address:
|
* The invite code stands in for the service's address, because it is the address:
|
||||||
* it is the string a wallet is given to reach this federation. A federation with no
|
* it is the string a wallet is given to reach this federation. A federation with no
|
||||||
@@ -217,11 +219,13 @@ const schema = [
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Base
|
<Base
|
||||||
title={t('fedimint.title', { name: titleName })}
|
title={title}
|
||||||
description={description}
|
description={description}
|
||||||
current="fedimints"
|
current="fedimints"
|
||||||
noindex={!indexable}
|
noindex={!indexable}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
|
image={ogImage ?? undefined}
|
||||||
|
imageAlt={imageAlt}
|
||||||
>
|
>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<nav class="crumbs" aria-label={t('fedimint.breadcrumb')}>
|
<nav class="crumbs" aria-label={t('fedimint.breadcrumb')}>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { ogImageFor } from '../../../lib/og';
|
|||||||
import { mintSubject } from '../../../lib/review-subject';
|
import { mintSubject } from '../../../lib/review-subject';
|
||||||
import { breadcrumbNode, mintServiceNode } from '../../../lib/schema';
|
import { breadcrumbNode, mintServiceNode } from '../../../lib/schema';
|
||||||
import { clampWords, cleanMintDescription, isIndexableMint, nameCollisions, seoName } from '../../../lib/seo';
|
import { clampWords, cleanMintDescription, isIndexableMint, nameCollisions, seoName } from '../../../lib/seo';
|
||||||
|
import { PRODUCT_JSONLD } from '../../../lib/site';
|
||||||
import { bannerHtml, limitsHtml, localNutName, nutRowsHtml } from '../../../lib/mint-state';
|
import { bannerHtml, limitsHtml, localNutName, nutRowsHtml } from '../../../lib/mint-state';
|
||||||
import {
|
import {
|
||||||
displayDomain, iconGradient, initials, shortNpub, transitionName, truncateMiddle,
|
displayDomain, iconGradient, initials, shortNpub, transitionName, truncateMiddle,
|
||||||
@@ -185,9 +186,13 @@ const title =
|
|||||||
* the plain status, and the limits are only quoted for a mint that can actually take
|
* the plain status, and the limits are only quoted for a mint that can actually take
|
||||||
* a deposit.
|
* a deposit.
|
||||||
*/
|
*/
|
||||||
|
// A mint with no name of its own (titleName fell back to the domain) gets the
|
||||||
|
// domain-only sentence, not "Community reviews for {domain} ({domain})".
|
||||||
const lead =
|
const lead =
|
||||||
cleanMintDescription(mint.description) ??
|
cleanMintDescription(mint.description) ??
|
||||||
t('seo.desc.fallback', { name: titleName === domain ? name : titleName, domain });
|
(titleName === domain
|
||||||
|
? t('seo.desc.fallbackDomain', { domain })
|
||||||
|
: t('seo.desc.fallback', { name: titleName, domain }));
|
||||||
|
|
||||||
const ratingPart = hasRating
|
const ratingPart = hasRating
|
||||||
? t('seo.desc.rated', { rating: f.decimal(mint.rating_avg!), n: mint.review_count })
|
? t('seo.desc.rated', { rating: f.decimal(mint.rating_avg!), n: mint.review_count })
|
||||||
@@ -251,6 +256,7 @@ const schema = [
|
|||||||
name: titleName,
|
name: titleName,
|
||||||
description,
|
description,
|
||||||
mintUrl: mint.url,
|
mintUrl: mint.url,
|
||||||
|
product: PRODUCT_JSONLD,
|
||||||
icon: icon ? new URL(icon, Astro.site).href : null,
|
icon: icon ? new URL(icon, Astro.site).href : null,
|
||||||
/*
|
/*
|
||||||
* Only when reviews actually carried ratings. `ratedCount` is what produced the
|
* Only when reviews actually carried ratings. `ratedCount` is what produced the
|
||||||
|
|||||||
Reference in New Issue
Block a user