Drop the nightly rebuild timer.

The timer was load-bearing while the mint list was a build-time snapshot: a
rebuild was the only way a new mint, a new review count or a changed status ever
reached /mints. The list hydrates now, so all three arrive within a second of
load, in every language, and rebuilding 2,000 pages at 03:30 to refresh numbers
that refresh themselves is twenty minutes of CPU for nothing.

cashumints-web.service stays exactly as it is — it is the deploy-time publish
step, and now the only thing that starts it is a deploy. A build still produces
what only a build can: the prerendered HTML a crawler reads, a social card per
mint, the sitemap and hreflang set, and a /mint/{host} page for every mint known
at build time.

The one thing that gets staler is that last item. A mint indexed since the last
deploy has no prerendered page: /mint/newhost is a 404, whose resolver looks the
address up against the live API and renders it — readable, reviewable, noindex
until a deploy gives it a real page. That was already true between nightly
builds; this only lengthens the window.

README documents the one-time host commands to remove the installed timer, and
gains a "Live lists" section describing what replaced it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
michilis
2026-08-25 16:29:09 +02:00
co-authored by Claude Opus 5
parent 14548179a0
commit 24fe2003b6
3 changed files with 110 additions and 52 deletions
+89 -18
View File
@@ -296,7 +296,52 @@ Three packages in order, and the order is a dependency chain rather than a habit
`shared` emits the types and the warning copy both other packages import, `api` compiles `shared` emits the types and the warning copy both other packages import, `api` compiles
`api/src` to `api/dist`, and `web` prerenders against a running API. `api/src` to `api/dist`, and `web` prerenders against a running API.
Output lands in `api/dist/` and `web/dist/`. Every page is prerendered once per language, so ~55 mints Output lands in `api/dist/` and `web/dist/`.
### Live lists
The prerendered mint list is a snapshot of what `GET /api/mints` said when the build ran.
It used to stay that until the next build, which is why there was a nightly timer: a mint
indexed at noon was reviewable at once — the 404 resolver saw to that — and had no card on
`/mints` until 03:30, beside cards whose ratings and statuses were equally old.
`/mints`, `/fedimints`, `/lnurl-mints` and the home page's three top-six strips now refetch
that endpoint once, after paint, and rebuild their grids from the answer. One request per
page, no relays involved: card counts have always come from the API's ingested review
aggregates, and review *bodies* remain a mint page and `/reviews` concern.
**The prerendered cards stay.** They are the first paint, they are what a crawler indexes,
and they are the whole page for a reader with no JavaScript — `<noscript>` already unhides
them, and the search, sort and filter controls act on whatever is in the DOM. Hydration
only ever replaces them with something newer, and never with nothing:
- a failed fetch does nothing at all, silently — a grid that is correct as of the last
build is a far better answer to a flaky network than an error about a list already on
screen;
- an API answering `[]` also does nothing. Serving an empty index is the failure the
[build gate](#rebuilds) exists to catch, and a page that rendered it as "no mints" would
be that bug wearing a different hat;
- whatever the reader had already set — a search they typed, a sort they picked, "hide
offline" — is re-applied to the new cards, so a refresh landing mid-interaction cannot
undo it.
Two pieces make it work. `web/src/lib/mint-cards.ts` is `MintCard.astro`'s browser twin,
the same relationship `review-cards.ts` has with the reviews panel: identical classes and
identical `data-*` attributes, because the sort keys, the search fields, the rank chips and
the shared-element view transitions are all read off the DOM. And `MintListItem` carries
the facts a chip is drawn from — `nuts`, `capabilities`, and the LNURL withdraw ceiling and
funding flag. Those replaced an N+1: `/mints` and `/lnurl-mints` each used to fetch
`GET /api/mints/:host` once per mint at build time to read two booleans off it, which is
tolerable on a build machine and unthinkable in every visitor's browser.
Strings come from the page's own inlined catalog, so a hydrated Spanish card says "En
línea", "54 reseñas" and "4,9", and its link is `/es/mint/…`. The one thing hydration
cannot improve is the home page's sentiment bars: the build gives those six cards real
rating distributions from a per-mint detail fetch, and the list payload has no
distribution in it, so a refreshed card falls back to the rating proxy the index pages
have always used.
Every page is prerendered once per language, so ~55 mints
and 9 static routes come out as ~200 pages, each with real titles, meta descriptions, and 9 static routes come out as ~200 pages, each with real titles, meta descriptions,
OpenGraph and Twitter tags, a social card, a self-referencing canonical, a full hreflang OpenGraph and Twitter tags, a social card, a self-referencing canonical, a full hreflang
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`
@@ -1359,10 +1404,36 @@ journalctl -t cashumints-alert -n 5 --no-pager
### Rebuilds ### Rebuilds
Mint pages are prerendered, so new mints and new review counts appear at the next build. **Builds happen on deploy. There is no timer.**
A nightly rebuild is enough; the site stays correct in between because the islands
refresh status and reviews at runtime, and an unbuilt mint still resolves through the ```bash
client-side fallback on the 404 page. sudo systemctl start cashumints-web # build, then publish
```
There used to be a `cashumints-web.timer` firing at 03:30 nightly, and it was load-bearing:
the mint list was a snapshot of whatever the API held when `astro build` ran, so a
rebuild was the only way a new mint, a new review count or a changed status ever reached
`/mints`. The list hydrates now — one `GET /api/mints` after paint, see
[Live lists](#live-lists) — so all three reach the page within a second of load, in every
language, and rebuilding 2,000 pages overnight to refresh numbers that refresh themselves
is twenty minutes of CPU for nothing.
What a build still produces, and therefore what a deploy is still for:
| Still built | Still stale between deploys |
| --- | --- |
| The prerendered HTML a crawler reads | The `ItemList` JSON-LD on the index pages |
| A social card per mint | The card for a mint indexed since the deploy |
| `sitemap.xml` and the hreflang set | A `/mint/{host}` page for a mint indexed since the deploy |
That last row is the one to know about. A mint indexed today has no prerendered page of
its own until the next deploy: `/mint/newhost` returns **404**, and the 404 page's
resolver looks the address up against the live API and renders it — readable, reviewable,
linkable, with a `noindex` on it until the deploy gives it a real page. That was already
true between nightly builds; dropping the timer only lengthens the window.
Deploy when you ship code, or when enough new mints have accumulated that their pages are
worth prerendering. Nothing breaks if you do not.
Publishing is a separate step from building, and the separation is the point: the copy Publishing is a separate step from building, and the separation is the point: the copy
the site server reads is only touched once a build has succeeded, so a failed build the site server reads is only touched once a build has succeeded, so a failed build
@@ -1464,7 +1535,7 @@ ExecStartPost=/usr/bin/rsync -a --delete-after --delay-updates web/dist/ /var/li
# ~500 prerendered pages plus a card per mint. Minutes, not seconds, on a small VPS, and # ~500 prerendered pages plus a card per mint. Minutes, not seconds, on a small VPS, and
# TimeoutStartSec is what bounds a Type=oneshot. # TimeoutStartSec is what bounds a Type=oneshot.
TimeoutStartSec=1800 TimeoutStartSec=1800
# A nightly rebuild should not starve the API it is reading from. # A build should not starve the API it is reading from.
Nice=10 Nice=10
UMask=0022 UMask=0022
@@ -1481,22 +1552,23 @@ ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
``` ```
There is deliberately no `[Install]` section: a rebuild should be scheduled, not fired on There is deliberately no `[Install]` section: this belongs to a deploy, not to a boot.
every boot.
```ini #### Removing the timer
# /etc/systemd/system/cashumints-web.timer
[Unit]
Description=Nightly cashumints.space rebuild
[Timer] On a host that still has the nightly timer installed, once:
OnCalendar=*-*-* 03:30:00
Persistent=true
[Install] ```bash
WantedBy=timers.target sudo systemctl disable --now cashumints-web.timer
sudo rm -f /etc/systemd/system/cashumints-web.timer
sudo systemctl daemon-reload
systemctl list-timers --all | grep cashumints # expect nothing
``` ```
`disable --now` both stops the pending job and removes the `timers.target` symlink;
without the `rm` and the `daemon-reload`, systemd keeps a unit it can still be asked to
start by name.
### First deploy ### First deploy
Order matters once: the site server refuses to start against a root that has no Order matters once: the site server refuses to start against a root that has no
@@ -1507,7 +1579,6 @@ Order matters once: the site server refuses to start against a root that has no
sudo systemctl enable --now cashumints # API first: the build reads from it sudo systemctl enable --now cashumints # API first: the build reads from it
sudo systemctl start cashumints-web # build, then publish to /var/lib/cashumints/web sudo systemctl start cashumints-web # build, then publish to /var/lib/cashumints/web
sudo systemctl enable --now cashumints-site # now it has something to serve sudo systemctl enable --now cashumints-site # now it has something to serve
sudo systemctl enable --now cashumints-web.timer
sudo nginx -t && sudo systemctl reload nginx sudo nginx -t && sudo systemctl reload nginx
``` ```
+21 -9
View File
@@ -6,12 +6,23 @@
# a social card per mint and prerenders every page from the live API. The daemon that # a social card per mint and prerenders every page from the live API. The daemon that
# hands the result out is cashumints-site.service. # hands the result out is cashumints-site.service.
# #
# Run it after a deploy: # Run it after a deploy, and only after a deploy:
# sudo systemctl start cashumints-web # sudo systemctl start cashumints-web
# #
# Mint pages are prerendered, so new mints and new review counts only appear at the # There used to be a cashumints-web.timer firing this at 03:30 every night, because the
# next build; pair this with a .timer for the nightly rebuild. There is deliberately no # mint list was a snapshot of whatever the API held when the build ran and a nightly
# [Install] section — a rebuild should be scheduled, not fired on every boot. # rebuild was the only way it ever changed. The list hydrates from the API after paint
# now, so a new mint, a new review count and a changed status all reach the page within
# a second of load, and rebuilding 2,000 pages at 03:30 to refresh numbers that refresh
# themselves is 20 minutes of CPU for nothing.
#
# What a build still produces, and therefore what a deploy is still for: the prerendered
# HTML a crawler reads, the social card per mint, the sitemap, and a `/mint/{host}` page
# for every mint known at build time. A mint indexed since the last deploy has no page of
# its own until the next one; the 404 fallback resolves it against the live API, so it is
# readable and reviewable in the meantime. That was already true between nightly builds.
#
# There is deliberately no [Install] section — this belongs to a deploy, not to a boot.
[Unit] [Unit]
Description=Rebuild the cashumints.space static site Description=Rebuild the cashumints.space static site
@@ -93,10 +104,11 @@ ExecStart=/usr/bin/pnpm build
# Publish, as a separate step from building. # Publish, as a separate step from building.
# #
# `astro build` empties dist before it writes, so the site server cannot read dist # `astro build` empties dist before it writes, so the site server cannot read dist
# directly — a nightly rebuild would be a nightly minute of 404s. It serves this copy # directly — a rebuild would be a minute of 404s. It serves this copy instead, and the
# instead, and the copy is only touched once a build has succeeded: a failed build # copy is only touched once a build has succeeded: a failed build leaves the previous
# leaves the previous site up rather than replacing it with a half-written one, which is # site up rather than replacing it with a half-written one, which is the same reason
# the same reason Requires=cashumints.service is above. # Requires=cashumints.service is above and the same reason the mint-count gate is an
# ExecStartPre rather than a check after the fact.
# #
# --delay-updates stages the changed files and renames them in at the end, so the window # --delay-updates stages the changed files and renames them in at the end, so the window
# where the tree is a mix of two builds is a rename rather than a whole transfer, and # where the tree is a mix of two builds is a rename rather than a whole transfer, and
@@ -107,7 +119,7 @@ ExecStartPost=/usr/bin/rsync -a --delete-after --delay-updates web/dist/ /var/li
# ~200 prerendered pages plus a card per mint. Minutes, not seconds, on a small VPS, and # ~200 prerendered pages plus a card per mint. Minutes, not seconds, on a small VPS, and
# TimeoutStartSec is what bounds a Type=oneshot. # TimeoutStartSec is what bounds a Type=oneshot.
TimeoutStartSec=1800 TimeoutStartSec=1800
# A nightly rebuild should not starve the API it is reading from. # A build should not starve the API it is reading from.
Nice=10 Nice=10
# The site server runs as cashumints and reads its own files, so this no longer has to # The site server runs as cashumints and reads its own files, so this no longer has to
-25
View File
@@ -1,25 +0,0 @@
# /etc/systemd/system/cashumints-web.timer
#
# The nightly rebuild. Mint pages are prerendered, so a new mint or a new review count
# appears at the next build; between builds the site stays correct because the islands
# refresh status and reviews at runtime, and a mint indexed since the last build still
# resolves through the client-side fallback on the 404 page.
#
# sudo systemctl enable --now cashumints-web.timer
#
# The service it starts builds first and publishes second, so a failed build leaves the
# running site untouched rather than replacing it with a half-written one.
[Unit]
Description=Nightly cashumints.space rebuild
[Timer]
OnCalendar=*-*-* 03:30:00
# Run it on the next boot if the machine was off at 03:30, rather than skipping a day.
Persistent=true
# Without this every host rebuilds on the same second. Harmless with one VPS; free
# insurance if there is ever a second.
RandomizedDelaySec=15m
[Install]
WantedBy=timers.target