WordPressTroubleshootingConfigurationHostingWordPress 6.xWordPress 7.xIntermediateCurrent

How to Fix WordPress Permalinks Returning 404 After a Migration or Server Change

A practical fix for WordPress 404 errors on every inner page while the homepage still loads: flush rewrite rules, restore .htaccess or repair the Nginx config.

The symptom pattern that identifies the problem

You migrate a WordPress site to a new server, or restore it from a backup, or switch PHP versions. The homepage loads fine. wp-admin works. But every single inner page — posts, pages, category archives — returns a 404. The content is not gone: the same post is still reachable at example.com/?p=123, and it still appears in the admin area. Only the “pretty” URLs fail.

That exact combination — homepage alive, inner pages 404, content intact — is the signature of broken rewrite rules. WordPress generates pretty permalinks through a front controller pattern: the web server rewrites requests that don’t match a real file or directory to index.php, and WordPress then parses the URL against its rewrite rule table. When either half of that mechanism breaks, the web server looks for a literal file at /2026/10/some-post/, finds nothing, and serves its own 404 page before WordPress is ever involved.

This article walks through the diagnosis and the fix for both Apache and Nginx. If your situation is different — the homepage is also broken, or you are seeing a white screen — start with our guide to fixing WordPress 500 errors instead.

Before you change anything: two 60-second checks

Confirm the content still exists. Take a known post ID and load it with a plain permalink: https://example.com/?p=123. If the post renders, the database is intact and the problem is purely in the rewrite layer. If the plain URL also 404s, you have a different problem — most likely a failed migration or a wrong home/siteurl value, which is covered in our guide to moving WordPress to a new domain.

Identify your web server. The fix differs completely between Apache and Nginx, and applying the wrong one wastes time. Check the Server response header with your browser’s developer tools, or ask your host. As a rule of thumb: shared hosting with cPanel is almost always Apache (or LiteSpeed, which reads the same .htaccess files); VPS setups configured by hand are often Nginx.

One more distinction worth making before you start: decide whether the URLs should still work. If you deliberately changed the permalink structure or the domain during the migration, old URLs returning 404 is expected behavior until you add redirects — that is a redirect-mapping task, not a rewrite-repair task, and conflating the two leads to hours of confused debugging.

Step 1: Flush the rewrite rules from the admin area

WordPress stores its compiled rewrite rule table in the database (the rewrite_rules option). This cache is regenerated on demand, and it can end up stale or empty after a migration, a database restore, or a plugin that registers custom post types loading in the wrong order.

The flush is deliberately unglamorous:

  1. Log in to wp-admin.
  2. Go to Settings → Permalinks.
  3. Change nothing. Click Save Changes.

Saving the screen regenerates the rewrite rules and, on Apache, makes WordPress attempt to write the .htaccess file. WordPress’s own documentation on customizing permalinks notes that pretty permalinks depend on server configuration being in place — if the server layer is broken, this step alone won’t fix the 404s, but it is free, safe, and fixes a meaningful share of cases. Start here always.

If WordPress cannot write .htaccess, it will display the rules you need to add manually — a signal that takes you straight to Step 2.

Step 2: On Apache, check or recreate .htaccess

On Apache, pretty permalinks depend on a .htaccess file in the WordPress root containing mod_rewrite rules. This file goes missing constantly during migrations: it is hidden by default in FTP clients, some backup tools skip dotfiles, and some control panels drop it when recreating the document root.

Connect via SFTP or your host’s file manager (enable “show hidden files”), and look for .htaccess in the same directory as wp-config.php. If it is missing or looks mangled, create it with the default rules. The Advanced Administration Handbook publishes the canonical block; WordPress wraps it between its own # BEGIN WordPress and # END WordPress markers, which you should keep so WordPress can manage the block on future updates. The rules themselves are short enough to reproduce in full:

<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>

The last three lines are the ones doing the permalink work: if the request matches neither an existing file (!-f) nor an existing directory (!-d), hand it to index.php. If WordPress runs in a subdirectory, RewriteBase and the final rule need to reflect that path.

Three server-side conditions can make a correct .htaccess fail anyway:

  • mod_rewrite not enabled. On a self-managed server, run a2enmod rewrite and restart Apache. On shared hosting it is always enabled.
  • AllowOverride disabled. Apache ignores .htaccess unless the virtual host sets AllowOverride All for the document root. This is the classic cause when a site works on old hosting and 404s on identical new hosting: the file copied over fine, but the new virtual host never allowed it to be read.
  • File permissions. The web server user needs read access; 644 is the standard setting.

If you prefer WP-CLI, wp rewrite flush --hard updates both the database rules and .htaccess — but the command reference documents two caveats worth knowing: --hard only writes .htaccess if mod_rewrite is listed under apache_modules in your wp-cli.yml, and it works only on single-site installs.

Step 3: On Nginx, repair the server block

Nginx has no .htaccess equivalent — WordPress cannot write its own rewrite rules, which means no amount of clicking Save Changes in the permalinks screen will fix a broken Nginx setup. The fix lives in the server block.

The WordPress Nginx documentation gives the single line that makes pretty permalinks work:

location / {
    try_files $uri $uri/ /index.php?$args;
}

try_files first attempts the request as a literal file, then as a directory, and only then falls back to index.php with the query string preserved. The handbook specifically calls out keeping the ?$args portion, because non-default permalinks break without it.

When inner pages 404 after a migration to Nginx, look for these specific mistakes:

  • A location / block missing entirely, or a root directive pointing at the wrong directory, so the file check never falls through to index.php.
  • A stray try_files $uri $uri/ =404; copied from a generic static-site template — the =404 terminal is exactly what produces clean 404s instead of handing requests to WordPress.
  • Forgetting to reload after editing: run nginx -t to validate the config, then systemctl reload nginx. Nginx does not pick up changes otherwise.

Step 4: Verify, and handle genuinely changed URLs

After applying the fix, test an inner page in a private browser window. If it loads, spot-check a paginated archive and a page with a custom permalink structure, since those exercise different rules.

If the 404s persist on old URLs even though the site works, you have crossed from repair into redirect territory: the URLs themselves changed. Map the old patterns to the new ones with 301 redirects, either through a redirects plugin or at the server level. Skipping this step after a structure change quietly discards accumulated search rankings — a cost that compounds over time, which is one reason migration projects deserve the planning discipline we describe in our WordPress development services overview.

Why this happens, and how to prevent a repeat

Understanding the mechanism makes the prevention obvious. Pretty permalinks need two cooperating pieces: the web server’s rewrite configuration (.htaccess on Apache, the server block on Nginx) and WordPress’s compiled rule table in the database. A migration touches both — files move, databases import, server configs are rebuilt by hand — and the two halves drift out of sync. The failure is almost never random; it is a migration checklist gap.

Two habits close that gap. First, include the dotfiles in every backup and verify .htaccess (or the Nginx config snippet) landed in the destination before switching DNS. Second, treat “load one inner page and one admin screen” as a release gate after any server change, not an optional courtesy. Comparing platforms before you commit to one also helps: some hosting environments make this entire class of failure structurally impossible, a factor worth weighing when you are deciding on a platform migration.

More background on how WordPress platforms behave under operational stress is in our WordPress platform overview.

Related guides

Continue with the surrounding work.

More WordPress guides
Related insights

Broader context from the YCTheme blog.

Article

When to Migrate a Website to a New Platform

A decision framework for moving between WordPress, Joomla, TYPO3 and Shopify: the signals that justify a migration and how to plan one without losing search visibility.