The client's brand is made by people in the dashboard under Merk (brand): colours, fonts,
logos, brand elements and voice. A custom-code site reads it with client.brand(), so a new
colour or logo reaches the site without a code change. The privacy side, the consent
configuration your privacy statement and cookie page describe, comes from client.privacy().
This page covers both, and where the client's business details for structured data come from.
client.brand()
const brand = await client.brand(); // GET /api/v1/brand, or nullOne brand kit per client: every site of the client, staging included, gets the same kit. The
answer is null when the client has no brand kit yet (the API answers 404 with
Deze werkruimte heeft nog geen brand kit.), so always handle that case. The SDK caches it
under kwisple:all and kwisple:brand; saving the brand sends every custom-code site of the
client the webhook with event: "brand.updated" (see Caching).
The Brand shape
| Field | Type | Meaning |
|---|---|---|
name | string | The brand's name. |
css | string | :root { --brand-* } for light, [data-theme="dark"] { … } for dark, plus @font-face rules when the fonts are self-hosted. Put it in a <style> tag. |
fontsHref | string | null | A Google Fonts stylesheet URL, or null when there are no Google fonts or they are self-hosted. |
fontPreloads | string[] | URLs of self-hosted woff2 files worth preloading. Empty otherwise. |
tokens | { light, dark } | The full token objects (below), for code that needs values rather than CSS variables. |
colors | array | The colour roles: { role, name, light, dark?, cmyk?, pantone? }. |
extraColors | array | Named colours beyond the roles: { name, light, dark?, cmyk?, pantone? }. Not in tokens or css. |
logos | array | { role, clearSpace, minWidth, asset } per logo. |
elements | array | Patterns, illustrations and icons: { kind, name, asset }. |
voice | object | Tagline, mission, tone words, do's and don'ts, example sentences, preferred and avoided words, languages, form of address. |
cmyk and pantone are print values exactly as the designer entered them; they are never
computed. asset is a full asset (url, width, height, alt, …), as in content.
Colours and pairings
| Role | Dashboard | Use |
|---|---|---|
primary, primaryForeground | Primair | main brand colour and text on it |
secondary, secondaryForeground | Secundair | second colour and text on it |
accent, accentForeground | Accent | small highlights and text on it |
background | Achtergrond | page background |
surface | Oppervlak (kaarten) | cards and panels |
text | Tekst | body text |
muted, border | supporting colours | secondary text, lines |
success, warning, danger | supporting colours | messages |
Every role has a light value and an optional dark value (the light one is used when it is empty).
The brand kit declares these pairings, and the dashboard checks each one in light and dark mode when the brand is saved:
| Text | On |
|---|---|
text | background |
text | surface |
muted | background |
primaryForeground | primary |
secondaryForeground | secondary |
accentForeground | accent |
A pairing below 3:1 blocks saving; between 3:1 and 4.5:1 it is a warning (fine for large text
only). So when you put text on a brand colour, use these pairings and you inherit WCAG AA: text in
--brand-primary-foreground on --brand-primary, never white text on --brand-accent because it
looked fine for this client. Combinations outside the list are not checked; check them yourself.
CSS variables
css defines these variables on :root (light) and [data-theme="dark"] (dark):
| Variable | Value |
|---|---|
--brand-<role> | each colour role in kebab case: --brand-primary, --brand-primary-foreground, --brand-background, --brand-text, … |
--brand-primary-50 … --brand-primary-950 | tints and shades of the primary colour (500 is the colour itself) |
--brand-font-heading, --brand-font-body, --brand-font-mono | font stacks |
--brand-text-xs … --brand-text-5xl | the type scale, from the base size and scale ratio |
--brand-heading-weight, --brand-heading-tracking, --brand-heading-case | heading style |
--brand-radius-sm, -md, -lg, -xl, -full | corner radii |
--brand-space | spacing factor (0.85, 1 or 1.2) |
--brand-shadow-sm, -md, -lg | shadows |
The starters map them onto Tailwind in globals.css, with fallbacks for a site without a kit:
@theme inline {
--color-primary: var(--brand-primary, #1d4ed8);
--color-primary-foreground: var(--brand-primary-foreground, #ffffff);
--color-background: var(--brand-background, #ffffff);
--color-text: var(--brand-text, #0f172a);
--font-heading: var(--brand-font-heading, ui-sans-serif, system-ui, sans-serif);
--font-body: var(--brand-font-body, ui-sans-serif, system-ui, sans-serif);
--radius-brand: var(--brand-radius-md, 0.5rem);
}Then bg-primary text-primary-foreground follows the brand. Never hard-code a brand colour in
the site.
Fonts
| Source in the kit | Dashboard | What the site gets |
|---|---|---|
| Google font | Google Fonts (gratis) | fontsHref: one CSS2 URL with the declared weights and display=swap. The --brand-font-* stack also names a metric-adjusted local fallback so the swap does not shift the layout. |
| Google font, self-hosted copy | (automatic) | When the platform has synced copies of the kit's Google fonts on brand save, fontsHref is null, css contains the @font-face rules and a fallback face, and fontPreloads lists the woff2 files to preload. |
| Uploaded font file | Eigen lettertypebestand | Only the family name, in --brand-font-*. The delivery answer carries no @font-face rule for it: load those files yourself. |
| Device font | Standaardletter van het toestel | A system font stack. |
In <head>:
{brand?.fontPreloads?.map((href) => (
<link key={href} rel="preload" href={href} as="font" type="font/woff2" crossOrigin="anonymous" />
))}
{brand?.fontsHref ? (
<>
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<link rel="stylesheet" href={brand.fontsHref} />
</>
) : null}
{brand ? <style>{brand.css}</style> : null}In Astro, <BrandHead brand={brand} /> from @kwisple/sdk/astro/components renders exactly this.
Logos
role | Dashboard | Use |
|---|---|---|
primary | Hoofdlogo | the full logo; required |
mark | Icoon | the symbol alone (favicon, avatar) |
markLight | Wit icoon | the symbol on dark backgrounds |
wordmark | Naam in letters | the name alone, for wide, low places |
monoLight | Wit logo | on photos and dark backgrounds |
monoDark | Zwart logo | print and light backgrounds |
clearSpace is the free space to keep around the logo, as a fraction of the logo's height
(0.5 = half the height on each side). minWidth is the smallest width in pixels. Respect both
in your layout:
const byRole = (role: string) => brand?.logos.find((l) => l.role === role) ?? null;
const headerLogo = byRole("primary");
const footerLogo = byRole("monoLight") ?? byRole("primary"); // footer on a dark backgroundPick a role by background: monoLight or markLight on dark sections, fall back to primary
when the kit has no such logo.
Brand elements
elements holds the brand's patterns, illustrations and icons (kind: pattern,
illustration, icon, other), each with a name and an asset. Single-colour SVGs recolour
well: the platform's social templates set them in the right brand colour per background. Use
patterns as decoration and never let them run through text or the logo.
Voice
voice is for code that writes or checks copy (tagline, mission, toneWords, dos,
donts, exampleSentences, preferredWords, avoidWords, languages, addressForm "je" or
"u"). A site rarely needs it beyond perhaps the tagline.
client.privacy()
const privacy = await client.privacy("nl"); // GET /api/v1/consent?locale=nlReturns { consent, analytics }: the consent configuration the agency set under
Website → Instellingen → Cookies & privacy and the statistics settings. The field reference,
the banner components and the statistics script are on
Statistics and consent. This section is about the pages around it.
The privacy statement
The privacy statement is content, written by the client or the agency. Model it as a single
section, as RANSON.N does with privacy (a label, a bijgewerkt date line and an inhoud
matrix of rich-text and table blocks, because rich text has no tables). Then set
Link naar de privacyverklaring under Cookies & privacy to its path (for example
/privacy): it becomes consent.privacyUrl, and the banner and the settings link to it.
The cookie overview
Generate the list of services and cookies from the configuration, so it always matches the banner:
import type { PrivacyInfo } from "@kwisple/sdk";
export function CookieTable({ consent }: { consent: PrivacyInfo["consent"] }) {
return consent.categories.map((cat) => (
<section key={cat.id}>
<h3>{cat.label}</h3>
<p>{cat.description}</p>
<table>
<thead>
<tr>
<th>{consent.texts.colName}</th>
<th>{consent.texts.colRetention}</th>
<th>{consent.texts.colPurpose}</th>
</tr>
</thead>
<tbody>
{cat.services.flatMap((s) =>
s.cookies.map((c) => (
<tr key={`${s.id}-${c.name}`}>
<td>{c.name} ({s.name})</td>
<td>{c.retention}</td>
<td>{c.purpose || s.purpose}</td>
</tr>
)),
)}
</tbody>
</table>
</section>
));
}Texts and retention are already in the requested locale.
The cookie settings link
Every page needs a way to change or withdraw consent when the site asks for it. Any element with
data-consent-open, or a link to #cookie-instellingen, opens the settings once
ConsentProvider (React) or <Consent> (Astro) is on the page. In rich text an editor can link
to #cookie-instellingen; RANSON.N's rich-text component turns that link into the settings
button. The Astro starter's footer shows Cookie-instellingen only when
privacy.consent.required.
Business details in JSON-LD
Search engines and AI answers read the business's name, address, phone, VAT number and opening hours from structured data. The delivery API does not expose the client's account details, so a custom-code site declares them as a global set and editors fill them in under Info op elke pagina. RANSON.N's global set is called Bedrijfsgegevens:
defineGlobalSet({
handle: "bedrijf",
name: "Bedrijfsgegevens",
icon: "building",
fields: {
naam: f.plainText({ label: "Bedrijfsnaam", translation: "none" }),
telefoon: f.plainText({ label: "Telefoon", translation: "none" }),
email: f.email({ label: "E-mail", translation: "none" }),
adres: f.plainText({ label: "Adres", multiline: true, translation: "none" }),
btw: f.plainText({ label: "Btw-nummer", translation: "none" }),
},
});Build the JSON-LD with the SDK's builders and the brand's logo:
import { JsonLd, organizationJsonLd } from "@kwisple/sdk/next";
const [site, brand, { bedrijf }] = await Promise.all([
client.site(),
client.brand(),
client.globals(),
]);
const origin = site.siteUrl.replace(/\/+$/, "");
<JsonLd
data={organizationJsonLd({
type: "Electrician", // a LocalBusiness subtype, or leave out for Organization
origin,
name: bedrijf.naam || site.name,
logo: brand?.logos.find((l) => l.role === "primary")?.asset.url,
telephone: bedrijf.telefoon || undefined,
email: bedrijf.email || undefined,
vatID: bedrijf.btw || undefined,
// RANSON.N splits its multi-line `adres` field into these parts
address: { street: "Voorbeeldstraat 12", postalCode: "8800", locality: "Roeselare" },
})}
/>;organizationJsonLd takes type, origin, name, description, logo, image,
telephone, email, vatID, address (street, postalCode, locality, region,
country, default BE), openingHours ({ days, opens, closes }[], with schemaDay(1…7) for
day names), areaServed, sameAs and priceRange, and sets @id to <origin>/#organization.
RANSON.N parses its multi-line address field into street, postal code and locality and builds the
opening hours from a second global set (src/lib/structured-data.tsx). Fill in the real details
before going live: canonical URLs, JSON-LD, the sitemap and robots all use the Site-URL.
Checklist
- Handle
brand === null. brand.cssin a<style>tag,fontsHreforfontPreloadsin<head>(or<BrandHead>).- Colours only through
--brand-*; text on a brand colour with its declared foreground. - Logos by role, with clear space and minimum width.
- A privacy statement page, linked under Cookies & privacy.
- A cookie overview generated from
privacy.consent.categories. - A settings link (
data-consent-openor#cookie-instellingen) when consent is required. - Business details as a global set, rendered with
organizationJsonLd.