Pick fonts, colors, and logos in Drupal — every connected frontend rebuilds with the new tokens automatically.

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-preview page 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     │
 └────────────────────────┘        └────────────────────────────┘
  1. Marketer saves the form → Drupal writes dc_brand.settings config + the cache tag invalidates the JSON endpoint → BrandChangeSubscriber schedules the build hook (default 60s debounce).
  2. Build hook POSTs to Netlify/Vercel → frontend rebuilds.
  3. During build, lib/brand.ts in the starter hits /api/dc-brand/settings once, 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.
  4. CDN serves the new HTML. Every Tailwind class like bg-primary-500, text-accent-700, or font-display resolves against the fresh tokens — no per-component changes.

Usage

On the dashboard

Open the admin menu → Decoupled DrupalBrand 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-preview page
  • 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.ts catches and returns a built-in FALLBACK palette (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-preview all still serve HTTP 200, using the fallback palette and the hardcoded LaunchPad fallback wordmark
  • /dc-config on 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.