DEV Community

Cover image for Version-Controlling WordPress Customizer Settings With Git
Martijn Assie
Martijn Assie

Posted on

Version-Controlling WordPress Customizer Settings With Git

Developer switches to new laptop: "Wait, where are all the customizer settings from yesterday??"

The settings: Trapped in database, zero version control, changes completely lost!!

My workflow now: Customizer settings auto-export to JSON files, committed to Git!!

  • Every save = JSON file updated
  • Full Git history of color changes, typography tweaks, layout adjustments
  • Deploy settings with code: git pull && wp customizer import
  • Never lose settings again!!

Here's the complete technical implementation:

The Problem With Customizer Settings

Current WordPress workflow:

Developer changes header color → Saved to wp_options table
Enter fullscreen mode Exit fullscreen mode

Problems:

  • No version history
  • Can't see what changed
  • Can't revert easily
  • Can't deploy with code
  • Lost if database corrupts
  • Completely disconnected from Git workflow!!

What we need:

Developer changes header color → JSON file updated → Git commit → Deploy everywhere
Enter fullscreen mode Exit fullscreen mode

Architecture: Database to Filesystem Sync

Core Concept

<?php
/**
 * Customizer Settings Version Control
 * 
 * Syncs theme_mods to JSON files for Git tracking
 */

class Customizer_Git_Sync {

    private $settings_dir;
    private $theme_slug;

    public function __construct() {
        $this->theme_slug = get_stylesheet();

        // Store in theme directory for Git tracking
        $this->settings_dir = get_stylesheet_directory() . '/customizer-settings';

        // Create directory if doesn't exist
        if (!file_exists($this->settings_dir)) {
            wp_mkdir_p($this->settings_dir);
        }

        $this->init_hooks();
    }

    private function init_hooks() {
        // Export on customizer save
        add_action('customize_save_after', array($this, 'export_on_save'));

        // Import on theme activation
        add_action('after_switch_theme', array($this, 'import_on_activation'));

        // Add export/import buttons to customizer
        add_action('customize_controls_enqueue_scripts', array($this, 'enqueue_customizer_scripts'));
        add_action('customize_register', array($this, 'add_export_import_section'));

        // AJAX handlers
        add_action('wp_ajax_export_customizer_settings', array($this, 'ajax_export'));
        add_action('wp_ajax_import_customizer_settings', array($this, 'ajax_import'));
    }

    /**
     * Export settings to JSON
     */
    public function export_settings() {
        // Get all theme mods
        $mods = get_theme_mods();

        if (empty($mods)) {
            return false;
        }

        // Get current WordPress customizer settings
        $settings = array();

        // Theme mods
        $settings['theme_mods'] = $mods;

        // Custom CSS (if exists)
        $custom_css = wp_get_custom_css();
        if ($custom_css) {
            $settings['custom_css'] = $custom_css;
        }

        // Site title/tagline (stored in options, not theme_mods)
        $settings['options'] = array(
            'blogname' => get_option('blogname'),
            'blogdescription' => get_option('blogdescription'),
        );

        // Add metadata
        $settings['_meta'] = array(
            'theme' => $this->theme_slug,
            'exported_at' => current_time('mysql'),
            'wp_version' => get_bloginfo('version'),
            'exported_by' => wp_get_current_user()->user_login,
        );

        // Convert to JSON
        $json = json_encode($settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

        // Save to file
        $filename = $this->settings_dir . '/customizer-settings.json';
        $result = file_put_contents($filename, $json);

        if ($result) {
            // Create Git-friendly changelog
            $this->create_changelog($settings);
        }

        return $result !== false;
    }

    /**
     * Create human-readable changelog
     */
    private function create_changelog($settings) {
        $changelog_file = $this->settings_dir . '/CHANGELOG.md';

        $changelog = "# Customizer Settings Changelog\n\n";
        $changelog .= "## " . date('Y-m-d H:i:s') . "\n\n";
        $changelog .= "Changed by: " . wp_get_current_user()->user_login . "\n\n";

        // List major settings
        if (isset($settings['theme_mods'])) {
            $changelog .= "### Theme Modifications\n\n";
            foreach ($settings['theme_mods'] as $key => $value) {
                if (is_array($value) || is_object($value)) {
                    $changelog .= "- **{$key}**: " . json_encode($value) . "\n";
                } else {
                    $changelog .= "- **{$key}**: {$value}\n";
                }
            }
        }

        // Append to changelog (keep history)
        file_put_contents($changelog_file, $changelog . "\n---\n\n", FILE_APPEND);
    }

    /**
     * Import settings from JSON
     */
    public function import_settings() {
        $filename = $this->settings_dir . '/customizer-settings.json';

        if (!file_exists($filename)) {
            return new WP_Error('file_not_found', 'Settings file not found');
        }

        $json = file_get_contents($filename);
        $settings = json_decode($json, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            return new WP_Error('json_error', 'Invalid JSON: ' . json_last_error_msg());
        }

        // Verify theme matches
        if (isset($settings['_meta']['theme']) && $settings['_meta']['theme'] !== $this->theme_slug) {
            return new WP_Error('theme_mismatch', 'Settings are for a different theme');
        }

        // Import theme mods
        if (isset($settings['theme_mods'])) {
            foreach ($settings['theme_mods'] as $key => $value) {
                set_theme_mod($key, $value);
            }
        }

        // Import custom CSS
        if (isset($settings['custom_css'])) {
            wp_update_custom_css_post($settings['custom_css']);
        }

        // Import options
        if (isset($settings['options'])) {
            foreach ($settings['options'] as $option => $value) {
                update_option($option, $value);
            }
        }

        return true;
    }

    /**
     * Auto-export after customizer save
     */
    public function export_on_save($wp_customize) {
        $this->export_settings();

        // Log for debugging
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('Customizer settings exported to JSON');
        }
    }

    /**
     * Auto-import on theme activation
     */
    public function import_on_activation() {
        $result = $this->import_settings();

        if (is_wp_error($result)) {
            error_log('Customizer import error: ' . $result->get_error_message());
        }
    }

    /**
     * Add export/import section to customizer
     */
    public function add_export_import_section($wp_customize) {
        $wp_customize->add_section('export_import_section', array(
            'title' => __('Git Sync', 'textdomain'),
            'description' => __('Version control for customizer settings', 'textdomain'),
            'priority' => 200,
        ));

        // Export button
        $wp_customize->add_setting('export_settings', array(
            'sanitize_callback' => '__return_false',
        ));

        $wp_customize->add_control('export_settings', array(
            'type' => 'button',
            'section' => 'export_import_section',
            'label' => __('Export to JSON', 'textdomain'),
            'description' => __('Download settings as JSON file', 'textdomain'),
            'input_attrs' => array(
                'class' => 'button button-primary',
                'value' => __('Export', 'textdomain'),
            ),
        ));

        // Import button
        $wp_customize->add_setting('import_settings', array(
            'sanitize_callback' => '__return_false',
        ));

        $wp_customize->add_control('import_settings', array(
            'type' => 'button',
            'section' => 'export_import_section',
            'label' => __('Import from JSON', 'textdomain'),
            'description' => __('Load settings from JSON file', 'textdomain'),
            'input_attrs' => array(
                'class' => 'button',
                'value' => __('Import', 'textdomain'),
            ),
        ));

        // Git status display
        $wp_customize->add_setting('git_status', array(
            'sanitize_callback' => '__return_false',
        ));

        $wp_customize->add_control('git_status', array(
            'type' => 'hidden',
            'section' => 'export_import_section',
            'description' => $this->get_git_status_html(),
        ));
    }

    /**
     * Get Git status for display
     */
    private function get_git_status_html() {
        $git_dir = get_stylesheet_directory();

        // Check if Git repo exists
        if (!is_dir($git_dir . '/.git')) {
            return '<p style="color: #dc3232;">⚠️ Not a Git repository</p>';
        }

        // Get Git status
        $output = shell_exec("cd {$git_dir} && git status --porcelain customizer-settings/");

        if (empty($output)) {
            return '<p style="color: #46b450;">✓ Settings committed</p>';
        }

        return '<p style="color: #ffba00;">⚠️ Uncommitted changes detected</p>' .
               '<pre style="background: #f0f0f1; padding: 10px; font-size: 11px;">' . 
               esc_html($output) . 
               '</pre>';
    }

    /**
     * Enqueue customizer scripts
     */
    public function enqueue_customizer_scripts() {
        wp_enqueue_script(
            'customizer-git-sync',
            get_stylesheet_directory_uri() . '/js/customizer-git-sync.js',
            array('jquery', 'customize-controls'),
            '1.0.0',
            true
        );

        wp_localize_script('customizer-git-sync', 'customizerGitSync', array(
            'ajax_url' => admin_url('admin-ajax.php'),
            'nonce' => wp_create_nonce('customizer_git_sync'),
        ));
    }

    /**
     * AJAX export handler
     */
    public function ajax_export() {
        check_ajax_referer('customizer_git_sync', 'nonce');

        if (!current_user_can('edit_theme_options')) {
            wp_send_json_error('Insufficient permissions');
        }

        $result = $this->export_settings();

        if ($result) {
            wp_send_json_success('Settings exported successfully');
        } else {
            wp_send_json_error('Export failed');
        }
    }

    /**
     * AJAX import handler
     */
    public function ajax_import() {
        check_ajax_referer('customizer_git_sync', 'nonce');

        if (!current_user_can('edit_theme_options')) {
            wp_send_json_error('Insufficient permissions');
        }

        $result = $this->import_settings();

        if (is_wp_error($result)) {
            wp_send_json_error($result->get_error_message());
        } else {
            wp_send_json_success('Settings imported successfully');
        }
    }
}

// Initialize
function init_customizer_git_sync() {
    if (current_user_can('edit_theme_options')) {
        new Customizer_Git_Sync();
    }
}
add_action('init', 'init_customizer_git_sync');
Enter fullscreen mode Exit fullscreen mode

JavaScript for Customizer Interface

Create /js/customizer-git-sync.js:

(function($) {
    'use strict';

    wp.customize.bind('ready', function() {

        // Export button handler
        $('input[value="Export"]').on('click', function(e) {
            e.preventDefault();

            $.ajax({
                url: customizerGitSync.ajax_url,
                type: 'POST',
                data: {
                    action: 'export_customizer_settings',
                    nonce: customizerGitSync.nonce
                },
                success: function(response) {
                    if (response.success) {
                        alert('✓ Settings exported to JSON!\\n\\nCommit the changes to Git.');
                    } else {
                        alert('✗ Export failed: ' + response.data);
                    }
                },
                error: function() {
                    alert('✗ AJAX error occurred');
                }
            });
        });

        // Import button handler
        $('input[value="Import"]').on('click', function(e) {
            e.preventDefault();

            if (!confirm('Import settings from JSON file?\\n\\nThis will overwrite current settings!')) {
                return;
            }

            $.ajax({
                url: customizerGitSync.ajax_url,
                type: 'POST',
                data: {
                    action: 'import_customizer_settings',
                    nonce: customizerGitSync.nonce
                },
                success: function(response) {
                    if (response.success) {
                        alert('✓ Settings imported!\\n\\nRefreshing customizer...');
                        location.reload();
                    } else {
                        alert('✗ Import failed: ' + response.data);
                    }
                },
                error: function() {
                    alert('✗ AJAX error occurred');
                }
            });
        });

        // Auto-save indicator
        wp.customize.state('saved').bind(function(saved) {
            if (saved) {
                console.log('Customizer saved - exporting to JSON...');
            }
        });
    });

})(jQuery);
Enter fullscreen mode Exit fullscreen mode

WP-CLI Commands

Create /wp-content/mu-plugins/customizer-cli.php:

<?php
/**
 * WP-CLI commands for customizer settings
 */

if (defined('WP_CLI') && WP_CLI) {

    class Customizer_CLI_Commands {

        /**
         * Export customizer settings to JSON
         *
         * ## EXAMPLES
         *
         *     wp customizer export
         */
        public function export($args, $assoc_args) {
            $sync = new Customizer_Git_Sync();
            $result = $sync->export_settings();

            if ($result) {
                WP_CLI::success('Settings exported to JSON');
                WP_CLI::log('File: ' . get_stylesheet_directory() . '/customizer-settings/customizer-settings.json');
            } else {
                WP_CLI::error('Export failed');
            }
        }

        /**
         * Import customizer settings from JSON
         *
         * ## EXAMPLES
         *
         *     wp customizer import
         */
        public function import($args, $assoc_args) {
            $sync = new Customizer_Git_Sync();
            $result = $sync->import_settings();

            if (is_wp_error($result)) {
                WP_CLI::error($result->get_error_message());
            } else {
                WP_CLI::success('Settings imported from JSON');
            }
        }

        /**
         * Show diff between database and JSON file
         *
         * ## EXAMPLES
         *
         *     wp customizer diff
         */
        public function diff($args, $assoc_args) {
            $theme_dir = get_stylesheet_directory();
            $json_file = $theme_dir . '/customizer-settings/customizer-settings.json';

            if (!file_exists($json_file)) {
                WP_CLI::error('JSON file not found');
                return;
            }

            // Get current settings
            $current = array(
                'theme_mods' => get_theme_mods(),
                'custom_css' => wp_get_custom_css(),
            );

            // Get JSON settings
            $json = json_decode(file_get_contents($json_file), true);

            // Compare
            $diff = array_diff_assoc($current['theme_mods'], $json['theme_mods']);

            if (empty($diff)) {
                WP_CLI::success('No differences found');
            } else {
                WP_CLI::log('Differences detected:');
                foreach ($diff as $key => $value) {
                    WP_CLI::log("  $key:");
                    WP_CLI::log("    Database: " . json_encode($value));
                    WP_CLI::log("    JSON:     " . json_encode($json['theme_mods'][$key] ?? 'not set'));
                }
            }
        }
    }

    WP_CLI::add_command('customizer', 'Customizer_CLI_Commands');
}
Enter fullscreen mode Exit fullscreen mode

Git Workflow Integration

Setup in Theme

# Navigate to theme directory
cd wp-content/themes/your-theme

# Initialize Git if not already
git init

# Create .gitignore
cat > .gitignore <<EOF
node_modules/
*.log
.DS_Store
EOF

# Add customizer settings directory
mkdir -p customizer-settings

# Create initial export
wp customizer export

# Commit
git add customizer-settings/
git commit -m "Initial customizer settings export"
Enter fullscreen mode Exit fullscreen mode

Deployment Workflow

# On development
# 1. Make customizer changes in WordPress
# 2. Settings auto-export to JSON
# 3. Commit to Git

git add customizer-settings/customizer-settings.json
git commit -m "Update header colors"
git push origin main

# On production
git pull origin main
wp customizer import
Enter fullscreen mode Exit fullscreen mode

Pre-Commit Hook

Create .git/hooks/pre-commit:

#!/bin/bash

# Check if customizer settings are uncommitted
if git diff --cached --name-only | grep -q "customizer-settings"; then
    echo "✓ Customizer settings included in commit"
else
    echo "⚠️  No customizer settings changes detected"
    echo "   Run: wp customizer export"
    echo ""
    read -p "Continue anyway? (y/n) " -n 1 -r
    echo
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        exit 1
    fi
fi
Enter fullscreen mode Exit fullscreen mode

Make executable:

chmod +x .git/hooks/pre-commit
Enter fullscreen mode Exit fullscreen mode

Advanced: Diff Visualization

Track exactly what changed:

<?php
/**
 * Generate visual diff for customizer changes
 */
function generate_customizer_diff() {
    $theme_dir = get_stylesheet_directory();
    $json_file = $theme_dir . '/customizer-settings/customizer-settings.json';

    if (!file_exists($json_file)) {
        return;
    }

    // Get settings from both sources
    $db_settings = get_theme_mods();
    $json_settings = json_decode(file_get_contents($json_file), true)['theme_mods'];

    // Generate HTML diff
    $diff_html = '<div class="customizer-diff">';
    $diff_html .= '<h3>Customizer Changes</h3>';

    // Check each setting
    foreach ($db_settings as $key => $value) {
        $json_value = $json_settings[$key] ?? null;

        if ($value !== $json_value) {
            $diff_html .= "<div class='diff-item'>";
            $diff_html .= "<strong>{$key}</strong><br>";
            $diff_html .= "<span class='old'>- " . esc_html(json_encode($json_value)) . "</span><br>";
            $diff_html .= "<span class='new'>+ " . esc_html(json_encode($value)) . "</span>";
            $diff_html .= "</div>";
        }
    }

    $diff_html .= '</div>';

    return $diff_html;
}
Enter fullscreen mode Exit fullscreen mode

Handling Sensitive Data

Exclude sensitive settings from version control:

<?php
// In Customizer_Git_Sync class

private function get_exportable_settings() {
    $all_mods = get_theme_mods();

    // Settings to exclude from export
    $exclude = array(
        'api_keys',
        'passwords',
        'tokens',
        'private_data',
    );

    // Filter out sensitive data
    $safe_mods = array_diff_key($all_mods, array_flip($exclude));

    return $safe_mods;
}
Enter fullscreen mode Exit fullscreen mode

Testing the Integration

# Export current settings
wp customizer export
# Check: customizer-settings/customizer-settings.json created

# Make changes in customizer
# - Change site title
# - Update header color
# - Modify typography

# Export again
wp customizer export

# Check Git diff
git diff customizer-settings/customizer-settings.json
# Should show your changes

# Commit
git add customizer-settings/
git commit -m "Update branding colors"

# Test import (revert changes)
wp customizer import
# Settings should restore from JSON
Enter fullscreen mode Exit fullscreen mode

Bottom Line

Stop losing customizer settings to database mysteries!!

This system:

  • Auto-exports settings to JSON on every save
  • Full Git history of all changes
  • Deploy settings with code (wp customizer import)
  • Revert bad changes instantly
  • Share settings across environments
  • Never lose configuration again!!

vs manual workflow:

  • Settings trapped in database
  • Zero version history
  • Can't see what changed
  • Lost if database crashes
  • Complete nightmare!!

Version-controlled customizer = professional WordPress development!! 🚀

This article contains affiliate links!

Top comments (0)