DEV Community

Cover image for Automating Global Styling Consistency Across WordPress Sites
Martijn Assie
Martijn Assie

Posted on

Automating Global Styling Consistency Across WordPress Sites

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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"
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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!! */
}
Enter fullscreen mode Exit fullscreen mode

Use anywhere in theme:

.custom-element {
  color: var(--wp--preset--color--primary);
  font-family: var(--wp--preset--font-family--heading);
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
  }
]
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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>";
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

Now:

  1. Edit configs/client-a.json
  2. Commit and push to GitHub
  3. 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!!
Enter fullscreen mode Exit fullscreen mode

Scenario 2: Typography Update

Client: "Switch heading font from Montserrat to Raleway"

{
  "fontFamilies": [
    {
      "fontFamily": "\"Raleway\", sans-serif",
      "slug": "heading",
      "name": "Heading"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Sync to all sites:

node scripts/sync.js configs/client-a.json
Enter fullscreen mode Exit fullscreen mode

All 25 sites now use Raleway!!

Scenario 3: Add New Color

{
  "palette": [
    // existing colors...
    {
      "slug": "success",
      "color": "#10B981",
      "name": "Success"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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}`);
    }
}
Enter fullscreen mode Exit fullscreen mode

Rollback

Git makes rollback easy:

# Revert to previous commit
git revert HEAD

# Sync old config
node scripts/sync.js configs/client-a.json
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
martijn_assie_12a2d3b1833 profile image
Martijn Assie

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)