Publishing and previews
How a document goes public, how scheduling and unpublishing work, and how to share signed preview links
When you publish a document the editor's request returns the moment the status flips. The slower work of rendering and cache invalidation runs in a Cloudflare Workflow you can watch from the admin. The same shape backs scheduling and unpublishing, so the lifecycle is symmetric end to end.
How publishing works
Cusp uses a hybrid render model: pages are pre-rendered to R2 at publish time, served from Cloudflare's Cache API with stale-while-revalidate, and the Worker only overlays request-time personalisation. Live server-side rendering is the fallback, never the hot path. Publishing a document runs the PublishDocument workflow, where each step is independently retryable and its progress is visible in the admin via the Workflows REST API.
The workflow renders the document's canonical URL to an HTML file in R2, then refreshes everything that page touches:
- Render the document — write the page HTML to rendered/{permalink}/index.html in R2, stamped with its surrogate keys.
- Render the listing — re-render the collection's listing page so the new document appears in its collection index. The home page is collection-independent and regenerates in the debounced aggregates step below.
- Index for search — add the document to the built-in search index so it surfaces in site search.
- Purge the cache — issue surrogate-key purges for the document and its listing, plus the sitemap and feed keys, so the edge serves the freshly rendered files.
- Regenerate aggregates — a debounced step rewrites the home page, sitemap.xml, the RSS feed, and robots.txt in R2 from the set of published documents, coalescing rapid successive publishes into one regeneration.
The document and its listings are rendered to R2 before the edge cache is invalidated, so the next request can never re-cache a stale page.
Surrogate-key cache invalidation
Every cached response carries a Surrogate-Key header listing the tags it belongs to, and the publish workflow purges exactly the affected tags rather than wiping the whole zone. The namespace is fixed so routes do not invent ad-hoc keys:
- doc:{id} — the document's page, its API representation, and its version list. Purged on publish, unpublish, republish, or delete.
- listing:{collection} — collection index pages and the collection API listing. Purged on any publish or unpublish in the collection.
- tag:{slug} and taxonomy:{slug} — reserved for tag and taxonomy pages. Taxonomies are on the roadmap, so no rendered page carries these keys today; they are part of the namespace ready for when taxonomy pages land.
- author:{userId} — reserved for an author's profile and listing. Like the tag and taxonomy keys, no rendered page carries it yet; it is part of the namespace ready for when author pages land.
- sitemap and feed — the XML artefacts. Purged on any publish or unpublish.
- theme, settings, nav — stamped on every cached HTML page, so a theme switch or a settings save can invalidate the whole site at once.
Purging is implemented with the Workers Cache API plus a KV registry of cached URLs per key, which works on the free and paid plans without needing Enterprise tag purge. The registry is best-effort: if a purge misses a URL, the stale-while-revalidate window bounds how long stale content can be served. Cross-colo invalidation rides a global cache epoch so a purge in one location is honoured everywhere.
Cache lifetimes are tuned per response shape. Document HTML is cached at the edge for an hour with a week-long stale-while-revalidate window; collection listings for ten minutes with a day-long stale-while-revalidate window; the sitemap, feed, and robots.txt for an hour, also with a day-long stale-while-revalidate window. Public HTML always sends max-age=0, must-revalidate to the browser, so an editor's next refresh sees changes immediately while the shared edge cache does the heavy lifting. Authenticated responses and preview-token responses are always private, no-store and never reach the edge cache.
Scheduled publishing
Set a future publish time and the document moves to the scheduled status. Cusp does not poll a cron table — it starts a ScheduledPublish workflow that sleeps until the scheduled moment using step.sleepUntil, costing nothing while it waits. When it wakes it checks that the document is still scheduled for that same time, flips it to published, and hands off to the normal PublishDocument pipeline.
Re-scheduling is safe: it starts a fresh sleeping instance, and the old one wakes to find the document no longer pinned to its time and quietly exits. Cancelling a schedule is simply moving the document away from the scheduled status — the sleeping workflow no-ops when it wakes.
Unpublishing
Unpublish mirrors publish. The editor's request synchronously does the work they need to see complete — it flips the status back to draft, deletes the rendered HTML from R2, evicts the page from the edge cache, and purges the doc:{id} key — so the URL really is a 404 by the time the response returns.
Everything else moves to the UnpublishDocument workflow, which re-renders the listing and home pages with the document removed, regenerates the sitemap and feeds, and purges the broader surrogate-key set. This closes a staleness window: without it, the sitemap and listings would keep advertising the unpublished URL until the next unrelated publish event. As with publish, the workflow re-renders the R2 origin before purging the edge cache.
Sharing drafts with signed preview links
Drafts and scheduled content are not public, but you often need a reviewer to see them. The admin generates a signed preview link with one click. The link carries a signed token that the Worker validates before it ever looks at the cache; on any failure it returns a 404, so a tampered or expired link leaks nothing about whether the document exists.
Preview tokens are deliberately narrow:
- Domain-locked — a token is bound to the hostname it was issued from and is rejected anywhere else, so a leaked link cannot be replayed against another hostname the deployment answers on.
- Single-use — a token is redeemable once. After the reviewer loads the page, presenting the same link again returns a 404. Generate a fresh link if they need to revisit.
- Time-limited — tokens expire, up to a maximum lifetime of thirty days.
- Scoped — a token authorises previewing a draft or scheduled document and nothing more; it grants no admin access.
Preview responses are always served private and no-store, so a preview is never cached at the edge and a reviewer always sees the current draft body. Single-use enforcement is backed by KV; if KV is briefly unavailable, preview fails open — the link may be redeemable more than once during the blip — because keeping reviewers locked out is the worse failure, and the signature, expiry, and domain checks still hold.
There is no terminal step and no config file to edit. You generate the link, copy it, and send it.
Read the versioning guide