Agency problem: 25 client sites, brand refresh changes primary color
Manual approach:
- Log into 25 WordPress dashboards
- Navigate to Customizer on each site
- Update colors manually
- Check each page
- 6 hours of clicking!!
My approach:
- Update master theme.json file
- Run sync script
- REST API pushes to all 25 sites
- Done in 90 seconds!!
Here's how to automate global styling consistency across multiple WordPress sites:
The Global Styling Problem
Before theme.json:
- Custom CSS scattered across themes
- Inline styles in Customizer
- Plugin-specific style settings
- Zero consistency, maximum chaos!!
With theme.json (but manual):
- Centralized styling... per site
- Still copying styles 50 times
- Client changes = manual updates everywhere
With automation:
- One source of truth
- Push updates to all sites
- Consistent branding across network
- Change once, update everywhere!!
Architecture: Master Config → REST API → All Sites
Component 1: Master theme.json Repository
GitHub repo structure:
global-styles-master/
├── configs/
│ ├── client-a.json # Brand A styles
│ ├── client-b.json # Brand B styles
│ └── default.json # Fallback
├── scripts/
│ ├── sync.js # Push to sites
│ ├── validate.js # Check theme.json
│ └── preview.js # Test locally
└── package.json
Component 2: REST API Endpoints
WordPress Global Styles REST API:
GET /wp-json/wp/v2/global-styles/<id>
POST /wp-json/wp/v2/global-styles/<id>
Component 3: Sync Automation
Node.js script pushes styles via REST API to all sites!!
Step 1: Create Master theme.json
client-a.json (Full Configuration)
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 2,
"settings": {
"color": {
"palette": [
{
"slug": "primary",
"color": "#FF6B35",
"name": "Primary"
},
{
"slug": "secondary",
"color": "#004E89",
"name": "Secondary"
},
{
"slug": "accent",
"color": "#F7C59F",
"name": "Accent"
},
{
"slug": "base",
"color": "#FFFFFF",
"name": "Base"
},
{
"slug": "contrast",
"color": "#1A1A1A",
"name": "Contrast"
}
],
"gradients": [
{
"slug": "primary-to-secondary",
"gradient": "linear-gradient(135deg, #FF6B35 0%, #004E89 100%)",
"name": "Primary to Secondary"
}
]
},
"typography": {
"fontFamilies": [
{
"fontFamily": "\"Inter\", sans-serif",
"slug": "body",
"name": "Body"
},
{
"fontFamily": "\"Montserrat\", sans-serif",
"slug": "heading",
"name": "Heading"
}
],
"fontSizes": [
{
"slug": "small",
"size": "14px",
"name": "Small"
},
{
"slug": "medium",
"size": "16px",
"name": "Medium"
},
{
"slug": "large",
"size": "24px",
"name": "Large"
},
{
"slug": "x-large",
"size": "32px",
"name": "Extra Large"
}
]
},
"spacing": {
"units": ["px", "em", "rem", "vh", "vw", "%"],
"spacingSizes": [
{
"slug": "small",
"size": "1rem",
"name": "Small"
},
{
"slug": "medium",
"size": "2rem",
"name": "Medium"
},
{
"slug": "large",
"size": "4rem",
"name": "Large"
}
]
},
"layout": {
"contentSize": "800px",
"wideSize": "1200px"
}
},
"styles": {
"color": {
"background": "var(--wp--preset--color--base)",
"text": "var(--wp--preset--color--contrast)"
},
"typography": {
"fontFamily": "var(--wp--preset--font-family--body)",
"fontSize": "var(--wp--preset--font-size--medium)",
"lineHeight": "1.6"
},
"elements": {
"h1": {
"typography": {
"fontFamily": "var(--wp--preset--font-family--heading)",
"fontSize": "var(--wp--preset--font-size--x-large)",
"fontWeight": "700"
}
},
"h2": {
"typography": {
"fontFamily": "var(--wp--preset--font-family--heading)",
"fontSize": "var(--wp--preset--font-size--large)",
"fontWeight": "600"
}
},
"link": {
"color": {
"text": "var(--wp--preset--color--primary)"
},
":hover": {
"color": {
"text": "var(--wp--preset--color--secondary)"
}
}
},
"button": {
"color": {
"background": "var(--wp--preset--color--primary)",
"text": "var(--wp--preset--color--base)"
},
"border": {
"radius": "4px"
},
"typography": {
"fontWeight": "600"
}
}
}
}
}
CSS variables automatically generated:
:root {
--wp--preset--color--primary: #FF6B35;
--wp--preset--color--secondary: #004E89;
--wp--preset--font-family--body: "Inter", sans-serif;
/* ... and 20+ more!! */
}
Use anywhere in theme:
.custom-element {
color: var(--wp--preset--color--primary);
font-family: var(--wp--preset--font-family--heading);
}
Step 2: Sync Script (Node.js)
scripts/sync.js
const fs = require('fs');
const axios = require('axios');
// Configuration
const sites = require('../sites.json'); // List of sites
const configFile = process.argv[2] || 'configs/default.json';
console.log(`📦 Loading config: ${configFile}`);
const globalStyles = JSON.parse(fs.readFileSync(configFile, 'utf8'));
async function syncToSite(site) {
console.log(`\n🔄 Syncing to: ${site.url}`);
try {
// Authenticate
const auth = Buffer.from(`${site.username}:${site.app_password}`).toString('base64');
// Get current global styles ID
const stylesResponse = await axios.get(
`${site.url}/wp-json/wp/v2/global-styles`,
{
headers: {
'Authorization': `Basic ${auth}`
}
}
);
if (stylesResponse.data.length === 0) {
console.log('❌ No global styles found');
return;
}
const globalStylesId = stylesResponse.data[0].id;
// Update global styles
const updateResponse = await axios.post(
`${site.url}/wp-json/wp/v2/global-styles/${globalStylesId}`,
{
settings: globalStyles.settings,
styles: globalStyles.styles
},
{
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json'
}
}
);
console.log(`✅ Success! Updated global styles ID ${globalStylesId}`);
// Optionally clear cache
if (site.has_cache_plugin) {
await clearCache(site, auth);
}
} catch (error) {
console.log(`❌ Error: ${error.message}`);
if (error.response) {
console.log(` Response: ${JSON.stringify(error.response.data)}`);
}
}
}
async function clearCache(site, auth) {
try {
// WP Rocket cache clear
await axios.post(
`${site.url}/wp-json/wp-rocket/v1/cache/clear`,
{},
{
headers: { 'Authorization': `Basic ${auth}` }
}
);
console.log('🧹 Cache cleared');
} catch (error) {
console.log('⚠️ Cache clear failed (might not be installed)');
}
}
// Sync to all sites
async function syncAll() {
console.log(`\n🚀 Syncing to ${sites.length} sites...\n`);
for (const site of sites) {
await syncToSite(site);
// Rate limit - wait 1 second between sites
await new Promise(resolve => setTimeout(resolve, 1000));
}
console.log('\n✨ Sync complete!\n');
}
syncAll();
sites.json (Site List)
[
{
"url": "https://clienta.com",
"username": "admin",
"app_password": "xxxx xxxx xxxx xxxx xxxx xxxx",
"has_cache_plugin": true
},
{
"url": "https://clientb.com",
"username": "admin",
"app_password": "yyyy yyyy yyyy yyyy yyyy yyyy",
"has_cache_plugin": true
},
{
"url": "https://clientc.com",
"username": "admin",
"app_password": "zzzz zzzz zzzz zzzz zzzz zzzz",
"has_cache_plugin": false
}
]
Generate Application Passwords:
WordPress Dashboard → Users → Profile → Application Passwords
Create new password for "Global Styles Sync"
Step 3: Run Sync
# Install dependencies
npm install axios
# Sync default config to all sites
node scripts/sync.js configs/default.json
# Output:
# 📦 Loading config: configs/default.json
# 🚀 Syncing to 3 sites...
#
# 🔄 Syncing to: https://clienta.com
# ✅ Success! Updated global styles ID 42
# 🧹 Cache cleared
#
# 🔄 Syncing to: https://clientb.com
# ✅ Success! Updated global styles ID 38
# 🧹 Cache cleared
#
# 🔄 Syncing to: https://clientc.com
# ✅ Success! Updated global styles ID 51
#
# ✨ Sync complete!
25 sites updated in 90 seconds!!
Step 4: WordPress Multisite Automation
For Multisite Networks
Different approach - direct database access:
<?php
/**
* Plugin Name: Global Styles Multisite Sync
* Description: Sync global styles across multisite network
*/
add_action('admin_menu', 'gss_add_admin_menu');
function gss_add_admin_menu() {
if (!is_super_admin()) {
return;
}
add_submenu_page(
'network/settings.php',
'Global Styles Sync',
'Global Styles Sync',
'manage_network',
'global-styles-sync',
'gss_admin_page'
);
}
function gss_admin_page() {
if (isset($_POST['sync_styles'])) {
gss_sync_to_all_sites();
}
?>
<div class="wrap">
<h1>Global Styles Multisite Sync</h1>
<form method="post">
<h2>Master Global Styles (JSON)</h2>
<textarea name="global_styles" rows="20" cols="100"><?php
echo esc_textarea(get_site_option('gss_master_styles', ''));
?></textarea>
<p>
<button type="submit" name="sync_styles" class="button button-primary">
Sync to All Sites in Network
</button>
</p>
</form>
</div>
<?php
}
function gss_sync_to_all_sites() {
$master_styles = json_decode($_POST['global_styles'], true);
if (!$master_styles) {
wp_die('Invalid JSON');
}
// Save master config
update_site_option('gss_master_styles', $_POST['global_styles']);
// Get all sites in network
$sites = get_sites(['number' => 999]);
$synced = 0;
foreach ($sites as $site) {
switch_to_blog($site->blog_id);
// Find global styles post for current theme
$theme_slug = get_stylesheet();
$global_styles_query = new WP_Query([
'post_type' => 'wp_global_styles',
'post_status' => 'publish',
'posts_per_page' => 1,
'no_found_rows' => true,
'meta_query' => [
[
'key' => 'theme',
'value' => $theme_slug
]
]
]);
if ($global_styles_query->have_posts()) {
$global_styles_post = $global_styles_query->posts[0];
// Update post content with new styles
wp_update_post([
'ID' => $global_styles_post->ID,
'post_content' => wp_json_encode([
'version' => 2,
'isGlobalStylesUserThemeJSON' => true,
'settings' => $master_styles['settings'],
'styles' => $master_styles['styles']
])
]);
$synced++;
}
restore_current_blog();
}
echo "<div class='notice notice-success'><p>✅ Synced to {$synced} sites!</p></div>";
}
Step 5: Validation & Preview
Validate theme.json
// scripts/validate.js
const Ajv = require('ajv');
const fs = require('fs');
const ajv = new Ajv();
// Fetch WordPress theme.json schema
const schema = require('./wp-theme-json-schema.json'); // Download from WordPress.org
const configFile = process.argv[2];
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
const validate = ajv.compile(schema);
const valid = validate(config);
if (valid) {
console.log('✅ Valid theme.json!');
} else {
console.log('❌ Invalid theme.json:');
console.log(validate.errors);
process.exit(1);
}
Preview Locally
# Copy to local WordPress theme
cp configs/client-a.json ~/Local\ Sites/test-site/app/public/wp-content/themes/my-theme/theme.json
# Refresh WordPress admin
# Check Appearance → Editor → Styles
Step 6: CI/CD Integration
GitHub Actions Workflow
.github/workflows/sync-styles.yml:
name: Sync Global Styles
on:
push:
branches:
- main
paths:
- 'configs/*.json'
workflow_dispatch:
inputs:
config:
description: 'Config file to sync'
required: true
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm install
- name: Validate theme.json
run: node scripts/validate.js ${{ github.event.inputs.config || 'configs/default.json' }}
- name: Sync to sites
env:
SITES_CONFIG: ${{ secrets.SITES_JSON }}
run: |
echo "$SITES_CONFIG" > sites.json
node scripts/sync.js ${{ github.event.inputs.config || 'configs/default.json' }}
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: '✅ Global styles synced to production'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
Now:
- Edit
configs/client-a.json - Commit and push to GitHub
- GitHub Actions automatically syncs to all sites!!
Real-World Scenarios
Scenario 1: Brand Color Refresh
Client: "Change primary color from #FF6B35 to #E63946"
Old way:
- Log into 25 sites
- Update Customizer on each
- 6 hours
New way:
# Edit configs/client-a.json
sed -i 's/#FF6B35/#E63946/g' configs/client-a.json
# Sync
node scripts/sync.js configs/client-a.json
# Done in 90 seconds!!
Scenario 2: Typography Update
Client: "Switch heading font from Montserrat to Raleway"
{
"fontFamilies": [
{
"fontFamily": "\"Raleway\", sans-serif",
"slug": "heading",
"name": "Heading"
}
]
}
Sync to all sites:
node scripts/sync.js configs/client-a.json
All 25 sites now use Raleway!!
Scenario 3: Add New Color
{
"palette": [
// existing colors...
{
"slug": "success",
"color": "#10B981",
"name": "Success"
}
]
}
Sync → success color available in block editor on all sites!!
Advanced: Per-Site Overrides
Some sites need slight variations:
// In sync.js
const siteOverrides = {
'clienta.com': {
'settings.color.palette[0].color': '#FF0000' // Different primary
}
};
function applyOverrides(styles, siteUrl) {
const overrides = siteOverrides[siteUrl];
if (overrides) {
for (const [path, value] of Object.entries(overrides)) {
setNestedValue(styles, path, value);
}
}
return styles;
}
Monitoring & Rollback
Check Sync Status
// scripts/check-status.js
async function checkAllSites() {
for (const site of sites) {
const response = await axios.get(
`${site.url}/wp-json/wp/v2/global-styles`,
{ headers: { 'Authorization': `Basic ${auth}` } }
);
const currentPrimary = response.data[0].settings.color.palette
.find(c => c.slug === 'primary').color;
console.log(`${site.url}: Primary = ${currentPrimary}`);
}
}
Rollback
Git makes rollback easy:
# Revert to previous commit
git revert HEAD
# Sync old config
node scripts/sync.js configs/client-a.json
Bottom Line
Stop manually updating styles across multiple sites!!
Automation wins:
- One source of truth (theme.json)
- REST API syncs to all sites
- Brand changes in seconds not hours
- Version control with Git
- Update 50 sites in 2 minutes!!
My agency:
- 32 client sites
- Brand refresh used to take 2 days
- Now takes 5 minutes
- Client thinks we're wizards!!
Setup time: 3 hours to build sync system
ROI: Pays for itself on first multi-site brand update!!
For agencies managing multiple sites: This automation is ESSENTIAL!!
Global styles + REST API + automation = styling consistency at scale!! 🎨
This article contains affiliate links!


Top comments (1)
Built similar system last year for agency with 18 sites... absolute lifesaver when client wanted to rebrand. Changed primary color in master theme.json, ran sync script, boom - all sites updated in under 2 minutes. Pro tip: keep sites.json in private repo and use environment variables for app passwords. Also validate theme.json with schema before pushing or you'll push broken styles to production (learned that the hard way lol)