DEV Community

Roberto Luna
Roberto Luna

Posted on

Injecting Dynamic Brand Colors Across a Next.js CRM with a Central BrandingInjector

Injecting Dynamic Brand Colors Across a Next.js CRM with a Central BrandingInjector

TL;DR: I replaced hundreds of inline color values in PlayaMXCRM with a single, runtime‑driven BrandingInjector that pulls brand tokens from a JSON file. The change eliminated UI drift and fixed a JSON‑pipeline crash that was blocking our weekly content‑automation job.


The Problem

Our CRM (a Next.js monorepo under apps/web) started looking like a patchwork quilt after a sprint of quick UI tweaks. Every component had hard‑coded hex values:

// apps/web/src/app/_components/Sidebar.tsx
<div style={{ backgroundColor: '#1a73e8', color: '#fff' }}>
  Sales
</div>
Enter fullscreen mode Exit fullscreen mode

The brand team released a new primary color (#0d47a1) and asked us to roll it out everywhere. The symptom was obvious: the navigation bar still used the old gray, the buttons showed mixed blues, and the weekly‑automation pipeline that builds our markdown newsletters crashed with:

Error: Unexpected token } in JSON at position 124
    at JSON.parse (<anonymous>)
    at src/lib/contentGenerator.js:45:17
Enter fullscreen mode Exit fullscreen mode

The JSON file (content/2026/09/05/VS/metadata.json) that drives the automation had been edited manually, and a stray comma introduced the syntax error. The crash prevented the devto and bluesky posts from being published on 2026‑09‑06.

So we had two intertwined problems:

  1. Brand drift – UI colors were scattered across dozens of components.
  2. Automation breakage – A malformed JSON file stopped the content pipeline.

Both needed a systematic fix.


What I Tried First

My first instinct was to search‑and‑replace every hex code with the new brand value using a regex across the repo:

git grep -l '#1a73e8' | xargs sed -i '' 's/#1a73e8/#0d47a1/g'
Enter fullscreen mode Exit fullscreen mode

That worked for the obvious places, but:

  • It missed colors defined in CSS modules (.module.css).
  • It introduced a typo in one component (#0d47a1; with a trailing semicolon) that caused a runtime warning.
  • Most importantly, the change was static – any future brand update would require another massive sweep.

I also tried to fix the JSON error by manually editing metadata.json, but the file is regenerated nightly by the content‑automation script, so my manual fix was overwritten on the next run.

Both approaches were dead ends. I needed a single source of truth for brand colors and a way to keep the automation pipeline resilient.


The Implementation

1. Central token store

I created a JSON file that lives next to the design system:

apps/web/src/styles/brandTokens.json
Enter fullscreen mode Exit fullscreen mode
{
  "primary": "#0d47a1",
  "secondary": "#ff9800",
  "background": "#f5f7fa",
  "text": "#212121"
}
Enter fullscreen mode Exit fullscreen mode

2. BrandingInjector (runtime helper)

A tiny library that reads the token file once (server‑side) and exposes a hook for components:

apps/web/src/lib/brandingInjector.ts
Enter fullscreen mode Exit fullscreen mode
import { useEffect, useState } from 'react';
import tokens from '@/styles/brandTokens.json';

type BrandTokens = typeof tokens;

/**
 * Returns the current brand palette.
 * In a real‑world scenario we could fetch this from a remote CMS.
 */
export function useBrandTokens(): BrandTokens {
  const [palette, setPalette] = useState<BrandTokens>(tokens);

  // Example: hot‑reload tokens in dev mode
  if (process.env.NODE_ENV === 'development') {
    useEffect(() => {
      const handler = () => setPalette(tokens);
      if (module.hot) {
        module.hot.accept('@/styles/brandTokens.json', handler);
      }
      return () => {
        if (module.hot) {
          module.hot.removeAcceptHandler('@/styles/brandTokens.json', handler);
        }
      };
    }, []);
  }

  return palette;
}
Enter fullscreen mode Exit fullscreen mode

3. Refactor components to use the hook

Before

// apps/web/src/app/_components/CrmShell.tsx
<div className="crm-shell" style={{ backgroundColor: '#1a73e8' }}>
  {children}
</div>
Enter fullscreen mode Exit fullscreen mode

After

// apps/web/src/app/_components/CrmShell.tsx
import { useBrandTokens } from '@/lib/brandingInjector';

export default function CrmShell({ children }: { children: React.ReactNode }) {
  const { primary, background } = useBrandTokens();

  return (
    <div
      className="crm-shell"
      style={{
        backgroundColor: primary,
        color: background,
      }}
    >
      {children}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

All UI components that previously used hard‑coded colors were updated similarly. The diff for CrmShell.tsx looks like this:

@@ -3,7 +3,9 @@ import React from 'react';
-import './CrmShell.css';
+import './CrmShell.css';
+import { useBrandTokens } from '@/lib/brandingInjector';

 export default function CrmShell({ children }) {
-  return <div className="crm-shell" style={{ backgroundColor: '#1a73e8' }}>{children}</div>;
+  const { primary } = useBrandTokens();
+  return <div className="crm-shell" style={{ backgroundColor: primary }}>{children}</div>;
 }
Enter fullscreen mode Exit fullscreen mode

4. Updating the CSS module fallback

Some components still rely on CSS modules. I added CSS custom properties that the injector writes at runtime:

/* apps/web/src/app/_components/Button.module.css */
.button {
  background-color: var(--brand-primary);
  color: var(--brand-text);
}
Enter fullscreen mode Exit fullscreen mode

And a small script that injects the variables into the document head (executed once on page load):

apps/web/src/lib/injectBrandCssVars.ts
Enter fullscreen mode Exit fullscreen mode
import tokens from '@/styles/brandTokens.json';

export function injectBrandCssVars() {
  const root = document.documentElement;
  Object.entries(tokens).forEach(([key, value]) => {
    root.style.setProperty(`--brand-${key}`, value);
  });
}
Enter fullscreen mode Exit fullscreen mode

I call injectBrandCssVars() inside pages/_app.tsx:

import { injectBrandCssVars } from '@/lib/injectBrandCssVars';

function MyApp({ Component, pageProps }: AppProps) {
  useEffect(() => {
    injectBrandCssVars();
  }, []);
  return <Component {...pageProps} />;
}
Enter fullscreen mode Exit fullscreen mode

5. Fixing the content‑automation pipeline

The pipeline reads metadata.json to generate markdown for Dev.to, Bluesky, Substack, etc. The previous crash was caused by a stray comma after "devto_url":

@@ -30,7 +30,7 @@
   "devto_url": "https://dev.to/zaerohell/injecting-dynamic-brand-colors-across-a-nextjs-crm-with-a-central-brandinginjector-3589",
-  "craft_doc_ids": {
+  "craft_doc_ids": {
Enter fullscreen mode Exit fullscreen mode

I added a lint step (npm run lint:json) that runs before the nightly


Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.

Repo: zaerohell/content-automation · 2026-09-06

#playadev #buildinpublic

Top comments (0)