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
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
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');
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);
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');
}
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"
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
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
Make executable:
chmod +x .git/hooks/pre-commit
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;
}
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;
}
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
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)