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:
co-authored by
Claude Opus 5
parent
14548179a0
commit
24fe2003b6
@@ -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
|
||||
`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,
|
||||
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`
|
||||
@@ -1359,10 +1404,36 @@ journalctl -t cashumints-alert -n 5 --no-pager
|
||||
|
||||
### Rebuilds
|
||||
|
||||
Mint pages are prerendered, so new mints and new review counts appear at the next build.
|
||||
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
|
||||
client-side fallback on the 404 page.
|
||||
**Builds happen on deploy. There is no timer.**
|
||||
|
||||
```bash
|
||||
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
|
||||
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
|
||||
# TimeoutStartSec is what bounds a Type=oneshot.
|
||||
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
|
||||
UMask=0022
|
||||
|
||||
@@ -1481,22 +1552,23 @@ ProtectControlGroups=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
```
|
||||
|
||||
There is deliberately no `[Install]` section: a rebuild should be scheduled, not fired on
|
||||
every boot.
|
||||
There is deliberately no `[Install]` section: this belongs to a deploy, not to a boot.
|
||||
|
||||
```ini
|
||||
# /etc/systemd/system/cashumints-web.timer
|
||||
[Unit]
|
||||
Description=Nightly cashumints.space rebuild
|
||||
#### Removing the timer
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03:30:00
|
||||
Persistent=true
|
||||
On a host that still has the nightly timer installed, once:
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
```bash
|
||||
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
|
||||
|
||||
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 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-web.timer
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
|
||||
@@ -6,12 +6,23 @@
|
||||
# a social card per mint and prerenders every page from the live API. The daemon that
|
||||
# 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
|
||||
#
|
||||
# Mint pages are prerendered, so new mints and new review counts only appear at the
|
||||
# next build; pair this with a .timer for the nightly rebuild. There is deliberately no
|
||||
# [Install] section — a rebuild should be scheduled, not fired on every boot.
|
||||
# There used to be a cashumints-web.timer firing this at 03:30 every night, because the
|
||||
# mint list was a snapshot of whatever the API held when the build ran and a nightly
|
||||
# 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]
|
||||
Description=Rebuild the cashumints.space static site
|
||||
@@ -93,10 +104,11 @@ ExecStart=/usr/bin/pnpm build
|
||||
# Publish, as a separate step from building.
|
||||
#
|
||||
# `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
|
||||
# instead, and the copy is only touched once a build has succeeded: a failed build
|
||||
# leaves the previous site up rather than replacing it with a half-written one, which is
|
||||
# the same reason Requires=cashumints.service is above.
|
||||
# directly — a rebuild would be a minute of 404s. It serves this copy instead, and the
|
||||
# copy is only touched once a build has succeeded: a failed build leaves the previous
|
||||
# site up rather than replacing it with a half-written one, which is the same reason
|
||||
# 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
|
||||
# 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
|
||||
# TimeoutStartSec is what bounds a Type=oneshot.
|
||||
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
|
||||
|
||||
# The site server runs as cashumints and reads its own files, so this no longer has to
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user