Add a [cdn] section and purge the edge after publishing

The site is about to sit behind Cloudflare, where the public pages will be
allowed to live at the edge for a day (`s-maxage`, next commit). That is only
correct if the edge is emptied the moment a new issue lands, so the origin now
does the emptying itself rather than leaving it to an operator to remember.

`[cdn]` is inert by default: with no `provider` nothing is called and no token
is needed, so an origin with no CDN behaves exactly as before. Setting
`provider = "cloudflare"` without both a zone id and `DAILY_EPUB_CDN__API_TOKEN`
is a config error — a half-configured purge would publish into a stale edge and
say nothing.

The purge is `purge_everything` on purpose. A new issue changes more than its
own page: `/`, `/issues`, `/feed.xml`, `/issues.json`, and the previous issue's
page too, whose "latest" nav marker moves. A per-URL list of that set is exactly
the kind of thing that silently rots, and everything expensive at the edge is
content-hashed, so refilling it costs one origin fetch.

A purge failure is logged at warn and never fails the run: the paper is already
published and recorded by then, and a few stale hours are not worth failing over.
`daily-epub cdn purge` runs the same code by hand; it takes no run lock because
it touches neither the database nor the publish directories.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va5eMEmWEnjMXBsBob5FDW
This commit is contained in:
2026-09-04 21:28:48 +00:00
co-authored by Claude Fable 5.1
parent 2388d7bd02
commit 41fe61a691
7 changed files with 580 additions and 1 deletions
+10 -1
View File
@@ -218,9 +218,12 @@ const OPTIONAL_KEYS: &[(&str, FieldKind)] = &[
("server.basic_auth_user", FieldKind::Text),
("server.basic_auth_pass", FieldKind::Secret),
("xtc.settings", FieldKind::Path),
("cdn.provider", FieldKind::Text),
("cdn.cloudflare_zone_id", FieldKind::Text),
("cdn.api_token", FieldKind::Secret),
];
const SECRET_SUFFIXES: &[&str] = &["api_key", "hmac_secret", "basic_auth_pass"];
const SECRET_SUFFIXES: &[&str] = &["api_key", "hmac_secret", "basic_auth_pass", "api_token"];
const PATH_KEYS: &[&str] = &[
"database_path",
@@ -255,6 +258,7 @@ const GROUP_ORDER: &[&str] = &[
"publish",
"xtc",
"server",
"cdn",
"miniflux",
];
@@ -367,6 +371,10 @@ pub const SETTINGS_HELP: &[(&str, &str)] = &[
("server.login_window_minutes", "Length of the login throttle window."),
("server.jobs_enabled", "Allow the dashboard to start the fixed systemd job catalogue."),
("server.journal_lines", "Journal lines shown on a dashboard job page (10-5000)."),
("cdn.provider", "cloudflare, or empty for no CDN integration at all. Setting it requires both cdn.cloudflare_zone_id and the API token."),
("cdn.cloudflare_zone_id", "Zone id from the Cloudflare dashboard overview for the site's zone."),
("cdn.api_token", "Environment only (DAILY_EPUB_CDN__API_TOKEN). Needs exactly one permission: Zone -> Cache Purge, scoped to that one zone."),
("cdn.purge_after_publish", "Purge the whole edge cache after generate publishes an issue. A purge failure is logged and does not fail the run; dry runs never purge."),
];
/// `DAILY_EPUB_` + the path upper-cased with `.` → `__` (§13.1 item 2).
@@ -1683,6 +1691,7 @@ mod tests {
"publish",
"xtc",
"server",
"cdn",
"miniflux",
]
);