You installed your SSL certificate, flipped your site URLs over to HTTPS, and your browser is still showing that frustrating broken padlock or yellow warning icon. That means mixed content: your HTML document loads securely over HTTPS, but sub-resources like images, stylesheets, or scripts are still being requested over plain HTTP.
You could throw a plugin like Really Simple SSL at it to rewrite strings on the fly, but patching URLs at PHP runtime adds unnecessary overhead. It’s much cleaner to fix the actual URLs in the database with WP-CLI and set up solid server-level redirects in .htaccess once and for all.

Find the Rogue HTTP Assets First
Before running database replacements, check what’s actually triggering the browser warnings. The quickest way is opening DevTools (F12) and checking the Console tab, which lists every downgraded or blocked request.
If you’re working directly over SSH without a browser handy, run a quick cURL command against your homepage to grab any hardcoded asset paths:
curl -sL https://example.com | grep -oE '(src|href)=["x27]http://[^"x27]+'This filters out any src or href tags still pointing to http://. For a breakdown of how browsers classify active vs. passive mixed content, check out the MDN Web Docs on Mixed Content.
Back Up the Database First (Seriously)
Don’t run a global search-and-replace across your database without taking a snapshot. While WP-CLI handles PHP serialized strings properly (unlike a raw SQL dump and find-and-replace), mistakes happen.
Dump your current database state to an SQL file outside your public web root:
wp db export ~/backup_before_ssl_fix_$(date +%F_%T).sqlIf you haven’t set up routine backups yet, check our guide on automating WordPress backups with WP-CLI and Cron to protect your setup going forward.
Dry-Run the Search and Replace
WP-CLI includes a built-in search and replace command that safely handles serialized arrays and objects in WordPress tables. Always use the --dry-run flag first to see how many matches it finds before actually writing changes to disk.
Head to your WordPress root (like cd ~/public_html) and run:
wp search-replace 'http://example.com' 'https://example.com' --dry-run --all-tables-with-prefixLook over the summary table WP-CLI prints out. It lists every scanned table (like wp_posts, wp_postmeta, and wp_options) and how many replacements it plans to make:
+------------------+----------------+--------------+------+
| Table | Column | Replacements | Type |
+------------------+----------------+--------------+------+
| wp_options | option_value | 14 | PHP |
| wp_posts | post_content | 142 | SQL |
| wp_posts | guid | 0 | SQL |
| wp_postmeta | meta_value | 58 | PHP |
+------------------+----------------+--------------+------+Notice how WP-CLI detects serialized data columns (marked with PHP under Type) and updates string length offsets automatically so your widgets and customizer settings don’t break.
Run the Live Database Replacement
If the dry-run numbers look right, run the command for real. Pass --skip-columns=guid because WordPress uses the GUID column as a unique identifier for RSS readers, not an active link—modifying it can cause feed readers to re-fetch all historical posts.
Execute the replacement:
wp search-replace 'http://example.com' 'https://example.com' --all-tables-with-prefix --skip-columns=guid --preciseThe --precise flag ensures WP-CLI scans columns byte by byte, which avoids issues if your site uses multibyte character sets. You can check out additional flags in the official WP-CLI search-replace documentation.
If you’re cleaning up URLs after moving to a new server, take a look at our walkthrough on migrating a WordPress site via SSH and WP-CLI.
Check Site URLs and wp-config.php
Sometimes the core siteurl and home options stay pinned to HTTP if an aggressive object cache missed the update or if hardcoded constants were set.
Check the current values:
wp option get siteurl
wp option get homeIf either still returns http://, update them directly:
wp option update siteurl 'https://example.com'
wp option update home 'https://example.com'Then open your wp-config.php file. If you see lines like define('WP_HOME', 'http://example.com');, change them to https:// or remove them altogether so the database options take precedence.
Force HTTPS Redirects in .htaccess
Now that database links are updated, tell Apache to catch any incoming plain HTTP requests and redirect them straight to HTTPS before WordPress even spins up. Open your .htaccess file:
nano ~/public_html/.htaccessPlace these rules at the very top of the file, above the # BEGIN WordPress block:
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
If your site is behind a reverse proxy or CDN (like Cloudflare or an Nginx SSL terminator), checking %{HTTPS} off directly can trigger an infinite redirect loop because the proxy talks to Apache over HTTP. In that setup, check the X-Forwarded-Proto header instead:
RewriteEngine On
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
For more details on rewrite flags, refer to the Apache mod_rewrite documentation.
Upgrade Rogue Third-Party Assets with CSP Headers
If your theme or a legacy plugin has hardcoded external HTTP assets (like an old tracking script or CDN stylesheet) that don’t live in your database, you can tell modern browsers to auto-upgrade those requests to HTTPS on the fly.
Add this header rule to your .htaccess file:
Header always set Content-Security-Policy "upgrade-insecure-requests;"
When the browser hits something like http://cdn.example.com/script.js, it rewrites the request to https://cdn.example.com/script.js before sending it out. Just keep in mind this only works if that external server actually supports HTTPS on port 443.
Grep for Hardcoded URLs in Theme and Plugin Files
WP-CLI only touches the MySQL database. It won’t touch static PHP templates, CSS files, or custom scripts sitting inside wp-content/themes/ or wp-content/plugins/.
Run a quick grep to catch any remaining hardcoded HTTP links in your files:
grep -rnw ~/public_html/wp-content/ -e 'http://example.com' --exclude-dir={cache,uploads}If you spot hardcoded paths in a custom child theme, swap them out for dynamic helpers like get_stylesheet_directory_uri() or wp_enqueue_style().
Flush Caches, Transients, and Elementor CSS
If the mixed content warning is still lingering after all that, your caching layer or page builder is probably serving cached CSS containing old HTTP background image URLs.
First, clear the object cache and all expired transients:
wp cache flush
wp transient delete --allIf you’re using Elementor, it stores compiled CSS in wp-content/uploads/elementor/css/ with hardcoded absolute image URLs. Force it to rebuild:
wp elementor flush-cssIf the Elementor builder gets finicky after updating URLs, see our fix for Elementor stuck on loading screen.
And if you’re setting up a fresh site and haven’t configured your SSL certificate yet, follow our step-by-step guide to install an SSL certificate on Hostinger shared hosting.
Frequently Asked Questions
Why did WP-CLI report zero replacements in wp_posts?
Usually this happens because of a URL format mismatch—like searching for http://example.com when the database actually contains http://www.example.com (or vice versa). Check your exact format by running wp option get siteurl and match that prefix in your search-replace command.
Does upgrade-insecure-requests fix third-party images that lack SSL?
No. The upgrade-insecure-requests header forces the browser to request the asset via HTTPS. If the external server doesn’t support SSL, the request will fail with a 404 or connection error. You’ll avoid the mixed content warning, but the image will break. In that case, download the image and re-host it locally in your Media Library.
Will running wp search-replace break my serialized theme options?
No, as long as you use wp search-replace. WP-CLI detects serialized PHP strings, unpacks them in memory, performs the replacement, recalculates string length offsets, and re-serializes the data before writing to the database. Running a raw SQL UPDATE in phpMyAdmin will break serialized arrays, but WP-CLI handles them safely.
Why do I get an ERR_TOO_MANY_REDIRECTS error after adding .htaccess rules?
This happens when a reverse proxy (like Cloudflare) handles SSL at the edge but connects back to your origin server over plain HTTP. Your .htaccess sees an HTTP request and redirects it to HTTPS, creating an infinite loop. Use the X-Forwarded-Proto rewrite condition mentioned above to prevent this.

