Cron (sidebar widgets and post stats)

Running it — part 4 of 6

scripts/refresh-sidebar.sh refetches the widget JSON (toots, Bluesky, Pixelfed, commits, RSS) and per-post stats, then uploads only those files -- no site rebuild:

*/30 * * * * /path/to/blog.sh/scripts/refresh-sidebar.sh

Every 30 minutes is plenty. The crontab lines in this section are recommendations; what actually runs is whatever the installation's own crontab says -- installing it is step 7 of Getting started. Post stats refresh live for posts younger than ~90 days; older posts get a full refresh about once a week (tracked in .stats_full_refresh_at). A failed fetch keeps the last known content rather than publishing an empty widget -- a one-minute network hiccup never blanks the sidebar. Systems without cron: a systemd timer or launchd job invoking the same script does the same thing. No widgets configured = no cron needed.

The on_this_day card (see On this day) fetches nothing, but its day turns over only here: its file is rewritten when the date changes and left alone on every other tick, in the site root and in every language tree.

With comments.approval: fav this job stops being optional. It is what reads which replies you favourited and writes comments.json, so without it a newly starred comment never reaches the site -- the pages keep whatever was uploaded last. The same "keeps the last known content" rule applies and matters more here: a failed fetch leaves the thread as last published rather than blanking a discussion.

Two timings to know. A comment starred under a recent post appears at the next run, so within the cron interval. Under a post older than ~90 days it waits for the weekly full pass -- run

./scripts/refresh-sidebar.sh --full

to do that pass now instead. And switching moderation back off deletes comments.json on the next run, so a rejected comment doesn't stay readable at a public URL after the page stops showing it.

A second, optional job publishes scheduled drafts (./blog.sh schedule) once their date arrives -- it exits immediately when nothing is due, so a tight interval costs nothing:

*/15 * * * * /path/to/blog.sh/scripts/publish-scheduled.sh >/dev/null

Every run of that job touches .last-scheduled-run in the project root, including the runs with nothing due. That file is the only evidence that anything is serving the queue at all, and it is what ./blog.sh doctor reads to tell a queue that is simply waiting from one whose cron was never set up.

A post is announced before the site is rebuilt, so the toot and the page it links to come from the same build. If the deploy then fails, the job leaves a .deploy-pending marker and the next run retries the deploy on its own, even with nothing due -- so an announcement never keeps pointing at a page that was never uploaded. A post that cannot be published (a slug the target year already owns, a malformed date) is reported by name and skipped; the rest of the batch still publishes.

One writer at a time

Building and deploying take an advisory lock (.blog-sh.lock in the project root), because two of the things that write public.nosync run from cron: the scheduled publish and the sidebar refresh (Cron). On a large archive a build plus a full deploy takes longer than a tick, so overlapping runs are ordinary -- and what they do to each other is not: a deploy walking a tree that is being rewritten, or pruning as an orphan a page the other run has just published.

A run that finds the lock held does not wait for it. A cron tick says so and leaves with exit 0 -- cron is back within its interval, and a queue of blocked publishes would all wake up and do the same work at once. A run you started reports it and exits non-zero, so ./blog.sh doesn't tell you a deploy happened when it didn't. The scheduled publish holds the lock for its whole run (publish, rebuild, deploy are one operation as far as the site is concerned), and the build and deploy it shells out to inherit it rather than deadlock against their own parent.

Reordering the queue takes the same lock, for a different reason: it does not write public.nosync at all, it writes the same post files the scheduled publish is publishing from. It holds the lock only for the moment of the move -- the checks and the writes together -- never while a prompt is open, so a queue left on screen never keeps the cron out.

If the filesystem can't do advisory locks -- some network mounts -- the lock degrades to no lock, which is where every installation was before this existed.