Brand Settings
Give your site builder one screen to pick the fonts, colors, and logos that render across your frontend. Every token lives in Drupal config, is served as JSON at /api/dc-brand/settings, and is consumed by the Astro and Next.js starters as CSS custom properties + Tailwind theme tokens — so a rebrand is a save + reload, not a code change.
What's configurable
- Typography — two Google Font families (heading + body). Picker surfaces 89 curated families spanning sans, serif, display, mono, and handwriting categories. Each family loads with
display=swap+ preconnect hints for fast first paint. - Colors — five semantic slots:
primary,secondary,accent,neutral,background. Native color pickers with a live 9-stop HSL ramp (50–900) rendered below each, so you can see the full scale the frontend will use. - Palette presets — eight one-click starter palettes (Ocean, Midnight, Sunset, Forest, Candy, Minimal, Enterprise, Terracotta) that populate all five slots simultaneously.
- Logos — light-mode and dark-mode uploads. PNG, SVG, JPG, or WebP. Light is used in headers on light backgrounds, dark in dark-themed footers.
- Live preview — optional iframe pane that embeds your frontend's
/brand-previewpage inside the admin so you can see the deployed render without leaving Drupal. - Build hook — optional Netlify or Vercel webhook URL that fires on save (after a configurable debounce window) to rebuild your static frontend.
How it's wired
Drupal space Frontend (Astro or Next.js)
┌────────────────────────┐ ┌────────────────────────────┐
│ /admin/config/brand │ │ lib/brand.ts │
│ — form saves config │ │ fetch at build time │
│ │ ───► │ ├ injects :root CSS vars │
│ /api/dc-brand/settings │ │ └ extends Tailwind theme │
│ — resolved JSON │ │ │
│ │ │ RootLayout / BaseLayout │
│ Debounced build hook │ ───► │ <style> block + <link> │
│ — POSTs on save │ │ preconnect to Google │
└────────────────────────┘ └────────────────────────────┘
- Marketer saves the form → Drupal writes
dc_brand.settingsconfig + the cache tag invalidates the JSON endpoint →BrandChangeSubscriberschedules the build hook (default 60s debounce). - Build hook POSTs to Netlify/Vercel → frontend rebuilds.
- During build,
lib/brand.tsin the starter hits/api/dc-brand/settingsonce, caches the payload for the build, injects<style>:root { --brand-primary-500: …; --brand-font-heading: …; }</style>into the root layout, and emits<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=…">for the chosen Google Fonts. - CDN serves the new HTML. Every Tailwind class like
bg-primary-500,text-accent-700, orfont-displayresolves against the fresh tokens — no per-component changes.
Usage
On the dashboard
Open the admin menu → Decoupled Drupal → Brand settings, or click the "Brand" tile on /dc-config. A toolbar shortcut is added automatically so it's one click from any admin page.
The form is organized as four vertical tabs:
- Typography — pick heading + body families, with a live Google Fonts sample rendered under each dropdown
- Colors — palette presets on top, five color pickers below, each with its computed 9-stop HSL ramp
- Logos — drag-and-drop upload for light and dark variants, thumbnails render inline
- Live preview — configure the frontend URL and get an embedded iframe of your running
/brand-previewpage - Build hook — webhook URL + debounce window (seconds)
On the frontend
Both starters (Astro and Next.js) ship the integration out of the box. Environment variable:
# .env.local
DRUPAL_BASE_URL=https://your-space.decoupled.website
# or for Next.js:
NEXT_PUBLIC_DRUPAL_BASE_URL=https://your-space.decoupled.website
On dev start, lib/brand.ts fetches on every request (cache bypassed) so Drupal edits surface on reload. On production builds, the fetch happens once per build, payload caches for the build's lifetime, resolved values bake into static HTML.
Using the tokens in your components
Every color and font is exposed as a Tailwind utility that reads the CSS var set by the brand config. No import, no context:
// Buttons rebrand automatically — nothing to change here.
<button className="bg-primary-500 hover:bg-primary-600 text-white">
Get Started
</button>
<h1 className="font-display text-4xl text-neutral-900">
Ship Faster
</h1>
<div className="bg-accent-50 border border-accent-200 text-accent-900 p-6 rounded-xl">
Highlighted section
</div>
Available token prefixes:
| Prefix | Resolves to |
|---|---|
bg-primary-{50..900}, text-primary-{50..900}, border-primary-{50..900} |
hsl(var(--brand-primary-{stop})) |
bg-secondary-{50..900} |
hsl(var(--brand-secondary-{stop})) |
bg-accent-{50..900} |
hsl(var(--brand-accent-{stop})) |
bg-neutral-{50..900} |
hsl(var(--brand-neutral-{stop})) |
bg-surface-{50..900} |
hsl(var(--brand-background-{stop})) |
font-sans |
var(--brand-font-body), ui-sans-serif, … |
font-display |
var(--brand-font-heading), ui-sans-serif, … |
Alpha modifiers work: bg-primary-500/20, text-accent-700/80, etc.
The /brand-preview page
Both starters ship a /brand-preview page that renders every resolved token — font samples, 9-stop color swatches, logos on light + dark backgrounds, and a Tailwind-class sampler. Useful for:
- Verifying a rebrand visually before deploying more broadly
- Configuring as the iframe target in the admin's Live Preview tab
- Quick debug of what values the frontend actually received
The JSON endpoint
Consumed by the starters, but also available for any other frontend you want to wire up.
Request
GET https://your-space.decoupled.website/api/dc-brand/settings
No authentication required — the endpoint is public.
Response shape
{
"fonts": {
"heading": {
"family": "Inter",
"href": "https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400;0,500;0,600;0,700;0,800&display=swap",
"weights": [400, 500, 600, 700, 800],
"category": "sans"
},
"body": { … }
},
"colors": {
"primary": {
"hex": "#3b82f6",
"scale": {
"50": { "h": 217, "s": 91.2, "l": 97, "css": "217 91.2% 97%" },
"100": { "h": 217, "s": 91.2, "l": 93, "css": "217 91.2% 93%" },
"500": { "h": 217, "s": 91.2, "l": 59.8, "css": "217 91.2% 59.8%" },
"900": { "h": 217, "s": 91.2, "l": 15, "css": "217 91.2% 15%" }
}
},
"secondary": { … }, "accent": { … }, "neutral": { … }, "background": { … }
},
"logos": {
"light": {
"url": "https://your-space.decoupled.website/sites/default/files/brand/logo-light.svg",
"alt": "logo-light.svg"
},
"dark": null
}
}
Cache behavior
The response is tagged with the dc_brand.settings config cache tag. Any save (via the form, drush cset, API, or the MCP tools later) auto-invalidates Drupal's page cache. Consumers get fresh values on the next fetch without needing a manual drush cr.
The response also sets max-age=60 + Access-Control-Allow-Origin: * so browser and CDN caches expire quickly and cross-origin frontends can fetch without CORS config.
What happens when the module is disabled
All integration code in the starters has a safe fallback:
- The endpoint returns 404 →
lib/brand.tscatches and returns a built-inFALLBACKpalette (same hexes as the module's install defaults) - One warning logs per process:
[dc_brand] module not enabled on <url> — rendering with default brand tokens - Header / Footer /
/brand-previewall still serve HTTP 200, using the fallback palette and the hardcoded LaunchPad fallback wordmark /dc-configon the Drupal side hides the "Brand" card when dc_brand is uninstalled
So your frontend never breaks because someone turned off the module.
Installation
Every space provisioned from the current dc_core install profile has dc_brand enabled by default. Existing tenants pick it up on the next drush updatedb via dc_core_update_10024.
To verify:
drush pm:list --status=enabled | grep dc_brand
curl -s https://your-space.decoupled.website/api/dc-brand/settings | jq '.colors.primary.hex'
If both return a result, you're wired up.
Related
- Getting Started — set up your first space
- Deployment — configure the Netlify/Vercel build hook that pairs with the Brand form
- Visual Page Builder — the other half of the marketer self-serve story