Brand layout
AgencySiteKitLayout wraps your site in your agency's brand. It fetches the
colours, fonts and radii you set in Sonor and publishes them as --sk-* CSS
variables, in light and dark, so your case studies (and the rest of your
site) can style themselves from one source. It also hands site-kit's client
modules the short-lived token they need.
Add it to your root layout
// app/layout.tsx
import { AgencySiteKitLayout } from '@sonordev/agency-site-kit';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AgencySiteKitLayout>{children}</AgencySiteKitLayout>
</body>
</html>
);
}It's an async server component, exported from the root entry and from
@sonordev/agency-site-kit/layout. On the server it fetches your brand with
getPortfolioBrandConfig (cached for an
hour, or until Sonor revalidates the portfolio tag, so a brand edit shows up
with your case studies) and mints the client token.
Props
| Prop | Default | What it does |
|---|---|---|
children | Your page | |
apiKey | SONOR_API_KEY | The key to fetch the brand with and to mint the client token from |
apiUrl | SONOR_API_URL, else https://api.sonor.io | The API origin |
brandConfig | Fetched from Sonor | A Partial<BrandConfig> to use instead of fetching. Missing fields fall back to the defaults. |
theme | Follows the visitor's system | 'light' or 'dark' forces one, by setting data-theme on the wrapper |
className | A class for the wrapper <div> | |
credential | true | Mint a short-lived client token for site-kit's client modules. See below. |
showLlmsTxtFooterLink | false | Render a hidden footer link to /llms.txt after your content |
What it renders
In order:
- A
preconnectand adns-prefetchhint for the Sonor API's origin. - A
<style data-agency-site-kit="brand">with the CSS variables. - The client token for site-kit's client modules (when
credentialis on and a key is set). It renders nothing visible and never wraps your content. - A
<div data-agency-site-kit="root">around your content, withdata-themewhen you passtheme, yourclassName, and inlinefont-family,colorandbackground-colorfrom the variables.
The CSS variables
| Variable | From BrandConfig |
|---|---|
--sk-primary, --sk-primary-rgb | primary |
--sk-secondary, --sk-secondary-rgb | secondary |
--sk-bg | background |
--sk-bg-elevated | backgroundElevated |
--sk-surface | surface |
--sk-surface-hover | surfaceHover |
--sk-border | surfaceBorder |
--sk-text-primary | textPrimary |
--sk-text-secondary | textSecondary |
--sk-text-tertiary | textTertiary |
--sk-radius-sm, --sk-radius, --sk-radius-lg | radius.sm, radius.md, radius.lg |
--sk-font-heading | fontHeading |
--sk-font | fontBody |
The -rgb variables hold R, G, B, for colours with transparency:
.card {
background: var(--sk-surface);
border: 1px solid var(--sk-border);
border-radius: var(--sk-radius-lg);
box-shadow: 0 12px 40px rgba(var(--sk-primary-rgb), 0.15);
}Light and dark
The light values go on :root and [data-theme="light"], the dark values on
[data-theme="dark"], and a prefers-color-scheme: dark rule applies the
dark values to :root unless it's marked data-theme="light". So by default
the site follows the visitor's system, and theme (or your own data-theme
attribute) pins it.
Dark mode changes only the colours. Its values come from your brand's
darkMode overrides, then the built-in dark defaults (DEFAULT_DARK_MODE).
Your primary and secondary carry over unless darkMode sets its own.
If Sonor can't be reached or has no brand set, the layout uses the built-in
light defaults (DEFAULT_BRAND_CONFIG) rather than failing the page. Any
field your brand leaves out falls back to those defaults too.
The client token
site-kit's client modules (analytics, forms, chat, booking) need a credential
in the browser, and your SONOR_API_KEY has to stay on the server. So the
layout mints a short-lived token from the key on the server and publishes that
instead, the same way site-kit's own SiteKitLayout does. That's why you
don't need a NEXT_PUBLIC_ copy of your key.
Mount those modules as siblings of your content, not wrappers around it, so your pages stay server-rendered. See site-kit's layout docs and analytics docs.
With site-kit's SiteKitLayout
If your site also mounts SiteKitLayout from @sonordev/site-kit, it
publishes its own token. Two publishers would race, so turn this one off:
// app/layout.tsx
import { SiteKitLayout } from '@sonordev/site-kit/layout';
import { AgencySiteKitLayout } from '@sonordev/agency-site-kit';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<SiteKitLayout>
<AgencySiteKitLayout credential={false}>{children}</AgencySiteKitLayout>
</SiteKitLayout>
</body>
</html>
);
}Only set credential={false} when something else publishes the token.
Without one, site-kit's client modules can't authenticate.
Brand utilities
The root entry exports the pieces the layout is built from, for when you need the CSS somewhere else:
| Export | What it does |
|---|---|
generateBrandCSS(config) | The full stylesheet above (light, dark and the system-preference rule) as a string. Missing fields fall back to the defaults. |
mergeBrandConfig(partial) | A complete BrandConfig from a partial one, over the defaults |
hexToRgb(hex) | '#6366f1' to '99, 102, 241'. Takes 3 or 6 hex digits, returns null for anything else. |
DEFAULT_BRAND_CONFIG | The light defaults: an indigo primary, white background, Inter, radii of 0.375rem, 0.5rem and 0.75rem |
DEFAULT_DARK_MODE | The dark colour defaults |
import { generateBrandCSS } from '@sonordev/agency-site-kit';
import { getPortfolioBrandConfig } from '@sonordev/agency-site-kit/portfolio/server';
const css = generateBrandCSS(await getPortfolioBrandConfig());That's the same fetch the layout makes, from the same cache entry, so the two
always agree, including on the DEFAULT_BRAND_CONFIG fallback. See
Fetching.
The llms.txt link
showLlmsTxtFooterLink renders a visually hidden <footer> with a link to
/llms.txt after your content. The better way to point AI crawlers at your
llms.txt is a response header: see
site-kit's llms.txt docs.