Documentation
Caching & CDN
Blobify writes every artifact with an explicit Cache-Control header, and CDNs respect origin headers by default. That means your caching posture is already decided unless you decide otherwise. This page explains what the defaults are and how to change them deliberately.
What Blobify sets on each artifact
There are two classes of objects in your bucket:
| Class | Artifacts | Cache-Control |
|---|---|---|
| Mutable | content/{model}/{id}/published.json, summary manifests (summaries/published/index.json), list-index manifests, lookup pointers, assets/catalog.json, schemas/manifest.json | no-store, max-age=0, must-revalidate |
| Immutable | Summary shard files, list-index page files, asset media files | public, max-age=31536000, immutable |
The split follows the pointer pattern: mutable objects are small pointers that must always be fresh, and they name versioned immutable objects that can be cached forever. Whenever content changes, Blobify writes new immutable files and updates the pointers. The immutable files themselves never change after creation.
Immutable artifacts are already optimally cached. Everything below is about the mutable class only.
Posture 1: always fresh (the default)
Do nothing. Every read of a mutable artifact goes to the bucket, and readers always see the latest published state within seconds of the queue worker converging.
This is the right choice for most sites. Reads are cheap (R2 has no egress fees and 10 million requests per month free through Cloudflare), and static-site generators only read at build time anyway. Don't add caching machinery until request volume or origin latency gives you a reason.
Posture 2: short TTL with stale-while-revalidate
If your site reads content JSON at request time (SSR, ISR misses, client-side fetches) and traffic is high, cache the mutable artifacts briefly at the edge and accept a bounded window of staleness.
With a Cloudflare Cache Rule on your bucket's domain:
When: hostname eq cdn.example.com
and ends_with(http.request.uri.path, ".json")
Then: Eligible for cache
Edge TTL: override origin, 60 seconds
Serve stale while revalidating: onPublished content is then at most ~60 seconds behind the dashboard, and origin reads drop to one per URL per minute regardless of traffic. Tune the TTL to the staleness your site can tolerate; 30–120 seconds covers most cases.
Notes:
- Scope the rule to your bucket hostname (or the path prefix you serve content from), not your whole zone.
- The rule matches immutable artifacts too, which is fine: the override is equal to or shorter than their origin header, and you can exclude them if you want their full year at the edge.
- Never write a rule that adds caching to the private tree (the
pv_...folder). Its paths are unguessable and its objects are markedno-store; leave both properties intact.
Posture 3: cache forever, purge on publish
The strongest setup: long edge TTLs on content JSON (and your rendered pages), invalidated the moment content actually changes. This is what webhook payloads are designed for: every delivery carries the affected entry's resolved page urls and stable revalidationTags.
Point a webhook at a small worker that verifies the signature (see Webhooks for the verification code) and purges the affected URLs:
// Cloudflare Worker: purge edge cache when Blobify publishes.
type BlobifyWebhookPayload = {
event: string;
space: string;
model: string;
contentId: string;
urls: string[];
};
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const rawBody = await request.text();
// Verify X-Blobify-Signature over rawBody first (code in the Webhooks doc).
const payload = JSON.parse(rawBody) as BlobifyWebhookPayload;
const files = [
// The rendered pages that show this entry.
...payload.urls.map((path) => `https://www.example.com${path}`),
// The entry's content JSON in the bucket.
`https://cdn.example.com/spaces/${payload.space}/content/${payload.model}/${payload.contentId}/published.json`,
];
await fetch(
`https://api.cloudflare.com/client/v4/zones/${env.ZONE_ID}/purge_cache`,
{
method: "POST",
headers: {
Authorization: `Bearer ${env.CF_API_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ files }),
},
);
return Response.json({ purged: files.length });
},
};If your space uses a content prefix (the default), include its cp_... segment in the content JSON URL. Copy the exact path from the dashboard or the generated client rather than hand-building it.
Because a changed entry also moves derived artifacts (summary manifests, list-index manifests, lookup pointers), either keep those on Posture 2's short TTL while giving published.json and pages the long TTL, or purge by prefix/tag where your plan supports it. The payload's revalidationTags are stable strings designed for tag-based purging and for Next.js revalidateTag.
content.unpublished, content.archived, and content.deleted deliveries carry the URLs that just stopped existing. Purge those too, so the edge doesn't keep serving removed content until the TTL runs out.
Framework-level caching
If your frontend is Next.js, you may not need CDN configuration at all: fetch content with tags from revalidationTags, and let the webhook handler call revalidateTag / revalidatePath. The complete handler is in the Webhooks doc. The generated client also coalesces concurrent identical reads and caches in memory for the life of the process, which is usually enough for build-time consumers.
Summary
| Posture | Staleness | Setup | Use when |
|---|---|---|---|
| Always fresh | none | nothing | default; build-time reads; low traffic |
| Short TTL + SWR | seconds, bounded | one CDN cache rule | request-time reads at volume |
| Purge on publish | none after purge | cache rule + webhook worker | high traffic, instant updates |