Blog

  • WordPress Backups: What to Back Up, How Often, and How to Test a Restore

    WordPress Backups: What to Back Up, How Often, and How to Test a Restore

    Back up two things: the database and wp-content. Everything else on a WordPress install can be downloaded again. Back up daily if the site changes daily, keep at least one copy off the hosting account, and test a restore on a copy before you actually need one — an untested backup is a guess, not a backup.

    I learned this the boring way. The one time a fix took a full day instead of ten minutes, the backup existed — it just would not restore, because nobody had ever opened it.

    The short version

    • Database first. Posts, pages, users, settings, plugin options, menus and widget assignments all live there.
    • wp-content second. Uploads, themes, plugins, mu-plugins. Custom code lives here and cannot be re-downloaded.
    • Frequency follows how much work you would redo — not a number someone told you.
    • One copy must live off the server. A snapshot inside the same hosting account dies with the account.
    • Schedule a restore drill. Quarterly is enough for a small site, and it takes about twenty minutes on a copy.

    What actually needs to be in the backup

    The database is small and changes constantly; the files are large and change rarely. Treat them separately, because that difference decides your schedule and your storage bill.

    In the database, the tables that matter are wp_posts (posts, pages, revisions, and the rows that define menus), wp_postmeta (custom fields — page builders store their entire layout here, so a database-only restore brings the layout back but not the images it points to), wp_users and wp_usermeta (logins and capabilities), wp_options (site URL, active plugins, cron schedule, theme settings), and the term tables that hold categories and tags. Plugin tables use their own prefixes and are easy to forget — a shop’s orders may sit in tables a “posts only” export never touches.

    In the files, wp-content/uploads is usually the bulk of it, and it is also the part people most often exclude by accident. wp-content/themes and wp-content/plugins can mostly be re-downloaded, except for the child theme you edited and the plugin you patched by hand — those are original work and belong in the backup. wp-content/mu-plugins must always be included; it loads automatically, which is exactly why malware likes to hide there. I covered that pattern in the five-minute infection check.

    Two files outside wp-content are worth the extra ten seconds: wp-config.php (database name, user, password, table prefix, and the security salts — losing the salts logs every user out) and .htaccess (redirects and caching rules that a plugin or your host wrote for you). The wp-config constants are covered in the seven settings I change on every new site.

    Item Where it lives How often
    Database MySQL, all tables with the site prefix Daily, and always before an update
    wp-content/uploads Filesystem Weekly, or after adding a batch of media
    Themes and plugins wp-content/themes, wp-content/plugins Before every update, plus weekly
    mu-plugins wp-content/mu-plugins Always, with the file backup
    Config and rewrite rules wp-config.php, .htaccess Every full backup
    Core files, caches, old archives wp-admin, wp-includes, wp-content/cache Never — reinstall instead

    How often you should back up

    Ask one question: if the site died right now, how much work would I be willing to redo? That answer is your interval. A blog that publishes twice a week does not need a database dump every hour, but it does need one before every plugin update, because that is when things actually break.

    My default for a small site:

    • Database: daily. It is small — usually a few megabytes — so there is no reason to be stingy.
    • Files: weekly, plus a manual snapshot whenever I install something new or edit a theme file.
    • Before anything risky: a manual backup with a name I will recognise, like before-woo-9-update. Updates, migrations, search-replace operations and malware cleanup all qualify.

    Retention matters as much as frequency. I keep a rolling window — daily for a week, weekly for a month, one monthly kept a year — because some damage is silent. A hacked file can sit unnoticed for weeks, and by then every recent backup already contains it.

    A hosting-provider snapshot is not the same as your own backup. Snapshots are fast and I use them, but they live in the same account — if the account is suspended or deleted, the snapshot goes with it. In my hacked-site recovery, the host’s copy was useful but not sufficient.

    Where the backup should live

    Keep three copies, on two different kinds of storage, with one of them off-site. That is the whole rule; everything else is detail.

    Practically: one copy on the server for fast restores, one on your own machine, one in cloud storage you control. If the backup holds user data, encrypt it — a dump with email addresses and password hashes does not belong in a public bucket.

    Two places a backup must never live: inside wp-content (a hack cleanup or a careless migration deletes that folder, and some server configurations will happily serve the archive to anyone who guesses the filename), and only in the same hosting account you are trying to protect yourself from.

    There is also a boring test that catches more problems than any checklist: download a backup and open it. If pulling the file takes hours or the archive refuses to open, you do not have a backup. You have a file with a hopeful name.

    Taking a backup you can actually restore

    On shared hosting, use the host’s own backup tool first — it knows where the database is and it produces an archive you can download in one click. Then keep a second, independent copy through a plugin that ships to remote storage, or by hand if you have SSH access.

    If you have WP-CLI, the whole job is three commands:

    # 1) Database — dump every table, with DROP statements so it can overwrite cleanly
    wp db export ~/backups/db-$(date +%F).sql --add-drop-table
    
    # 2) Files — wp-content plus config, skipping caches and old archives
    tar -czf ~/backups/files-$(date +%F).tar.gz \
      --exclude='wp-content/cache' \
      --exclude='wp-content/updraft' \
      --exclude='wp-content/ai1wm-backups' \
      wp-content wp-config.php .htaccess
    
    # 3) Pull both copies off the server
    scp user@host:~/backups/*-$(date +%F).* ./backups/

    Without SSH, the same result comes from cPanel’s Backup tool plus phpMyAdmin: export the database with the “Add DROP TABLE” option enabled, then zip wp-content in File Manager. It is slower, but it works, and slow beats absent.

    Whatever route you take, check the excludes. The most common silent failure I have seen is a backup job that skipped wp-content/uploads to save space, so the restore brought back every post with a broken image. Back up uploads separately at a lower frequency instead.

    Test the restore, not just the backup

    A backup is only proven when it has been restored once. Do it on a copy — a subdomain, a spare folder, or a local install — never on the live site. Twenty minutes, once a quarter:

    1. Create a clean target. New database, new directory, nothing shared with production.
    2. Import the database dump and unpack the files into the new directory.
    3. Point wp-config.php at the new database. If the domain differs, update the siteurl and home rows in wp_options, then run a serialised-safe search-replace so internal links stop pointing at the live domain. Do not do this with a plain text editor on a dump — serialised data breaks silently.
    4. Open the site and log in. Check the homepage, one post with images, the media library, permalinks (resave them if 404s appear), and the one plugin the business depends on.
    5. Write down the date and anything you had to fix. If the restore failed, you have just found out at the cheapest possible moment.

    That drill is also how you discover that your backup excludes were wrong, your archive was truncated, or your plugin’s remote storage credentials expired months ago.

    The checklist I actually use

    • Database dumped daily, with DROP statements, all prefixed tables included.
    • wp-content archived weekly, uploads and mu-plugins included.
    • wp-config.php and .htaccess saved alongside it.
    • No backup archives stored inside wp-content.
    • At least one copy off the hosting account, encrypted if it holds user data.
    • A manual snapshot before every update, migration or cleanup, with a dated name.
    • A restore tested on a copy this quarter, with the result written down.

    The next time you touch this site, take one manual backup first and time how long the download takes. If it is slower than you expected, or the archive will not open, you have found your real problem before it found you.

  • 7 WordPress Settings I Change on Every New Site

    7 WordPress Settings I Change on Every New Site

    I install WordPress on a fresh domain a few times a year, and the first thirty minutes decide whether the site is easy to run or a slow pile of annoyances. These are the seven settings I change before I write a single post, in the order I change them. None of them needs a plugin, and all of them are reversible except permalinks — which is exactly why permalinks go first.

    The short version

    • Permalinks to /post-name/, set before any content exists.
    • Time zone and admin email set to something you actually use.
    • The “discourage search engines” box checked, then unchecked — and checked again after every migration.
    • Comments closed on old posts, pingbacks and trackbacks off.
    • File editor disabled and revisions capped in wp-config.php.

    1. Permalinks: decide once, before you publish

    Settings → Permalinks → Post name. A fresh install defaults to a date-based structure, which is fine for a diary and bad for everything else: the URL carries the month, the day, and no words anyone would search for.

    The reason this is step one is that changing it after publishing is the only item here that breaks things: every old URL returns a 404 unless you redirect it, and every link you posted elsewhere stops pointing at the right page.

    If the site is already live, change it deliberately: change the structure, then add a redirect from the old pattern. WordPress does that for slugs, not for a permalink change.

    2. General: the time zone and the mailbox nobody reads

    Settings → General. Three fields matter and two of them get ignored on nearly every install I look at.

    • Timezone. Pick your city, not UTC. If you schedule posts, WordPress converts your publish time to UTC and back; a wrong zone means a 9am post goes out at 5pm, and you spend ten minutes convinced the scheduler is broken.
    • Administration Email Address. This is where password resets, new-user notices and critical-error recovery links go. Use a mailbox on the domain that you check, not a personal address you set in 2019 and no longer open.
    • Site title and tagline. The tagline is not decoration — most themes print it under the logo, and “Just another WordPress site” tells a visitor you did not finish setting up.

    While you are here, set the date and time format to something unambiguous. 03/04/2026 means two different days depending on who is reading.

    3. Reading: the checkbox that quietly costs you months

    Settings → Reading → Search engine visibility. “Discourage search engines from indexing this site” should stay unchecked on production. On a staging copy or a site you are rebuilding, it should be checked.

    The failure mode is always the same: you build on staging, launch, and forget. The site looks perfect from your browser because you are logged in and looking at it directly. Google sees a page that asks politely not to be indexed, and six weeks later you are writing forum posts about having zero impressions.

    So I check it twice — once during setup, once after any DNS or host change. It takes ten seconds and it is the single most consequential checkbox in the admin.

    4. Discussion: close the doors you are not watching

    Settings → Discussion. Comments on a small site attract two kinds of traffic: real readers, and bots that found a 2019 post about nothing. The settings that help:

    • Uncheck Allow link notifications from other blogs (pingbacks and trackbacks). They are almost entirely noise on a small site, and they are a known way to get junk anchors onto a page.
    • Set Automatically close comments on posts older than 14 days. Old posts are where the spam lands, because nobody is reading the moderation queue for them.
    • Tick Comment must be manually approved for first-time commenters, and untick nested comments beyond two levels deep if your theme handles them badly.

    If you do not want comments at all, uncheck “Allow people to submit comments on new posts” — but do it in the same sitting, because that checkbox lives on this page and nowhere else.

    5. Media: stop generating image sizes nobody will use

    Settings → Media. Every upload generates several resized copies before you ever insert it. On a site that publishes screenshots and diagrams, that adds up quietly in disk space and in backup size.

    My numbers: thumbnail at 300×300 cropped, medium maxed at 768, large maxed at 1200. Those cover a card, a body image, and a full-width image on the layouts I use. Anything larger I upload deliberately, and WordPress makes a “scaled” copy of very large images anyway.

    Two habits that go with it: rename the file before uploading (wordpress-permalinks-settings.png, not screenshot-2026-10-23-at-11.04.12.png), because the filename becomes part of the URL and the default alt text; and compress before upload instead of relying on a plugin.

    6. wp-config.php: four constants worth typing by hand

    This is the only step that is not a screen in the admin. Edit wp-config.php over SFTP and add these above the line that says That’s all, stop editing:

    define('DISALLOW_FILE_EDIT', true);   // no theme/plugin editor in the admin
    define('WP_POST_REVISIONS', 10);      // cap revisions instead of unlimited
    define('WP_DEBUG', false);            // never true on a live site
    define('WP_DEBUG_LOG', false);        // no wp-content/debug.log on production

    What each one buys you:

    • DISALLOW_FILE_EDIT removes the editor from Appearance and Plugins. It also means a stolen admin session cannot turn into code execution by pasting PHP into a theme file. The trade-off is real: you now need SFTP to change anything in code, which is the point.
    • WP_POST_REVISIONS stops WordPress storing a new row for every autosave. A long editing session on a single post can otherwise leave dozens of copies in the database forever.
    • WP_DEBUG and WP_DEBUG_LOG off on production, because a debug log records file paths and errors you do not want sitting in a web-readable directory. Turn them on for an hour when chasing a fatal error, then off again — the log is what tells you why the site broke, and which log to open is a separate problem.

    7. Users, plugins, and the sample content

    The last pass is cleanup, and it is the pass people skip.

    • Delete the sample post, the sample page, and the default comment. They are published, indexed, and linked from your menu otherwise.
    • Delete Hello Dolly. Delete any plugin you cannot explain in one sentence, including the ones that came bundled with a theme.
    • Create one admin account with a real display name and a password from a manager, and do not use admin as the username — it is the first guess in every brute-force script. If more than one person needs access, give them Editor, not Administrator.
    • On the Plugins screen, turn on auto-updates for the plugins you trust. Core minor releases and security releases already install themselves; the plugin list is where the forgotten one lives.

    If you inherited the site rather than built it, run the checks in how to check if your WordPress site is infected in 5 minutes before you touch settings — there is no point tidying a site that is already serving someone else’s links. And if yours has already been hit, the recovery sequence I wrote up in my WordPress site was hacked and serving Japanese spam covers the order to do things in.

    The 15-minute checklist

    Setting Where Value I use
    Permalinks Settings → Permalinks Post name
    Time zone Settings → General Your city, not UTC
    Admin email Settings → General A mailbox you read daily
    Visibility Settings → Reading Unchecked on live
    Pingbacks Settings → Discussion Off
    Comment ageing Settings → Discussion Close after 14 days
    Image sizes Settings → Media 300 / 768 / 1200
    File editor wp-config.php DISALLOW_FILE_EDIT true
    Revisions wp-config.php 10
    Debug wp-config.php false on production

    Do this one now

    If you only have two minutes, open Settings → Permalinks and set it to Post name while the site is still empty. Everything else on this list can be fixed later without breaking a link; that one cannot.

    Then keep the list somewhere you will find it: the next time you stand up a staging copy, the checkbox in step three is exactly the one that gets left in the wrong position. Settings are cheap to change and expensive to forget.

  • What HTTP 500 Actually Means (and Which Log Tells You Why)

    What HTTP 500 Actually Means (and Which Log Tells You Why)

    A 500 error means the server accepted your request and then crashed while building the page — it is not a WordPress message, not a browser problem, and not something a cache purge will fix. The reason is written in exactly one place: the server’s error log, not the browser console. So the fastest path is always the same: note the time of the failure, open the PHP error log for that minute, and read the first fatal line.

    I have chased this error on my own sites and on client sites more times than I want to admit, and the pattern never changes: people start deactivating plugins one by one. The log already says which part failed. Here is the order I use now.

    The short version

    • 500 = the server failed while generating the page. PHP crashed, or the web server died before PHP even ran. The browser is shown a generic page on purpose.
    • The log you want is the PHP error log, or the nginx/Apache error_log if PHP never got a chance to run.
    • Read the first fatal error in the time window, not the last line. One crash usually drags a dozen warnings and timeouts behind it.
    • 502, 503 and 504 are different animals. Check the status code before you touch anything.
    • No log entry at all usually means .htaccess, a firewall rule, wrong permissions, or a PHP handler mismatch after a version change on the host.

    First, confirm which error you have

    The number matters: each one points at a different layer of the stack. I have wasted an hour reading PHP logs for what was actually an nginx timeout.

    Status What it means Where to look first
    500 Internal Server Error PHP threw a fatal error, or the web server hit a configuration problem it cannot recover from. PHP error log, then the web server error_log.
    502 Bad Gateway The web server asked PHP-FPM for the page and got garbage or nothing back. nginx/Apache error_log, PHP-FPM log (pool crashed, socket missing).
    503 Service Unavailable The server is deliberately refusing work: maintenance mode, overload, or the PHP pool is down. Host status page, server load, PHP-FPM pool status.
    504 Gateway Timeout The request took longer than the proxy was willing to wait. PHP max_execution_time, proxy timeouts, a slow external API call.

    If the page shows 502 or 504, skip straight to the server logs — WordPress never finished the request, so it has nothing to say.

    Where the log actually lives

    It depends on who runs PHP. Three places cover almost everything I see.

    1. Shared hosting (cPanel-style). The account usually has a logs directory next to public_html with a per-domain error log inside — on LiteSpeed servers it is named after the domain, like example.com.error.log. The cPanel Errors page lists recent PHP errors too, and it is the fastest way in when you only have the file manager.

    2. A VPS or dedicated server you manage. The web server and PHP-FPM keep their own logs:

    • nginx: /var/log/nginx/error.log
    • Apache on Debian/Ubuntu: /var/log/apache2/error.log
    • Apache on RHEL/AlmaLinux: /var/log/httpd/error_log
    • PHP-FPM: /var/log/php-fpm/www-error.log, or /var/log/php8.x-fpm.log depending on the package

    3. Inside WordPress. If you have file access, you can make WordPress keep its own log. This only catches errors that happen after WordPress has loaded, so it is a complement to the server log, not a replacement.

    // wp-config.php — log PHP errors to a file instead of printing them
    define( 'WP_DEBUG', true );
    define( 'WP_DEBUG_LOG', true );      // writes wp-content/debug.log
    define( 'WP_DEBUG_DISPLAY', false ); // keeps errors out of the HTML
    @ini_set( 'display_errors', 0 );

    Then follow the file while you reload the broken page:

    # shared hosting (adjust the account name and domain)
    tail -f /home/USERNAME/logs/example.com.error.log
    
    # VPS with nginx + PHP-FPM
    sudo tail -n 200 /var/log/nginx/error.log
    sudo tail -n 200 /var/log/php-fpm/www-error.log

    The log line that answers the question

    A typical PHP fatal looks like this, and every part of it is useful:

    [21-Oct-2026 13:43:07 UTC] PHP Fatal error:  Uncaught Error: Call to undefined function
    wc_get_orders() in /home/user/public_html/wp-content/plugins/acme-tools/acme.php:42
    Stack trace: #0 {main}  thrown in /home/user/public_html/wp-content/plugins/acme-tools/acme.php on line 42

    Four things to pull out of it:

    1. The timestamp. Compare it to the time the page broke. Logs are often in the server’s timezone or UTC, so account for the offset before deciding the entry is unrelated.
    2. The severity. Fatal error and Uncaught Error stop the request. Warning and Notice usually do not, and chasing those first wastes an afternoon.
    3. The path and line number. That path is usually the answer: it names the theme or plugin file that was running when the process died.
    4. The message. It names the mechanism: undefined function, memory exhausted, execution time exceeded, permission denied.

    A few messages come up again and again, and each one has a standard first move:

    • “Allowed memory size of N bytes exhausted” — something is loading more than the limit allows. Raise memory_limit a step to confirm, then find the code or import doing it.
    • “Maximum execution time exceeded” — a loop, a huge import, or a slow external request. If the same URL hangs in a browser tab, it is a page load, not a background job.
    • “Call to undefined function” — a plugin calling code that is not loaded: a plugin deleted while another depends on it, or a PHP extension missing on the new server.
    • “Cannot redeclare” — the same function defined twice, usually two plugins from the same codebase.

    When you see dozens of warnings in the same minute, ignore them until you find the fatal. The last line in a crash window is almost never the cause.

    My five-minute triage order

    1. Write down the URL and the time — one line in a notes app. “It broke earlier today” is not a time.
    2. Open the account error log from the panel’s file manager or the cPanel Errors page.
    3. Find the fatal in that window and read the file path. If it points into a plugin, you know the culprit.
    4. If the log is empty, test the three things that produce a 500 without a PHP entry: rename .htaccess to .htaccess.bak and reload (a bad rule leaves no PHP trace); check whether the host changed the PHP version overnight; check the panel’s firewall log for a blocked request.
    5. If it is a plugin, rename the plugins folder through the file manager so WordPress disables everything, then put plugins back in halves. Faster than the admin screen when the admin screen also returns 500.
    6. Check disk usage. A full disk or exhausted inode limit makes writes fail and pages return 500 with a log entry that looks unrelated.

    When it is not your code

    Some 500s start and end without you changing anything, and that is a strong signal. If several sites on the same server break in the same minute, the PHP-FPM pool or the web server went down and plugin bisecting will not help. If only one action fails — a form submission, a checkout — check the firewall log first, because a security rule update can start blocking a POST body it used to allow.

    Two checks settle it fast: does a plain HTML file in the same account load, and did the failure start without a deploy? A static file that loads while every PHP page fails points at the PHP layer, not the site. And if the log is empty while the site serves pages or links you did not write, treat it as a compromise rather than an error — my five-minute infection check covers the first things to look at, and the recovery walkthrough covers what to do when you find injected files.

    What I do after the fix

    The fix is only half the job. The other half is making the next 500 take one minute instead of one evening:

    • Keep the log path in the notes for every site — host, panel, log location, PHP version.
    • Leave WP_DEBUG_LOG on for staging, and WP_DEBUG_DISPLAY off everywhere.
    • Watch the size of debug.log. An unrotated debug log can fill a disk, and a full disk causes the exact 500 you were debugging.
    • Update plugins one at a time on staging, not twelve at once at midnight.

    If you are stuck in the middle of one right now and want a second pair of eyes, my contact page is the place to reach me. Otherwise: open your site’s error log today, before you need it. Ten minutes now saves the panic later, because you will already know where the answer lives.

  • How to Check If Your WordPress Site Is Infected in 5 Minutes

    How to Check If Your WordPress Site Is Infected in 5 Minutes

    You can tell whether a WordPress site is infected in about five minutes without installing anything: check five places in order — robots.txt, your sitemap, the administrator list, the plugin list, and the newest files in wp-content. If two or more of those come back with something you did not put there, treat the site as compromised and work from that assumption. The order matters because it runs from cheapest to slowest, and a single odd result is usually a false alarm while a pattern is not.

    I ran exactly this sequence on my own blog after it started serving Japanese spam, and the first check was enough to confirm it. Here is the whole thing, with what each result actually means.

    The short version

    • robots.txt — fetch it raw. Attackers add Disallow or Sitemap lines pointing at paths and domains you never created.
    • Sitemap — open /wp-sitemap.xml and look for URLs that are not your posts.
    • Users — filter the list to Administrators. Note anything you did not create; delete nothing yet.
    • Plugins — compare against what you remember installing. Unknown folders, missing authors, and deactivated-but-present plugins are the tells.
    • Files — sort wp-content by modified date. Anything changed in the last 48 hours that you did not touch is a suspect.

    Check 1: robots.txt takes ten seconds

    Open https://yoursite.com/robots.txt in a browser with no cache, or fetch it from the command line. A normal WordPress install has almost nothing in it:

    User-agent: *
    Disallow: /wp-admin/
    Allow: /wp-admin/admin-ajax.php
    
    Sitemap: https://yoursite.com/wp-sitemap.xml
    

    What you are looking for is anything extra. Injected lines usually look like this:

    User-agent: *
    Disallow: /go/
    Sitemap: https://example-spam-domain.top/sitemap.xml
    

    A Disallow on a path you never created is the attacker hiding a doorway directory from crawlers. A Sitemap line pointing off your domain is the attacker advertising theirs. Either one is a confirmed infection — nothing legitimate writes those lines into robots.txt.

    Note that the file may be generated by a plugin such as Yoast or Rank Math, or it may be a physical file on disk. If it is static, check its timestamp in your file manager as well: a modification date you cannot explain is its own signal.

    Check 2: the sitemap shows what got published behind your back

    Open https://yoursite.com/wp-sitemap.xml, or /sitemap_index.xml if a plugin manages it. Then open the posts sitemap itself and scan for words you would never write — brand names, a language you do not publish in, or a redirect path like /go/.

    Two different things show up here, and they mean different things:

    • URLs that resolve to real posts — someone published content through your dashboard. Check the post author and the revision history before deleting anything.
    • URLs that 404 or redirect — the entries were injected into the sitemap rather than created as posts. That points at a modified theme file or a plugin, not at a hijacked login.

    If your sitemap is clean but robots.txt carried an injected Sitemap: line, the attacker is advertising their own sitemap instead of yours. That is still an infection, and it still needs the same cleanup.

    Check 3: the administrator list

    In the dashboard go to Users and filter by Administrator. Count them, then look at the email domain and the username pattern of anyone you do not recognise. Attacker accounts very often use a throwaway address and a name that would look like a normal subscriber if you only glanced at it.

    Do not delete the account yet. Deleting a user while the malware is still live can trigger a re-creation routine on the next page load, and you lose the only evidence of how it got in. Note the username, change your own password, and enable two-factor authentication first. If a plugin on your site records last login, check whether the suspicious account has ever actually logged in: an administrator that has never logged in was probably created by a script, which means the entry point is still open.

    Check 4: plugins and themes

    Open Plugins → Installed Plugins and actually read the list rather than scanning for update badges. The tells are boring, which is exactly why they get missed:

    • A plugin you did not install, often with a generic name like “WP Cache Manager Pro” or unusual capitalisation.
    • A plugin that is deactivated but still present. A deactivated plugin is not executed, so malware packs usually pair it with a must-use plugin that re-activates it.
    • An author field that is empty, or a version number that does not match the plugin’s real release history.
    • A second theme next to your active one that you never installed.

    Now open wp-content/mu-plugins through your host’s file manager. Must-use plugins have no dashboard entry at all, load on every request, and cannot be deactivated from the admin. If that folder holds files you did not add, you have found the loader.

    Check 5: files that changed recently

    This is the slowest check and the most conclusive. Open the file manager or connect over SFTP, go to wp-content, and sort by last modified, newest first. Then look at three things.

    PHP files inside uploads/. There is no legitimate reason for an uploads folder to contain a .php file. One is enough to be certain: that is a backdoor.

    Core files. Files in wp-includes/ and wp-admin/ should change only when you update WordPress. If their timestamps do not line up with your last update, compare them against a fresh copy of the same version and diff the two.

    Your theme’s functions.php. Attackers like it because it loads on every request and scanners tend to ignore theme files unless they recognise the payload. Look for long base64 blobs, eval(), gzinflate(), or an unexplained block of code at the very top of the file.

    What the results mean

    What you found What it usually means Next step
    Injected lines in robots.txt Confirmed infection, spam injection Restore robots.txt, then find the loader in mu-plugins
    Unknown URLs in the sitemap Posts created through the dashboard, or an injected sitemap Check post authors and revisions
    An admin you did not create Dashboard credentials are compromised Change every password, enable 2FA, then remove the account
    Unknown or deactivated plugin Malware pack left behind after an earlier cleanup Remove the folder entirely, not just deactivate it
    PHP in uploads/ or a modified core file Active backdoor Full restore from a clean backup, then rotate keys and passwords
    Only old timestamps, nothing unusual Probably not infected Re-run in 24 hours; the checks are cheap

    A five-minute checklist you can repeat

    1. Fetch /robots.txt and read every line.
    2. Open the sitemap and scan for foreign URLs.
    3. Count administrators against who should be there.
    4. Read the plugin list and check mu-plugins.
    5. Sort wp-content by modified date and look at the top ten entries.

    Run it once a month, and again the moment something changes: a sudden drop in traffic, a manual action in Search Console, or a page ranking in search that you never wrote. The checks do not prevent an infection, but they shorten the gap between the infection and the cleanup — and that gap is the part that actually costs you traffic and trust.

    Start with robots.txt today. It is ten seconds, and it is the single check most likely to tell you the truth on the first try. If you have already found something, the full order of operations I used is written up in my post on recovering a hacked WordPress site. If you are unsure whether a specific file is legitimate, send me the first twenty lines through the contact page and I will tell you what it does. More write-ups like this live under the WordPress category.

  • My WordPress Site Was Hacked and Serving Japanese Spam — Exactly How I Recovered

    My WordPress Site Was Hacked and Serving Japanese Spam — Exactly How I Recovered

    On October 5, 2026, I opened imivantan.com and got a white page with five words: This page isn’t working. HTTP ERROR 500.

    I assumed it was a plugin conflict. It wasn’t.

    When I checked robots.txt, I found a list of sitemaps I had never created. When I opened sitemap.xml, my domain was serving a Japanese hardware store — complete with a “25th anniversary sale” banner, power tools, kerosene delivery and machine rental.

    My blog had been hacked. The attacker wasn’t interested in my content — they were using my domain to rank Japanese e-commerce pages.

    This is what I found, why I chose to rebuild instead of repair, and the checklist I used to come back clean.

    The short version

    • Every page on the site returned HTTP 500, while parts of the domain were still serving spam.
    • robots.txt had been rewritten to advertise sitemaps I never created.
    • sitemap.xml rendered a fake Japanese storefront in HTML instead of XML.
    • The spam sitemaps were regenerated daily — the kit was still active.
    • Wayback Machine showed the site was healthy on 18 June 2026, so the compromise happened after that.
    • The old WordPress install couldn’t be trusted, so I rebuilt it from scratch.

    What I actually found

    The homepage was dead. So was every other page I tried — posts, categories, even wp-login.php. That’s the HTTP 500 error in the screenshot below.

    But robots.txt was alive, and it looked like this:

    User-agent: *
    Allow: /
    
    Sitemap: https://imivantan.com/?sitemapindex.xml
    Sitemap: https://imivantan.com/?sitemap254.xml
    Sitemap: https://imivantan.com/?sitemap113.xml
    Sitemap: https://imivantan.com/?sitemap61.xml
    Sitemap: https://imivantan.com/?sitemap177.xml
    Sitemap: https://imivantan.com/?sitemap230.xml
    Sitemap: https://imivantan.com/goods.php?sitemap390.xml

    I have never used sitemap files with random numbers in the filename. I have never had a goods.php. And my site has never sold anything.

    The sitemap index pointed to hundreds of pages like /products/suicide/p072000115 and /composition/charm/n39zdzba245px.shtml. Opening the sitemap URL itself produced this:

    A fake Japanese hardware store page served from imivantan.com/sitemap.xml, with a 25th anniversary banner and Japanese product categories such as power tools and kerosene delivery

    Every spam URL I checked had a <lastmod> date of that same day. This wasn’t an old infection sitting quietly in a corner — the kit was generating new pages on a schedule.

    What is the “Japanese keyword hack”?

    It’s a known pattern, documented by Google as the Japanese keyword hack. Attackers compromise a site — very often an outdated WordPress install — and inject pages, sitemaps and redirects that target Japanese search queries. The pages usually promote products the site owner has nothing to do with: tools, supplements, manga sets, rental services.

    The site owner often doesn’t notice, because the hacked pages are hidden from normal visitors or served only to search engines. In my case the main site was already broken, which is how I found it.

    Why should you care even if your site is small? Because the damage isn’t the spam pages themselves. It’s what Google does when it finds them: security warnings, manual actions, lost trust — and a domain that suddenly has to prove it’s legitimate again.

    The timeline

    Timeline: 18 June 2026 last healthy archive, 5 October 2026 site down with Japanese spam, 21 October 2026 rebuilt on clean WordPress

    Three dates matter. The site was healthy in the Wayback Machine on 18 June 2026. By 5 October it was down and serving spam. I rebuilt it on 21 October.

    How I diagnosed it (five checks you can run today)

    1. Check the status code. curl -I https://yourdomain.com/ — a 500 on every page usually means the PHP application is broken, not that the whole server is down.
    2. Read robots.txt. Unknown sitemap URLs are the fastest red flag. Attackers rewrite this file because search engines read it first.
    3. Open sitemap.xml. It should be XML. If it renders a web page — especially in another language — your rewrites have been hijacked.
    4. Check the Wayback Machine. The CDX API gives you a quick history: web.archive.org/cdx/search/cdx?url=yourdomain.com*&output=json. Find the last capture that looks normal.
    5. Open Google Search Console. Look at Security Issues and Manual Actions. If there’s a hack warning, you’ll see it there before you see it in traffic.

    Why I rebuilt instead of cleaning

    I could have tried to remove the spam files and patch the old install. I didn’t, for four reasons:

    • The site was fully down. The old install wasn’t just infected — it wasn’t serving anything.
    • I couldn’t confirm the entry point. Without knowing how they got in, “cleaning” is guesswork. Backdoors are designed to survive cleanups.
    • There was no verified clean backup. A backup taken after the compromise just restores the compromise.
    • The content was small. Around ten posts — recoverable from the Wayback Machine and my own notes. A fresh install was faster and safer than forensics on a dead site.

    If you have a backup you know is clean, restoring is a perfectly good option. If you don’t — rebuild. Your site is not worth more than your domain’s reputation.

    The recovery checklist I used

    1. Preserve evidence first. Screenshot the error page, robots.txt and a few spam URLs. You’ll need them for your host, your developer and Google Search Console.
    2. Check the server, not just the site. Look at other sites on the same hosting account, cron jobs, mail logs, FTP/SSH users and hosting panel accounts. One hacked site on a shared server is everyone’s problem.
    3. Rebuild or restore. Fresh WordPress, or a backup you can prove is clean. Update core, themes and plugins before adding anything else.
    4. Rotate every credential. WordPress admin, database, SFTP/SSH, hosting panel, mailbox, registrar. All of them — assume the old ones are known.
    5. Harden the new install. Security plugin, two-factor authentication, no unused plugins or themes, XML-RPC disabled if you don’t use it, file editing off.
    6. Clean up for Google. In Search Console, check Security Issues, then submit a fresh sitemap and request a review if you were flagged. Make sure old spam URLs return 404 or 410.
    7. Set up backups you will actually test. Automatic, off-server, and restore one to prove it works.
    8. Monitor. Uptime monitoring plus a monthly look at robots.txt and sitemap.xml. Ten minutes a month is cheaper than a rebuild.

    What I changed on the new site

    The new install is deliberately boring. A clean WordPress, the default theme, two plugins, one admin account with a unique password stored in a password manager, and an email address on my own domain for site notifications.

    I also cleaned out the demo content that came with the install — sample pages, placeholder navigation, a footer full of links to nowhere. A small thing, but if you’re rebuilding anyway, start with a site you’d be happy to show someone.

    Still on the list: a security plugin with login protection, scheduled off-site backups, and Search Console verification for the new domain setup. I’ll write about those as I finish them.

    What this actually cost me

    About two hours of diagnosis, an evening of rebuilding, and a domain that now has to earn back trust. No dramatic traffic loss to report — the site was small and the blog was just starting. That’s the honest version, and it’s exactly why I didn’t try to save a broken install.

    Five things I’d tell anyone running WordPress

    • Your robots.txt and sitemap are public. Check them once a month. Most owners never do.
    • WordPress is a target. Not because it’s insecure by design, but because it’s popular. Updates are not optional.
    • A backup you haven’t restored is not a backup. Test one before you need one.
    • Hacks are usually invisible. This one didn’t deface my site — it quietly turned my domain into someone else’s storefront.
    • Own your platform. A self-hosted WordPress install is what let me rebuild in hours instead of waiting on a platform’s support queue.

    What’s next

    Now that the site is clean, the real work starts: publishing consistently, getting indexed again, and documenting what actually moves the needle for a small site in 2026.

    If you run WordPress, open yourdomain.com/robots.txt right now. If you see sitemap URLs you don’t recognize, start with step one of the checklist above — and don’t wait for the traffic to tell you something’s wrong.

    Ivan.T builds and runs small websites, including a hosting company and a PDF tools site. More about me.