Common causes
- The .htaccess file is missing or lacks the WordPress rewrite block (common after migration)
- Apache AllowOverride is set to None, so .htaccess is ignored
- mod_rewrite is not enabled on Apache
- Nginx has no try_files rule passing requests to index.php
- WordPress installed in a subdirectory with a wrong RewriteBase
- A plugin or custom post type registered rewrites without flushing them
How to fix it
- Re-save permalinks. Go to Settings > Permalinks and click Save Changes without changing anything. This flushes WordPress's rewrite rules and rewrites .htaccess if it is writable.
- Check .htaccess exists. In the WordPress root (enable hidden files), confirm .htaccess contains the '# BEGIN WordPress' block. If not, add it manually.
- Enable AllowOverride on your own Apache server. In the vhost's <Directory> block set AllowOverride All (or at least FileInfo), then reload Apache. Shared hosts already allow this.
- Enable mod_rewrite. On Debian/Ubuntu run sudo a2enmod rewrite and sudo systemctl reload apache2.
- Add try_files on Nginx. Nginx ignores .htaccess. In the site's location / block use try_files $uri $uri/ /index.php?$args; then run nginx -t and reload.
- Fix subdirectory installs. If WordPress lives in /blog/, set RewriteBase /blog/ and RewriteRule . /blog/index.php [L] in that folder's .htaccess.
.htaccess (WordPress root, Apache/LiteSpeed)
# 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 How to stop it happening again
- Include .htaccess (a hidden file) in migrations and backups
- Flush permalinks after activating plugins that add custom post types
- Keep your Nginx WordPress config in version control