Caching
createCache and createKeyedCache — server-side SWR primitives.
The SDK ships two server-side cache primitives. They live in the consumer site's memory (per SSR process) and refresh via the publish webhook. Stale-while-revalidate semantics out of the box.
import { createCache, createKeyedCache } from '@brandfine/client/cache'Single-slot — createCache
For resources that don't vary by any key (workspace metadata).
const workspaceCache = createCache({
label: 'workspace',
ttl: 24 * 60 * 60 * 1000, // 24h safety net
fetch: () => bf.workspace.get(),
})
const ws = await workspaceCache.get()
workspaceCache.invalidate() // called from webhookKeyed — createKeyedCache
For per-key dimensions: per-locale posts, per-nav-key navigations, anything where the same fetcher returns different data for different inputs.
const postsCache = createKeyedCache({
label: 'posts',
ttl: 60 * 60 * 1000,
fetch: (locale) => bf.posts.list({ type: 'blog', locale }),
})
await postsCache.get('en')
await postsCache.get('pt')
postsCache.invalidate('en') // one key
postsCache.invalidate() // all keysThe contract
| Condition | Behavior |
|---|---|
| Fresh (not dirty + within TTL) | Serve cached. |
| Cold start (no data) | Block on fresh fetch. |
| Invalidated (webhook) | Block on fresh fetch — not stale-while-revalidate. |
| TTL-expired with cached data | Stale-while-revalidate. Serve stale + bg refresh. |
| Refetch error with cached data | Log via onError, serve stale (stale-while-error). |
| Concurrent reads during in-flight | Dedupe — all await the same promise. |
The invalidate-blocks-fresh rule is the important one: when the webhook fires, the next page render must see the new data, not the old. Otherwise editors would need to refresh twice to see their changes.
Custom error logger
const cache = createCache({
label: 'workspace',
ttl: 24 * 60 * 60 * 1000,
fetch: () => bf.workspace.get(),
onError: (err) => logger.warn({ err }, 'workspace refetch failed'),
})Defaults to console.warn if omitted.
Caching adapted shapes
When the API's BrandfinePost doesn't match what your page
templates expect, adapt at the cache boundary:
type Service = {
slug: string
title: string
applyUrl: string | null
}
function adapt(post: BrandfinePost<ServiceConfig>): Service {
return {
slug: post.slug,
title: post.title,
applyUrl: post.customConfig?.apply?.url ?? null,
}
}
const servicesCache = createKeyedCache<Service[]>({
label: 'services',
ttl: 60 * 60 * 1000,
fetch: async (locale) =>
(await bf.posts.list({ type: 'services', locale })).map(adapt),
})