If your WordPress URLs look like example.com/index.php/2024/01/my-post/ instead of the clean example.com/my-post/, it means WordPress cannot use URL rewriting and is falling back to including index.php in every URL. This is usually caused by a missing or misconfigured Apache module, incorrect .htaccess permissions, or a server that does not support the required rewrite rules. This guide covers all the common causes and fixes.
Step 1: Check Permalink Settings
First, verify that WordPress is configured to use pretty permalinks:
- Go to Settings > Permalinks in the WordPress admin dashboard.
- Select any structure other than "Plain" (e.g., "Post name" is the most common choice).
- Click Save Changes.
When you save permalink settings, WordPress attempts to write rewrite rules to the .htaccess file. If it cannot write to the file, it will show a notice at the bottom of the page with the rules you need to add manually.
Step 2: Enable mod_rewrite (Apache)
WordPress pretty permalinks require Apache's mod_rewrite module. Check if it is enabled:
# Check if mod_rewrite is loaded:
apache2ctl -M | grep rewrite
# If no output, enable it:
sudo a2enmod rewrite
sudo systemctl restart apache2
# Verify it is now loaded:
apache2ctl -M | grep rewrite
# Output: rewrite_module (shared)
Step 3: Set AllowOverride to All
Even with mod_rewrite enabled, Apache must be configured to allow .htaccess files to override the server configuration. The AllowOverride directive controls this:
# Edit your VirtualHost or Apache config:
sudo nano /etc/apache2/sites-available/example.com.conf
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/html
<Directory /var/www/html>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
If AllowOverride is set to None (the default in many Apache configurations), the .htaccess file is completely ignored, and WordPress rewrite rules will not work.
# Test and reload:
sudo apache2ctl configtest
sudo systemctl reload apache2
Step 4: Verify the .htaccess File
WordPress generates the following rewrite rules in the .htaccess file in the WordPress root directory:
# 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
Verify the file exists and contains these rules:
# Check the .htaccess file:
cat /var/www/html/.htaccess
# If the file is empty or missing, WordPress could not write to it.
# Check the file permissions:
ls -la /var/www/html/.htaccess
# The file must be writable by the web server user:
sudo chown www-data:www-data /var/www/html/.htaccess
sudo chmod 644 /var/www/html/.htaccess
If the file does not exist, create it and paste the WordPress rewrite rules above, then save the permalink settings again in the WordPress admin.
Step 5: WordPress in a Subdirectory
If WordPress is installed in a subdirectory (e.g., /blog/), the RewriteBase must match:
# For WordPress at example.com/blog/:
RewriteBase /blog/
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /blog/index.php [L]
Nginx Configuration (try_files)
If your server runs Nginx instead of Apache, .htaccess files are not used at all. Nginx uses the try_files directive to achieve the same result:
server {
listen 80;
server_name example.com;
root /var/www/html;
index index.php;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
The key line is try_files $uri $uri/ /index.php?$args; — it tells Nginx to first look for an exact file match, then a directory match, and finally pass the request to index.php (WordPress's front controller). Without this line, all non-file URLs will return 404.
# Test and reload Nginx:
sudo nginx -t
sudo systemctl reload nginx
Flushing Rewrite Rules
WordPress caches rewrite rules internally. If you have made server-level changes (enabled mod_rewrite, fixed AllowOverride) but URLs are still broken:
- Go to Settings > Permalinks.
- Without changing anything, click Save Changes.
This forces WordPress to regenerate and re-write the rewrite rules. You can also flush rewrite rules via WP-CLI:
wp rewrite flush
Permalinks Break After Migration
After migrating WordPress to a new server, permalinks often break because:
- mod_rewrite is not enabled on the new server. Run
sudo a2enmod rewrite. - AllowOverride is set to None in the new server's Apache config.
- .htaccess was not copied during migration (hidden files are often skipped by default in
cporrsyncwithout the right flags). - Different web server: Migrating from Apache to Nginx means .htaccess rules are irrelevant — you need to configure
try_filesin the Nginx server block. - File ownership changed: The
.htaccessfile is owned by a different user and Apache cannot read it.
After fixing the server configuration, always flush rewrite rules from the Permalinks settings page.
Troubleshooting Checklist
- Is
mod_rewriteenabled? Check withapache2ctl -M | grep rewrite. - Is
AllowOverride Allset for the DocumentRoot directory? - Does
.htaccessexist in the WordPress root with the correct rewrite rules? - Is
.htaccessreadable by Apache? Check permissions and ownership. - Did you save permalink settings after making server changes?
- If using Nginx, is
try_filesconfigured in the server block? - Is WordPress in a subdirectory? If so, is
RewriteBaseset correctly?
For more on clean URLs and removing file extensions in general, see removing file extensions from URLs.