022 —WordPress
Reviving a dead WordPress site: a 90-minute playbook
A Dutch agency owner sent a Loom at 23:41: white screen, no logs, no SSH. Ninety minutes later the cart was live. Here is the order we work in.
A Dutch agency owner sent us a Loom at 23:41 last Tuesday. Twelve seconds of him refreshing wp-admin and watching the browser hang. The site, a WooCommerce shop doing about €4k/day, had been “fine yesterday.” Today: white screen, no logs, no SSH, just FTP and a phpMyAdmin link from 2019. He had a customer email open in the other tab asking why checkout was broken.
This is the playbook we walked him through. Ninety minutes from white screen to live cart. Most dead WordPress sites are not actually dead; they are misconfigured in two or three places, and the fixes are deterministic. The trick is the order you work in.
The first ten minutes: confirm what you are looking at
Before touching anything, write down what you can see from outside. Open the front page. Open /wp-admin. Open /wp-login.php. Open /wp-cron.php in the browser. Note the exact failure mode of each. A white screen is not the same as a 500, which is not the same as a redirect loop, which is not the same as “database connection error.” Each one points at a different organ.
The four failure modes you will meet, in roughly descending frequency:
- 500 Internal Server Error on every URL. Almost always a PHP version mismatch or a fatal in a plugin.
- Error establishing a database connection. wp-config.php credentials are wrong, the DB is down, or the host renamed the socket.
- White screen of death. A PHP fatal with
display_errorsoff. The real error is in the log. - Redirect loop on /wp-admin. A siteurl/home mismatch in wp_options, or a stale .htaccess.
Pull /wp-config.php over FTP and add three lines at the top, just under <?php:
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
Reload the front page once. WordPress will now write to /wp-content/debug.log. Pull that file. Ninety percent of the time the first fatal in there names the file and line that killed the site. The other ten percent of the time the log is empty, which is its own signal: the failure is happening before WordPress boots, which means .htaccess, PHP version, or the database.
Triage the file system
Once you know the failure mode, the file system is the cheapest place to look. Three things to check, in this order.
PHP version against the codebase
Most hosts silently upgraded their default PHP to 8.2 or 8.3 in the last eighteen months. WordPress core handles that fine. Plugins from 2017 do not. Open the host control panel and check the active PHP version. Then check the most recently-modified plugin in /wp-content/plugins/. If the site was untouched since 2019 and PHP just moved to 8.2, the fatals you are seeing in debug.log will mention things like Cannot use "self" when no class scope is active or the removed each() function. The php.net migration notes are the canonical list of what 7.x code breaks on.
The fix is to step PHP down one major version in the host panel, confirm the site loads, then plan a real upgrade. Do not try to patch the plugin live at 23:41.
Plugin and theme disable, surgically
If debug.log names a plugin, do not deactivate it through wp-admin (you cannot reach wp-admin). Rename its folder over FTP:
mv /wp-content/plugins/broken-plugin \
/wp-content/plugins/broken-plugin.off
WordPress treats a missing folder as a deactivated plugin and will not complain. The same trick works on the active theme: rename it, and WordPress falls back to whichever default theme (twentytwentyfour, twentytwentythree) is still installed. If no default is installed, drop one in over FTP before renaming.
File permissions
If the host migrated the account or restored from backup, permissions can come back wrong. The canonical safe set:
find . -type d -exec chmod 755 {} \;
find . -type f -exec chmod 644 {} \;
chmod 600 wp-config.php
If you only have FTP, your client (Transmit, Cyberduck, FileZilla) can apply these recursively from the right-click menu. 600 on wp-config.php is not paranoia; it stops other tenants on shared hosting from reading your DB credentials.
The database, the part everyone fears
If the front page now loads but /wp-admin redirects forever, or if the site is up but every link goes to the wrong domain, the problem is in wp_options. Two rows control almost everything: siteurl and home.
Open the database (phpMyAdmin if that is all you have, a proper MySQL editor if you can) and run:
SELECT option_name, option_value
FROM wp_options
WHERE option_name IN ('siteurl', 'home');
If they point at http://staging.oldhost.com and your live domain is https://shop.example.nl, that is your redirect loop. Do not just UPDATE those two rows and walk away. WordPress stores the old URL in serialized PHP across wp_postmeta, wp_options, and any plugin tables that cached menu structures or widget configs. A naive find-and-replace breaks the serialization length prefixes and corrupts the data silently.
The right tool is wp-cli if you have SSH:
wp search-replace 'http://staging.oldhost.com' 'https://shop.example.nl' \
--all-tables --skip-columns=guid --dry-run
Drop --dry-run once the count looks sane. If you do not have SSH, use Search Replace DB from interconnect/it: drop it in a folder outside the web root if you can, run it once, delete it. Never leave that script on a live server.
The --skip-columns=guid flag matters. The guid column in wp_posts is a permanent identifier and is not used for URL routing, despite looking like a URL. Rewriting it confuses RSS readers and breaks feed deduplication for anyone subscribed.
When the DB itself is the problem
If wp_options looks fine but you still get “Error establishing a database connection,” check three things, in order. First, the credentials in wp-config.php against what the host panel says today. Hosts rotate DB passwords on plan changes more often than they admit. Second, the DB_HOST value: some hosts moved from localhost to a socket path like localhost:/var/run/mysqld/mysqld10.sock or a private hostname like db-internal.host.net. Third, run a quick repair:
define('WP_ALLOW_REPAIR', true);
Add that to wp-config.php, then visit /wp-admin/maint/repair.php. Click “Repair and Optimize Database.” Remove the line when done. The endpoint is public when the flag is on, which is fine for ten minutes but not for a week.
.htaccess and the rewrite stack
If the front page loads but every subpage 404s, .htaccess is missing, truncated, or was replaced by a host migration. WordPress regenerates it on permalink save, but you do not need wp-admin to do that. Drop this into the web root:
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
If you are on nginx, this block does nothing, and the equivalent lives in the server config (which you usually cannot edit on shared hosting). On nginx hosts, ask support to confirm the try_files $uri $uri/ /index.php?$args; directive is present. That single line is the nginx equivalent of the entire block above.
While you are in .htaccess, add two things that should always be there on a production WordPress:
<Files wp-config.php>
Require all denied
</Files>
<Files xmlrpc.php>
Require all denied
</Files>
The first stops anyone from downloading wp-config.php if PHP ever stops parsing (which is the failure mode that leaks your DB password). The second kills xmlrpc.php, which has been a brute-force amplification target for a decade and which approximately nobody uses anymore. The Apache mod_authz_core docs cover the modern Require syntax if you are still running 2.2-era Deny from all lines.
Bring it back and harden the perimeter
Once the front page, wp-admin, and a sample product or post URL all return 200, do not call it done. The site is alive; it is not yet hardened. Twenty minutes of work prevents the next call.
Reset the admin password from the database, because you do not know who has it:
UPDATE wp_users
SET user_pass = MD5('temp-pwd-rotate-now')
WHERE user_login = 'admin';
WordPress will rehash to phpass on first login. Force every other user to log in again by changing the auth salts in wp-config.php. The WordPress salt endpoint gives you a fresh block; paste it over the existing eight define() lines. Every session token in the database is now invalid.
Then audit the plugin list. Anything not updated in 18 months is a candidate for removal. The WPScan vulnerability database will tell you which of the active plugins have known CVEs against the installed version. Run it once, write down the three worst, deal with them this week. The boring pattern across every dead WordPress site we have seen: an old contact form, an old caching plugin, an old SEO plugin. The pattern is boring, which is exactly why it works.
Finally, take a snapshot of the working state. Tar the web root and dump the database, before you change anything else:
tar -czf wp-site-$(date +%F).tar.gz public_html/
mysqldump -u user -p dbname > wp-db-$(date +%F).sql
If you only have FTP, pull the whole tree to local disk and export the database from phpMyAdmin to .sql.gz. Whatever you do, you want a known-good baseline before the next change.
What we ended up building for this
We hit this exact ninety-minute loop often enough that it became most of our consulting work on legacy site maintenance. The slow part was never the fix; it was the FTP-edit-reload-grep-debug.log cycle, plus the moment-of-fear when you UPDATE a wp_options row with no undo. When we built Pier we ran into this exact thing, so the way we ended up handling it was a docked FTP and MySQL workspace where every file save and every SQL write lands in a version history you can roll back with one click. Same playbook, fewer 02:00 emails.
The smallest thing you can do today: open one client site’s wp-config.php and paste the WP_DEBUG_LOG block above, commented out. When the call comes, you uncomment three lines instead of reading wp-config.php in a panic.
— Questions —
What if WP_DEBUG_LOG shows an empty file?
Empty debug.log usually means the failure happens before WordPress boots. Look at .htaccess, the PHP version, or the database credentials in wp-config.php first.
Can I just UPDATE wp_options siteurl and home with SQL?
For those two rows specifically, yes. For URLs in posts, postmeta, or widget configs, no. Those columns hold serialized PHP and need a search-replace that rewrites length prefixes.
Is it safe to leave xmlrpc.php blocked?
For most sites, yes. The Jetpack mobile app and a few legacy ping services still use it. If pingbacks fail or the WP mobile app refuses to connect, that block is the cause.
How fast is this if the database itself is corrupted?
If InnoDB tables are corrupted at the storage level, ninety minutes is optimistic. Restore from the most recent mysqldump, then replay any orders or posts from email confirmations.