WordPress Multisite networks power some of the web’s largest publishing platforms, educational institutions, and enterprise content systems. Managing dozens or hundreds of sites individually is impractical. WP-CLI provides essential tools for executing bulk operations across entire networks efficiently and consistently.
- Understanding WordPress Multisite Architecture
- Basic Multisite Navigation
- Iterating Through Network Sites
- Network-Wide Plugin Management
- Bulk Content Operations
- Database Maintenance Across Network
- Network-Wide Configuration Updates
- Selective Site Operations
- User Management Across Network
- Performance Optimization Network-Wide
- Monitoring and Reporting
- Related Links
Understanding WordPress Multisite Architecture
WordPress Multisite transforms a single WordPress installation into a network of sites sharing core files, plugins, and themes while maintaining separate databases for content. This architecture enables centralized management but requires specialized tools for network-wide operations.
Each site in a multisite network has a unique site ID, URL, and database tables. Network administrators need methods to iterate through sites, apply configurations selectively, and verify operations across the entire network. WP-CLI’s multisite commands make these complex operations manageable.
Basic Multisite Navigation
Before performing bulk operations, understand how to navigate and query your multisite network.
# List all sites in the network
wp site list
# List sites with specific columns
wp site list --fields=blog_id,url,registered
# Count total sites
wp site list --format=count
# Get specific site details
wp site list --site__in=1,5,10
# Filter sites by URL pattern
wp site list --url='*.example.com'
# Find archived/deleted/spam sites
wp site list --archived=true
wp site list --deleted=true
wp site list --spam=true
Get detailed information about a specific site:
# Get site URL
wp site url 5
# Get site domain and path
wp site list --field=url --blog_id=5
# Check if site is public
wp option get blog_public --url=example.com/site
Iterating Through Network Sites
The foundation of bulk operations is iterating through all sites and executing commands for each.
#!/bin/bash
# Basic site iteration example
# Get all site URLs
for url in $(wp site list --field=url); do
echo "Processing: $url"
wp --url="$url" option get blogname
done
# Using site IDs
for site_id in $(wp site list --field=blog_id); do
echo "Processing site ID: $site_id"
SITE_URL=$(wp site url $site_id)
wp --url="$SITE_URL" plugin list --status=active
done
Advanced iteration with error handling:
#!/bin/bash
# iterate-sites.sh - Robust site iteration with error handling
set -euo pipefail
LOG_FILE="/var/log/multisite-operations-$(date +%Y%m%d).log"
SUCCESS_COUNT=0
FAILURE_COUNT=0
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
process_site() {
local url=$1
log "Processing: $url"
# Execute operation with error handling
if wp --url="$url" cache flush 2>&1 | tee -a "$LOG_FILE"; then
((SUCCESS_COUNT++))
log "SUCCESS: $url"
else
((FAILURE_COUNT++))
log "FAILED: $url"
fi
}
# Main execution
log "Starting network-wide operation"
# Get all active sites (exclude archived, deleted, spam)
SITES=$(wp site list --archived=false --deleted=false --spam=false --field=url)
TOTAL=$(echo "$SITES" | wc -l)
log "Total sites to process: $TOTAL"
# Process each site
echo "$SITES" | while read -r site_url; do
process_site "$site_url"
done
log "Operation completed. Success: $SUCCESS_COUNT, Failed: $FAILURE_COUNT"
Network-Wide Plugin Management
Manage plugins across all sites in your network consistently.
# Network activate a plugin (available to all sites)
wp plugin install jetpack --activate-network
# Network deactivate a plugin
wp plugin deactivate jetpack --network
# Check plugin status across all sites
for url in $(wp site list --field=url); do
echo "Site: $url"
wp --url="$url" plugin list --status=active --fields=name,version
done
# Activate plugin on all sites
for url in $(wp site list --field=url); do
wp --url="$url" plugin activate woocommerce
echo "Activated on: $url"
done
# Deactivate plugin from specific sites
for url in $(wp site list --field=url); do
if [[ "$url" == *"store"* ]]; then
echo "Keeping plugin on: $url"
else
wp --url="$url" plugin deactivate woocommerce
echo "Deactivated on: $url"
fi
done
Selective plugin deployment based on site characteristics:
#!/bin/bash
# deploy-plugin-conditional.sh - Deploy plugin based on conditions
PLUGIN_SLUG="custom-plugin"
REQUIRED_THEME="storefront"
echo "Deploying $PLUGIN_SLUG to sites using $REQUIRED_THEME theme"
for url in $(wp site list --field=url); do
# Get active theme for site
ACTIVE_THEME=$(wp --url="$url" theme list --status=active --field=name)
if [ "$ACTIVE_THEME" == "$REQUIRED_THEME" ]; then
echo "Activating $PLUGIN_SLUG on $url"
wp --url="$url" plugin activate "$PLUGIN_SLUG"
# Configure plugin settings
wp --url="$url" option update custom_plugin_enabled 1
else
echo "Skipping $url (theme: $ACTIVE_THEME)"
fi
done
echo "Selective deployment completed"
Bulk Content Operations
Perform content operations across multiple sites simultaneously.
# Count posts across all sites
for url in $(wp site list --field=url); do
COUNT=$(wp --url="$url" post list --post_type=post --format=count)
echo "$url: $COUNT posts"
done
# Bulk publish scheduled posts
for url in $(wp site list --field=url); do
SCHEDULED=$(wp --url="$url" post list --post_status=future --format=count)
if [ "$SCHEDULED" -gt 0 ]; then
echo "$url has $SCHEDULED scheduled posts"
wp --url="$url" post list --post_status=future --field=ID | while read -r post_id; do
wp --url="$url" post update $post_id --post_status=publish
done
fi
done
# Delete spam comments network-wide
for url in $(wp site list --field=url); do
SPAM_COUNT=$(wp --url="$url" comment list --status=spam --format=count)
if [ "$SPAM_COUNT" -gt 0 ]; then
echo "Deleting $SPAM_COUNT spam comments from $url"
wp --url="$url" comment delete $(wp --url="$url" comment list --status=spam --format=ids) --force
fi
done
Network-wide content creation:
#!/bin/bash
# create-default-pages.sh - Create standard pages on all sites
PAGES=(
"Privacy Policy|privacy-policy|Privacy policy content here"
"Terms of Service|terms-of-service|Terms of service content here"
"Contact|contact|Contact information here"
)
for url in $(wp site list --field=url); do
echo "Creating pages on: $url"
for page_data in "${PAGES[@]}"; do
IFS='|' read -r title slug content <<< "$page_data"
# Check if page already exists
if ! wp --url="$url" post list --post_type=page --name="$slug" --format=ids | grep -q '[0-9]'; then
POST_ID=$(wp --url="$url" post create \
--post_type=page \
--post_title="$title" \
--post_name="$slug" \
--post_content="$content" \
--post_status=publish \
--porcelain)
echo "Created '$title' (ID: $POST_ID) on $url"
else
echo "'$title' already exists on $url"
fi
done
done
echo "Page creation completed"
Database Maintenance Across Network
Maintain database health across all sites in the network.
#!/bin/bash
# network-database-maintenance.sh - Database cleanup for all sites
LOG_FILE="/var/log/network-db-maintenance-$(date +%Y%m%d).log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log "Starting network-wide database maintenance"
for url in $(wp site list --field=url); do
log "Processing: $url"
# Get initial database size
SITE_ID=$(wp site list --url="$url" --field=blog_id)
INITIAL_SIZE=$(wp db size --tables="wp_${SITE_ID}_*" --format=json 2>/dev/null || echo "0")
# Delete post revisions older than 90 days
REVISIONS=$(wp --url="$url" post list --post_type=revision --format=count)
if [ "$REVISIONS" -gt 0 ]; then
log " Deleting $REVISIONS revisions"
wp --url="$url" post delete $(wp --url="$url" post list --post_type=revision --format=ids) --force
fi
# Clean transients
log " Cleaning transients"
wp --url="$url" transient delete --expired
# Delete orphaned metadata
log " Cleaning orphaned metadata"
wp --url="$url" db query "
DELETE pm FROM wp_${SITE_ID}_postmeta pm
LEFT JOIN wp_${SITE_ID}_posts wp ON wp.ID = pm.post_id
WHERE wp.ID IS NULL
" 2>/dev/null || true
# Optimize tables
log " Optimizing tables"
wp db optimize 2>/dev/null || true
log " Completed: $url"
done
log "Network database maintenance completed"
Network-Wide Configuration Updates
Apply configuration changes across all sites consistently.
#!/bin/bash
# update-network-config.sh - Apply settings network-wide
# Configuration to apply
CONFIG=(
"blog_public|0|Disable search engine indexing"
"default_comment_status|closed|Close comments by default"
"timezone_string|America/New_York|Set timezone"
"date_format|F j, Y|Set date format"
"time_format|g:i a|Set time format"
)
echo "Applying configuration to all sites..."
for url in $(wp site list --field=url); do
echo "Configuring: $url"
for config_line in "${CONFIG[@]}"; do
IFS='|' read -r option value description <<< "$config_line"
wp --url="$url" option update "$option" "$value"
echo " Set $option = $value ($description)"
done
done
echo "Configuration applied to all sites"
Selective Site Operations
Execute operations on filtered subsets of sites.
#!/bin/bash
# selective-operations.sh - Target specific sites
# Operation 1: Update only sites matching URL pattern
echo "Updating sites with 'blog' in URL..."
for url in $(wp site list --field=url | grep 'blog'); do
echo "Processing: $url"
wp --url="$url" plugin activate akismet
done
# Operation 2: Target sites by registration date
echo "Processing recently created sites..."
CUTOFF_DATE=$(date -d "30 days ago" +%Y-%m-%d)
wp site list --format=json | jq -r --arg date "$CUTOFF_DATE" '.[] |
select(.registered >= $date) | .url' | while read -r url; do
echo "Recent site: $url"
wp --url="$url" post create \
--post_title="Welcome" \
--post_content="Welcome to our network!" \
--post_status=publish
done
# Operation 3: Target sites by size
echo "Finding large sites..."
for url in $(wp site list --field=url); do
POST_COUNT=$(wp --url="$url" post list --format=count)
if [ "$POST_COUNT" -gt 1000 ]; then
echo "Large site: $url ($POST_COUNT posts)"
# Perform operation for large sites
wp --url="$url" option update posts_per_page 20
fi
done
User Management Across Network
Manage users and permissions network-wide.
# Add super admin
wp super-admin add username
# Remove super admin
wp super-admin remove username
# List all super admins
wp super-admin list
# Add user to specific sites
USER_ID=123
for url in $(wp site list --field=url | head -5); do
wp --url="$url" user set-role $USER_ID editor
echo "Added user $USER_ID as editor on $url"
done
# Find users across all sites
for url in $(wp site list --field=url); do
USER_COUNT=$(wp --url="$url" user list --format=count)
echo "$url: $USER_COUNT users"
done
Network-wide user audit:
#!/bin/bash
# audit-network-users.sh - Comprehensive user audit
REPORT_FILE="/var/log/network-user-audit-$(date +%Y%m%d).txt"
echo "=== Network User Audit ===" > "$REPORT_FILE"
echo "Date: $(date)" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
# Super admins
echo "Super Administrators:" >> "$REPORT_FILE"
wp super-admin list >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
# Users per site
echo "Users per site:" >> "$REPORT_FILE"
for url in $(wp site list --field=url); do
ADMIN_COUNT=$(wp --url="$url" user list --role=administrator --format=count)
EDITOR_COUNT=$(wp --url="$url" user list --role=editor --format=count)
TOTAL_COUNT=$(wp --url="$url" user list --format=count)
echo "$url:" >> "$REPORT_FILE"
echo " Admins: $ADMIN_COUNT" >> "$REPORT_FILE"
echo " Editors: $EDITOR_COUNT" >> "$REPORT_FILE"
echo " Total: $TOTAL_COUNT" >> "$REPORT_FILE"
done
echo "Audit completed: $REPORT_FILE"
Performance Optimization Network-Wide
Optimize performance across all network sites.
#!/bin/bash
# optimize-network.sh - Network-wide performance optimization
echo "=== Network Performance Optimization ==="
TOTAL_SITES=$(wp site list --format=count)
CURRENT=0
for url in $(wp site list --field=url); do
((CURRENT++))
echo "[$CURRENT/$TOTAL_SITES] Optimizing: $url"
# Clear expired transients
wp --url="$url" transient delete --expired
# Clear object cache
wp --url="$url" cache flush
# Regenerate missing thumbnails
MISSING=$(wp --url="$url" media regenerate --only-missing --yes --dry-run 2>&1 | grep -o '[0-9]* images' | grep -o '[0-9]*')
if [ "$MISSING" -gt 0 ]; then
echo " Regenerating $MISSING thumbnails"
wp --url="$url" media regenerate --only-missing --yes
fi
# Update rewrite rules
wp --url="$url" rewrite flush
echo " Completed"
sleep 1 # Avoid overloading server
done
echo "Network optimization completed"
Monitoring and Reporting
Generate comprehensive reports about your multisite network.
#!/bin/bash
# network-health-report.sh - Generate network health report
REPORT_FILE="/var/reports/network-health-$(date +%Y%m%d).html"
# HTML header
cat > "$REPORT_FILE" << 'EOF'
<!DOCTYPE html>
<html>
<head>
<title>Network Health Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #4CAF50; color: white; }
.warning { background-color: #ffeb3b; }
.error { background-color: #f44336; color: white; }
</style>
</head>
<body>
<h1>WordPress Network Health Report</h1>
<p>Generated: $(date)</p>
EOF
# Network statistics
TOTAL_SITES=$(wp site list --format=count)
ACTIVE_SITES=$(wp site list --archived=false --deleted=false --spam=false --format=count)
cat >> "$REPORT_FILE" << EOF
<h2>Network Overview</h2>
<ul>
<li>Total Sites: $TOTAL_SITES</li>
<li>Active Sites: $ACTIVE_SITES</li>
<li>Archived/Deleted/Spam: $((TOTAL_SITES - ACTIVE_SITES))</li>
</ul>
<h2>Site Details</h2>
<table>
<tr>
<th>Site URL</th>
<th>Posts</th>
<th>Users</th>
<th>Active Plugins</th>
<th>Theme</th>
</tr>
EOF
# Site details
for url in $(wp site list --field=url); do
POST_COUNT=$(wp --url="$url" post list --post_type=post --format=count)
USER_COUNT=$(wp --url="$url" user list --format=count)
PLUGIN_COUNT=$(wp --url="$url" plugin list --status=active --format=count)
THEME=$(wp --url="$url" theme list --status=active --field=name)
cat >> "$REPORT_FILE" << EOF
<tr>
<td>$url</td>
<td>$POST_COUNT</td>
<td>$USER_COUNT</td>
<td>$PLUGIN_COUNT</td>
<td>$THEME</td>
</tr>
EOF
done
# HTML footer
cat >> "$REPORT_FILE" << 'EOF'
</table>
</body>
</html>
EOF
echo "Report generated: $REPORT_FILE"
