createBrandfineClient
The SDK's entry point. Typed HTTP client over the /external/* surface.
import { createBrandfineClient } from '@brandfine/client'
const bf = createBrandfineClient({
baseUrl: 'https://api.brandfine.co',
apiKey: process.env.BRANDFINE_API_KEY!,
})Returns a stateless, multi-instance-safe handle. Construct one
per (baseUrl, apiKey) pair — multi-tenant consumers create
several side-by-side.
Options
| Option | Type | Required | Notes |
|---|---|---|---|
baseUrl | string | ✓ | No trailing slash (the client trims one if present). |
apiKey | string | ✓ | Workspace-scoped key. |
fetch | typeof fetch | — | Override for tests / edge runtimes / tracing wrappers. |
userAgent | string | — | Defaults to @brandfine/client. |
Empty baseUrl or apiKey throws at construction — fail loud,
not silent.
Methods
bf.posts.list({ type, locale, forceLimit }) // paginated, returns flat array
bf.posts.getBySlug(slug) // null on 404
bf.workspace.get<TCustom, TSchema>() // narrow customConfig + schemaOrg
bf.navigations.get(key) // null on 404
bf.categories.list({ locale })
bf.analytics.install(opts?) // inject the tracker, see /docs/sdk/analytics
bf.submissions.create(input) // POST a contact-form submission
bf.get<T>(path, opts?) // escape hatch for new endpointsError handling
Non-2xx responses (except 404 on nullable404 endpoints) throw
BrandfineApiError:
import { BrandfineApiError } from '@brandfine/client'
try {
await bf.posts.list()
} catch (err) {
if (err instanceof BrandfineApiError) {
console.error(err.status, err.statusText, err.body, err.url)
if (err.status === 429) await sleep(1000) // backoff
}
throw err
}publishedAt for sitemap <lastmod>
Every BrandfinePost carries a publishedAt ISO timestamp. It's
the time of the last publish that changed content — re-stamped
on every republish with a new contentHash, untouched on no-op
republishes. Use it directly for sitemap entries:
const posts = await bf.posts.list({ type: 'services', locale: 'en' })
const sitemap = posts.map((p) => ({
url: `https://example.com/services/${p.slug}`,
lastmod: p.publishedAt,
}))The singular bf.posts.getBySlug(slug) response includes a
translations[] array with per-locale publishedAt, so you can
compute a cross-locale lastmod when one URL represents the whole
article family:
const post = await bf.posts.getBySlug('schengen-visa')
if (post) {
const familyLastmod = post.translations
?.reduce((max, t) => (t.publishedAt > max ? t.publishedAt : max),
post.publishedAt) ?? post.publishedAt
}Draft edits don't affect publishedAt — only publishes do — so
this is also the right field for "fresh content" badges or
chronological feeds, not just sitemaps.
Typed customConfig
When you have a known shape for customConfig (per post type),
parameterize the methods:
type ServiceConfig = {
apply: { visaSlug: string; destinationCode: string } | null
faq: Array<{ question: string; answer: string }>
}
const services = await bf.posts.list<ServiceConfig>({ type: 'services' })
// services[0].customConfig is now typed as ServiceConfig | nullThe default is unknown — opt into typing incrementally as the
consumer's needs solidify.