Kwisple
Browse the docs

Content and data

Brand kit and privacy data

What client.brand() returns (colours and their pairings, fonts, logos per role, brand elements, voice) and how to use it in a site, plus client.privacy() for the privacy and cookie pages and the client's business details in JSON-LD.

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 null

One 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

FieldTypeMeaning
namestringThe brand's name.
cssstring: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.
fontsHrefstring | nullA Google Fonts stylesheet URL, or null when there are no Google fonts or they are self-hosted.
fontPreloadsstring[]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.
colorsarrayThe colour roles: { role, name, light, dark?, cmyk?, pantone? }.
extraColorsarrayNamed colours beyond the roles: { name, light, dark?, cmyk?, pantone? }. Not in tokens or css.
logosarray{ role, clearSpace, minWidth, asset } per logo.
elementsarrayPatterns, illustrations and icons: { kind, name, asset }.
voiceobjectTagline, 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

RoleDashboardUse
primary, primaryForegroundPrimairmain brand colour and text on it
secondary, secondaryForegroundSecundairsecond colour and text on it
accent, accentForegroundAccentsmall highlights and text on it
backgroundAchtergrondpage background
surfaceOppervlak (kaarten)cards and panels
textTekstbody text
muted, bordersupporting colourssecondary text, lines
success, warning, dangersupporting coloursmessages

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:

TextOn
textbackground
textsurface
mutedbackground
primaryForegroundprimary
secondaryForegroundsecondary
accentForegroundaccent

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):

VariableValue
--brand-<role>each colour role in kebab case: --brand-primary, --brand-primary-foreground, --brand-background, --brand-text, …
--brand-primary-50 … --brand-primary-950tints and shades of the primary colour (500 is the colour itself)
--brand-font-heading, --brand-font-body, --brand-font-monofont stacks
--brand-text-xs … --brand-text-5xlthe type scale, from the base size and scale ratio
--brand-heading-weight, --brand-heading-tracking, --brand-heading-caseheading style
--brand-radius-sm, -md, -lg, -xl, -fullcorner radii
--brand-spacespacing factor (0.85, 1 or 1.2)
--brand-shadow-sm, -md, -lgshadows

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 kitDashboardWhat the site gets
Google fontGoogle 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 fileEigen lettertypebestandOnly the family name, in --brand-font-*. The delivery answer carries no @font-face rule for it: load those files yourself.
Device fontStandaardletter van het toestelA 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

roleDashboardUse
primaryHoofdlogothe full logo; required
markIcoonthe symbol alone (favicon, avatar)
markLightWit icoonthe symbol on dark backgrounds
wordmarkNaam in lettersthe name alone, for wide, low places
monoLightWit logoon photos and dark backgrounds
monoDarkZwart logoprint 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 background

Pick 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=nl

Returns { 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.

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.

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.css in a <style> tag, fontsHref or fontPreloads in <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-open or #cookie-instellingen) when consent is required.
  • Business details as a global set, rendered with organizationJsonLd.

Last updated 2026-10-30