You migrate WordPress to a new server, switch your permalink structure to “Post name,” and suddenly every page except the homepage returns a 404. The fix sounds simple — but the root cause is almost never where you expect it.
What’s Actually Happening
When WordPress is set to use Plain permalinks (e.g. /?p=123), every request goes directly to index.php with a query string. PHP handles it — no server magic required.
The moment you switch to a “pretty” structure like /sample-post/ or /category/post-name/, WordPress no longer appends a query string. The browser sends a request for a path (/about-us/) that doesn’t physically exist on disk. The web server needs to be told: “If this file or folder doesn’t exist, hand the request to WordPress’s index.php and let it figure it out.”
That instruction lives in rewrite rules. If the server doesn’t have them — or can’t read them — every non-homepage URL 404s. The homepage works because / maps directly to index.php, which always exists.
On a shared host, these rules are often pre-configured invisibly. When you migrate to a VPS or a different server stack, you’re starting from scratch — and the defaults rarely include WordPress rewrite support out of the box.
All the Reasons This Can Happen
- 01Missing or malformed .htaccess The WordPress-generated rewrite block is absent, corrupted, or was overwritten during a plugin conflict or FTP transfer.
- 02mod_rewrite not enabled (Apache) Apache’s rewrite engine is a loadable module. On a fresh Ubuntu/Debian install, it is disabled by default and must be explicitly activated.
- 03AllowOverride not set to All (Apache) Even with mod_rewrite active, Apache won’t read your
.htaccessunless the virtual host config hasAllowOverride All. The default is oftenNone. - 04Wrong document root or rewrite path (OpenLiteSpeed) OLS stores the
.htaccesspath in its vhost config. A typo, double slash, or stale migration path means OLS reads the rules from a file that doesn’t exist or is the wrong location. - 05try_files directive missing (Nginx) Nginx doesn’t use
.htaccessat all. Rewrite logic must live inside the server block. Without the correcttry_filesdirective, Nginx serves a 404 for every pretty URL. - 06Rewrite not enabled in OLS WebAdmin OpenLiteSpeed has a per-virtual-host toggle for rewrite engine and
.htaccessauto-loading. Both can be independently disabled. - 07File permission or ownership issues If the web server user (
nobody,www-data) can’t read.htaccess, it silently skips it — no error, just 404s. - 08Permalink settings not saved after migration WordPress writes its own
.htaccessblock when you click Save on the Permalinks page. If you restored a database but never visited that settings page, the block may be outdated or completely missing. - 09WordPress site URL mismatch If
siteurlorhomein the database still points to the old domain/IP, WordPress generates wrong internal redirect chains that break pretty URLs silently. - 10A caching or CDN layer serving stale 404s A misconfigured Cloudflare rule, server cache, or LiteSpeed Cache plugin can cache a 404 response and serve it indefinitely — even after the underlying rewrite rules are fixed.
Fix By Server Type
Find your web server below and follow only that section. You can confirm your server with:
# Check which web server is running
apache2 -v # Apache
nginx -v # Nginx
ls /usr/local/lsws # OpenLiteSpeedStep 1 — Enable mod_rewrite
sudo a2enmod rewrite
sudo systemctl restart apache2Step 2 — Set AllowOverride in your VHost config
Open your site’s Apache config (usually in /etc/apache2/sites-available/):
<VirtualHost *:80>
ServerName yourdomain.com
DocumentRoot /var/www/html
<Directory /var/www/html>
Options Indexes FollowSymLinks
AllowOverride All # ← This is critical
Require all granted
</Directory>
</VirtualHost>Step 3 — Verify .htaccess content
cat /var/www/html/.htaccessIt must contain the WordPress rewrite block. If it doesn’t, recreate it:
cat > /var/www/html/.htaccess << 'EOF'
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
EOF
sudo chown www-data:www-data /var/www/html/.htaccess
sudo chmod 644 /var/www/html/.htaccess
sudo systemctl restart apache2Nginx ignores .htaccess completely. All rewrite logic must be placed directly in the Nginx server block config.
Step 1 — Edit your Nginx site config
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
root /var/www/html;
index index.php index.html;
# WordPress permalink rewrite — this is the critical block
location / {
try_files $uri $uri/ /index.php?$args;
}
# Pass PHP to FastCGI
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
# Block .htaccess access (not needed but good practice)
location ~ /\.ht {
deny all;
}
}Step 2 — Test and reload
sudo nginx -t # Test config — must say OK
sudo systemctl reload nginxUsing try_files $uri $uri/ /index.php; without ?$args will break WordPress’s query strings (pagination, search, etc.). Always include ?$args.
Step 1 — Check and fix the vhost config rewriteFile path
This is the most common OLS-specific cause. Open your vhost config:
# Find this block inside context / { ... rewrite { ... } }
# WRONG — double slash or wrong path:
rewriteFile /var/www//.htaccess
# CORRECT — must exactly match your WordPress root:
rewriteFile /var/www/html/.htaccessStep 2 — Ensure rewrite is enabled in the same config
context / {
location $VH_ROOT
allowBrowse 1
indexFiles index.php
rewrite {
enable 1
inherit 1
rewriteFile /var/www/html/.htaccess
}
}
rewrite {
enable 1
autoLoadHtaccess 1
}Step 3 — Fix file ownership and restart
# OLS runs as 'nobody' — confirm with: ps aux | grep lshttpd
chown nobody:nobody /var/www/html/.htaccess
chmod 644 /var/www/html/.htaccess
sudo /usr/local/lsws/bin/lswsctrl restartStep 4 — Alternatively, use OLS WebAdmin Panel
Go to https://YOUR_IP:7080 → Virtual Hosts → [your vhost] → Rewrite and confirm:
- Enable Rewrite = Yes
- Auto Load from .htaccess = Yes
Save → Graceful Restart.
Step 1 — Install URL Rewrite Module
IIS doesn’t ship with URL rewriting. Download and install the URL Rewrite 2.1 module from Microsoft’s official IIS downloads page, then restart IIS.
Step 2 — Create web.config in your WordPress root
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="WordPress" patternSyntax="Wildcard">
<match url="*" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Rewrite" url="index.php" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>The Final Step — Always Do This Last
Regardless of server type, after any rewrite config change, go to:
WP Admin → Settings → Permalinks → click Save Changes
This forces WordPress to regenerate the .htaccess rewrite block using the correct structure for your current permalink setting. No change needed — just open the page and save.
Quick Reference
| Server | Rewrite Config Location | Key Check |
|---|---|---|
| Apache2 | /etc/apache2/sites-available/*.conf | AllowOverride All + mod_rewrite enabled |
| Nginx | /etc/nginx/sites-available/*.conf | try_files $uri $uri/ /index.php?$args; |
| OpenLiteSpeed | /usr/local/lsws/conf/vhosts/*/vhconf.conf | Correct rewriteFile path, enable=1 |
| IIS | web.config in WordPress root | URL Rewrite Module installed |
Bonus: Clear All Caches
After fixing the server config, if pages still 404, clear every cache layer in this order:
1. WP plugin cache (LiteSpeed Cache, W3 Total Cache, WP Rocket) → Purge All
2. Cloudflare → Caching → Purge Everything
3. Your browser → hard refresh (Ctrl+Shift+R / Cmd+Shift+R)
4. OLS / LiteSpeed server cache:
sudo /usr/local/lsws/bin/lswsctrl restart
5. PHP OPcache:
php -r "opcache_reset();" # or restart PHP-FPMSummary
WordPress pretty permalinks require the web server to redirect all non-existent paths to index.php. This works automatically on managed hosts because the stack is pre-configured. On a self-managed VPS or after a server migration, you have to set it up yourself. The exact mechanism varies by server — .htaccess + mod_rewrite for Apache, a try_files directive for Nginx, a correctly pointed rewriteFile for OpenLiteSpeed, and a web.config with URL Rewrite Module for IIS — but the underlying principle is identical across all of them.
Once the server-level config is in place, always finish by re-saving your permalink settings in the WordPress admin. That single click regenerates the correct rewrite block and flushes WordPress’s own internal rewrite cache.





























