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>
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
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:
- Brand drift – UI colors were scattered across dozens of components.
- 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'
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
{
"primary": "#0d47a1",
"secondary": "#ff9800",
"background": "#f5f7fa",
"text": "#212121"
}
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
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;
}
3. Refactor components to use the hook
Before
// apps/web/src/app/_components/CrmShell.tsx
<div className="crm-shell" style={{ backgroundColor: '#1a73e8' }}>
{children}
</div>
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>
);
}
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>;
}
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);
}
And a small script that injects the variables into the document head (executed once on page load):
apps/web/src/lib/injectBrandCssVars.ts
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);
});
}
I call injectBrandCssVars() inside pages/_app.tsx:
import { injectBrandCssVars } from '@/lib/injectBrandCssVars';
function MyApp({ Component, pageProps }: AppProps) {
useEffect(() => {
injectBrandCssVars();
}, []);
return <Component {...pageProps} />;
}
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": {
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)