Dev #1
@@ -38,6 +38,11 @@ PUBLIC_API_URL=
|
||||
# Canonical origin for canonical links, OpenGraph tags and the sitemap.
|
||||
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 ─────────────────────────────────────────────────────────────
|
||||
# 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,
|
||||
|
||||
@@ -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`
|
||||
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
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
# The social card, the icons and the manifest. Not fingerprinted, since their names
|
||||
# are referenced from outside the site, so a week with revalidation rather than a
|
||||
# year of immutability.
|
||||
# The favicons and the manifest. Not fingerprinted, since their names are referenced
|
||||
# from outside the site, so a week with revalidation rather than a year of immutability.
|
||||
location ~* ^/(og\.png|favicon\.(ico|svg)|apple-touch-icon\.png|icon-\d+\.png|site\.webmanifest)$ {
|
||||
expires 7d;
|
||||
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
|
||||
# blocks only if you build with PUBLIC_API_URL pointing at a separate API host.
|
||||
location /api/ {
|
||||
|
||||
+1
-12
@@ -114,12 +114,6 @@
|
||||
"card.reviewed": "reviewed {when}",
|
||||
"card.noReviews": "no reviews yet",
|
||||
"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.crumbMints": "Cashu mints",
|
||||
"mint.copyUrl": "Copy mint URL",
|
||||
@@ -546,6 +540,7 @@
|
||||
"seo.mint.titleNoReviews": "{name}: Cashu mint 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.fallbackDomain": "Community reviews for the Cashu mint at {domain}.",
|
||||
"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.noReviews": "No reviews yet.",
|
||||
@@ -557,7 +552,6 @@
|
||||
"seo.mint.ogAlt.other": "{name}: {rating} stars from {n} reviews on Cashumints.space",
|
||||
"seo.mint.ogAltNoReviews": "{name} on Cashumints.space",
|
||||
"nav.home": "Home",
|
||||
"mint.description.fallbackDomain": "A Cashu mint running at {domain}.",
|
||||
"mint.more.title": "More mints",
|
||||
"mint.more.all": "See all mints",
|
||||
"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.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.",
|
||||
"fedimint.title": "{name} · Fedimint federation reviews",
|
||||
"fedimint.crumb": "Fedimints",
|
||||
"fedimint.breadcrumb": "Breadcrumb",
|
||||
"fedimint.unnamed": "Unnamed federation",
|
||||
@@ -611,10 +604,6 @@
|
||||
"fedimint.more.title": "More 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.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.mint": "Ecash mint",
|
||||
"fedimint.module.wallet": "On-chain wallet",
|
||||
|
||||
+1
-12
@@ -118,12 +118,6 @@
|
||||
"card.reviewed": "reseñado {when}",
|
||||
"card.noReviews": "sin reseñas aún",
|
||||
"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.crumbMints": "Mints Cashu",
|
||||
"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.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.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.other": "Valorado {rating}/5 en {n} reseñas firmadas en Nostr.",
|
||||
"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.ogAltNoReviews": "{name} en Cashumints.space",
|
||||
"nav.home": "Inicio",
|
||||
"mint.description.fallbackDomain": "Un mint Cashu que funciona en {domain}.",
|
||||
"mint.more.title": "Más mints",
|
||||
"mint.more.all": "Ver todos los mints",
|
||||
"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.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.",
|
||||
"fedimint.title": "{name} · Reseñas de la federación Fedimint",
|
||||
"fedimint.crumb": "Fedimints",
|
||||
"fedimint.breadcrumb": "Ruta de navegación",
|
||||
"fedimint.unnamed": "Federación sin nombre",
|
||||
@@ -615,10 +608,6 @@
|
||||
"fedimint.more.title": "Más 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.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.mint": "Mint de ecash",
|
||||
"fedimint.module.wallet": "Monedero on-chain",
|
||||
|
||||
+1
-12
@@ -114,12 +114,6 @@
|
||||
"card.reviewed": "laatste review {when}",
|
||||
"card.noReviews": "nog geen reviews",
|
||||
"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.crumbMints": "Cashu mints",
|
||||
"mint.copyUrl": "Mint-URL kopiëren",
|
||||
@@ -546,6 +540,7 @@
|
||||
"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.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.other": "Beoordeeld met {rating}/5 uit {n} op Nostr ondertekende 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.ogAltNoReviews": "{name} op Cashumints.space",
|
||||
"nav.home": "Home",
|
||||
"mint.description.fallbackDomain": "Een Cashu-mint die draait op {domain}.",
|
||||
"mint.more.title": "Meer mints",
|
||||
"mint.more.all": "Bekijk alle mints",
|
||||
"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.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.",
|
||||
"fedimint.title": "{name} · Reviews van deze Fedimint-federatie",
|
||||
"fedimint.crumb": "Fedimints",
|
||||
"fedimint.breadcrumb": "Kruimelpad",
|
||||
"fedimint.unnamed": "Naamloze federatie",
|
||||
@@ -611,10 +604,6 @@
|
||||
"fedimint.more.title": "Meer federaties",
|
||||
"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.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.mint": "Ecash mint",
|
||||
"fedimint.module.wallet": "On-chain wallet",
|
||||
|
||||
@@ -57,14 +57,20 @@ interface Props {
|
||||
schema?: SchemaNode[];
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* 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 {
|
||||
title, description, current, mintCount, ogType = 'website',
|
||||
noindex = false, offGraph = false, schema = [], image = OG_IMAGE,
|
||||
imageAlt,
|
||||
} = Astro.props;
|
||||
|
||||
/*
|
||||
@@ -217,7 +223,7 @@ const noscriptMotion =
|
||||
<meta property="og:image" content={imageUrl} />
|
||||
<meta property="og:image:width" content={String(OG_IMAGE_WIDTH)} />
|
||||
<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
|
||||
@@ -227,7 +233,7 @@ const noscriptMotion =
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<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
|
||||
|
||||
+34
-14
@@ -15,14 +15,15 @@
|
||||
* with the visible page is worse than none: it teaches a crawler that neither can be
|
||||
* 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,
|
||||
* so they are not in the prerendered HTML. Marking up text a crawler cannot see in
|
||||
* 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. */
|
||||
@@ -118,6 +119,19 @@ export function siteNodes(o: {
|
||||
url: `${o.origin}/`,
|
||||
inLanguage: o.locale,
|
||||
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.
|
||||
*
|
||||
* `Service` with an explicit `serviceType`, which is what a Cashu mint is: something a
|
||||
* third party runs and you can use. Not a `Product` (nothing is sold here), not an
|
||||
* `Organization` (the operator is usually anonymous and this site knows nothing about
|
||||
* them beyond a URL), not a `SoftwareApplication` (the software is named in `version`,
|
||||
* but this page is about one deployment, not about cdk-mintd in general).
|
||||
* `Service` is the honest base type: something a third party runs and you can use.
|
||||
* Not an `Organization` — the operator is usually anonymous, this site knows nothing
|
||||
* about them beyond a URL, and Google shows no review snippets for reviews an
|
||||
* Organization node carries about itself. Not a `SoftwareApplication` — the software
|
||||
* 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
|
||||
* search result. That is the accepted trade: `Product` would be eligible and would also
|
||||
* be a lie about what a custodial mint is, on a site whose whole premise is not
|
||||
* overclaiming.
|
||||
* `Service` alone is not on Google's review-snippet list, so a rated mint *also*
|
||||
* carries `Product` (a dual-typed node), which is the eligible type. The reviews
|
||||
* behind the `AggregateRating` are third-party Nostr events about someone else's
|
||||
* 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: {
|
||||
canonical: string;
|
||||
@@ -213,9 +231,11 @@ export function mintServiceNode(o: {
|
||||
mintUrl: string;
|
||||
icon?: string | null;
|
||||
rating: { value: number; ratingCount: number; reviewCount: number } | null;
|
||||
/** Add the review-snippet-eligible `Product` type. Only honoured when rated. */
|
||||
product?: boolean;
|
||||
}): SchemaNode {
|
||||
const node: SchemaNode = {
|
||||
'@type': 'Service',
|
||||
'@type': o.product && o.rating ? ['Product', 'Service'] : 'Service',
|
||||
'@id': `${o.canonical}#mint`,
|
||||
name: text(o.name, 120),
|
||||
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';
|
||||
|
||||
/**
|
||||
* 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
|
||||
* `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_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 { breadcrumbNode, mintServiceNode } from '../../../lib/schema';
|
||||
import { clampWords, cleanMintDescription, isIndexableMint, nameCollisions, seoName } from '../../../lib/seo';
|
||||
import { PRODUCT_JSONLD } from '../../../lib/site';
|
||||
import { bannerHtml } from '../../../lib/mint-state';
|
||||
import { localModuleName, localNetworkName, moduleRowsHtml, otherModules } from '../../../lib/fedimint-ui';
|
||||
import { iconGradient, initials, transitionName, truncateMiddle } from '../../../lib/format';
|
||||
@@ -197,6 +198,7 @@ const schema = [
|
||||
canonical,
|
||||
name: titleName,
|
||||
description,
|
||||
product: PRODUCT_JSONLD,
|
||||
/*
|
||||
* 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
|
||||
@@ -217,11 +219,13 @@ const schema = [
|
||||
---
|
||||
|
||||
<Base
|
||||
title={t('fedimint.title', { name: titleName })}
|
||||
title={title}
|
||||
description={description}
|
||||
current="fedimints"
|
||||
noindex={!indexable}
|
||||
schema={schema}
|
||||
image={ogImage ?? undefined}
|
||||
imageAlt={imageAlt}
|
||||
>
|
||||
<div class="page">
|
||||
<nav class="crumbs" aria-label={t('fedimint.breadcrumb')}>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ogImageFor } from '../../../lib/og';
|
||||
import { mintSubject } from '../../../lib/review-subject';
|
||||
import { breadcrumbNode, mintServiceNode } from '../../../lib/schema';
|
||||
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 {
|
||||
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
|
||||
* 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 =
|
||||
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
|
||||
? t('seo.desc.rated', { rating: f.decimal(mint.rating_avg!), n: mint.review_count })
|
||||
@@ -251,6 +256,7 @@ const schema = [
|
||||
name: titleName,
|
||||
description,
|
||||
mintUrl: mint.url,
|
||||
product: PRODUCT_JSONLD,
|
||||
icon: icon ? new URL(icon, Astro.site).href : null,
|
||||
/*
|
||||
* Only when reviews actually carried ratings. `ratedCount` is what produced the
|
||||
|
||||
Reference in New Issue
Block a user