.htaccess and Nginx Rules Cheat Sheet for WordPress
Copy-ready Apache .htaccess and Nginx rules for WordPress: rewrites, one-hop HTTPS redirects, 301s, security headers, caching, compression, CORS, IP rules, staging auth and upload limits.
This sheet pairs each Apache (and LiteSpeed) .htaccess rule with its Nginx equivalent. Apache reads .htaccess files on every request, in every directory from the document root down, if the server config allows it with AllowOverride. Nginx has no per-directory files: rules go in the server config (usually a file under /etc/nginx/sites-available/ or /etc/nginx/conf.d/) and only take effect after a reload. On shared hosting you can usually edit .htaccess but not the Nginx config, so ask your host for Nginx changes.
Placeholders: EXAMPLE.COM is your bare domain, WWW.EXAMPLE.COM your preferred host, /PATH/TO/ an absolute filesystem path, 203.0.113.10 an example IP address (from the documentation range, replace it with yours), and PHP-VERSION the PHP-FPM socket name your server uses. Before you edit: download a copy of the working .htaccess. One typo produces a site-wide 500 error, and restoring the copy is the fix.
Default WordPress rewrite blocks
WordPress writes the single-site block itself when you save Settings, Permalinks (if the file is writable). Keep your own rules outside the # BEGIN WordPress and # END WordPress markers, because WordPress may rewrite everything between them. Put redirects above the WordPress block so they run before the catch-all.
Single site
# 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
Multisite, subdirectory network
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
# add a trailing slash to /wp-admin
RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/ [R=301,L]
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2 [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2 [L]
RewriteRule . index.php [L]
Multisite, subdomain network
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
# add a trailing slash to /wp-admin
RewriteRule ^wp-admin$ wp-admin/ [R=301,L]
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteRule ^(wp-(content|admin|includes).*) $1 [L]
RewriteRule ^(.*\.php)$ $1 [L]
RewriteRule . index.php [L]
The Network Setup screen (Tools, Network Setup) prints the exact rules for your install, including a different RewriteBase if WordPress lives in a subfolder. Copy from there when in doubt.
Nginx equivalent
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/PHP-VERSION-fpm.sock;
}
| Piece | What it does |
|---|---|
try_files $uri $uri/ /index.php?$args; |
Serve the file if it exists, then the directory, otherwise hand the request to WordPress with the original query string. This one line replaces the whole Apache block. |
$args vs $is_args$args |
Both work for WordPress. $is_args adds the ? only when a query string exists. |
fastcgi_pass |
Socket path or 127.0.0.1:9000. The socket name depends on distro and PHP version. Debian and Ubuntu also ship snippets/fastcgi-php.conf. |
| Multisite on Nginx | Subdomain networks work with the same try_files. Subdirectory networks need extra rewrites for /SITE/wp-admin, /SITE/wp-content and /SITE/*.php, mirroring the Apache rules above. |
Force HTTPS and the canonical host in one hop
A chain such as http://EXAMPLE.COM to https://EXAMPLE.COM to https://WWW.EXAMPLE.COM costs an extra round trip and dilutes redirect signals. One rule can fix both scheme and host at once.
Apache
RewriteEngine On
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} !^www\.example\.com$ [NC]
RewriteRule ^ https://www.example.com%{REQUEST_URI} [R=301,L,NE]
To prefer the bare domain, swap the host in both lines (!^example\.com$ and https://example.com).
Apache behind a CDN, proxy or load balancer
When TLS ends at the proxy, the origin sees plain HTTP and %{HTTPS} is always off, which causes a redirect loop. Test the forwarded header instead (confirm which header your proxy sends):
RewriteCond %{HTTP:X-Forwarded-Proto} !https [OR]
RewriteCond %{HTTP_HOST} !^www\.example\.com$ [NC]
RewriteRule ^ https://www.example.com%{REQUEST_URI} [R=301,L,NE]
Nginx
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://www.example.com$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com;
ssl_certificate /PATH/TO/fullchain.pem;
ssl_certificate_key /PATH/TO/privkey.pem;
return 301 https://www.example.com$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name www.example.com;
# certificate lines and the real site config go here
}
The certificate must cover both names, or browsers show a certificate error on the bare domain before the redirect can happen. Also set the WordPress Address and Site Address (Settings, General) to the same canonical URL, or WordPress will redirect back.
301 redirects on Apache
| Directive | Example | Behavior |
|---|---|---|
Redirect (mod_alias) |
Redirect 301 /old-page/ https://www.example.com/new-page/ |
Prefix match. /old-page/anything goes to /new-page/anything. The query string is carried over. |
RedirectMatch (mod_alias) |
RedirectMatch 301 ^/old-page/?$ https://www.example.com/new-page/ |
Regular expression against the URL path. Anchor with ^ and $ to avoid matching more than you meant. |
RewriteRule (mod_rewrite) |
RewriteRule ^old-page/?$ /new-page/ [R=301,L] |
Regex with conditions and flags. In .htaccess the pattern matches the path without the leading slash. |
| Whole domain move |
RewriteCond %{HTTP_HOST} ^(www\.)?old-example\.com$ [NC]
RewriteRule ^ https://www.example.com%{REQUEST_URI} [R=301,L,NE]
|
Keeps every path. Only safe if the new site uses the same URL structure. |
| Capture groups | RewriteRule ^blog/(.*)$ /news/$1 [R=301,L] |
$1 is the first group in the RewriteRule pattern. %1 is the first group from the last matched RewriteCond. |
RewriteRule flags
| Flag | Meaning | Notes |
|---|---|---|
R=301 |
External redirect with that status | Plain R means 302. Use 302 while testing, because browsers cache 301s hard. |
L |
Last: stop processing this pass | In .htaccess, a rewritten URL starts a new pass through the rules, so L does not stop a loop on its own. END stops all further passes. |
NC |
No case: case-insensitive match | Use on host names. Also valid on RewriteCond. |
QSA |
Query string append | Only matters when the target contains its own ?. Without QSA, a target query string replaces the original. |
QSD |
Query string discard | Drops the original query string. A target ending in a bare ? does the same. |
NE |
No escape | Stops mod_rewrite hex-encoding characters such as % and # in the target, which prevents double encoding. |
OR |
On RewriteCond: OR with the next condition | Conditions are ANDed by default. |
Variables for RewriteCond
| Variable | Holds | Example test |
|---|---|---|
%{HTTPS} |
on or off |
RewriteCond %{HTTPS} off |
%{HTTP_HOST} |
The Host header sent by the client | RewriteCond %{HTTP_HOST} ^example\.com$ [NC] |
%{REQUEST_URI} |
The URL path, starting with /, without the query string |
RewriteCond %{REQUEST_URI} !^/wp-admin/ |
%{QUERY_STRING} |
Everything after ?, without the ? |
RewriteCond %{QUERY_STRING} (^|&)p=123(&|$) |
%{REQUEST_FILENAME} |
The filesystem path the request maps to | -f file exists, -d directory exists |
%{HTTP:Header-Name} |
Any request header | %{HTTP:X-Forwarded-Proto} |
%{REMOTE_ADDR} |
Client IP as Apache sees it (the proxy IP behind a CDN) | RewriteCond %{REMOTE_ADDR} !^203\.0\.113\.10$ |
Redirecting a URL with a query string
The RewriteRule pattern never sees the query string. Match it with a condition and drop it with QSD:
RewriteCond %{QUERY_STRING} (^|&)page_id=42(&|$)
RewriteRule ^$ /about/ [R=301,L,QSD]
Redirects on Nginx: return vs rewrite
| Directive | Example | Use when |
|---|---|---|
return |
location = /old-page/ { return 301 /new-page/; } |
Fixed redirects. Fastest and clearest: no regex is evaluated. |
return with variables |
return 301 https://www.example.com$request_uri; |
$request_uri is the original path with the query string. |
rewrite ... permanent |
rewrite ^/blog/(.*)$ /news/$1 permanent; |
Pattern redirects with captures. permanent is 301, redirect is 302. |
| Query string with rewrite | rewrite ^/old$ /new? permanent; |
Rewrite appends the original arguments automatically. A trailing ? in the replacement drops them. |
| Many redirects | map $request_uri $new_uri { /old-a/ /new-a/; /old-b/ /new-b/; } then if ($new_uri) { return 301 $new_uri; } |
Hundreds of one-to-one redirects. The map block goes in the http context. |
Location matching order: exact = first, then the longest prefix (a ^~ prefix stops the search), then regex locations (~ and ~*) in the order they appear in the file, and finally the longest plain prefix if no regex matched.
Block xmlrpc.php
XML-RPC is used for brute-force and pingback abuse. It is also used by the Jetpack plugin and some older mobile and publishing apps, so check before blocking.
| Server | Rule |
|---|---|
| Apache 2.4 |
<Files xmlrpc.php> Require all denied </Files> |
| Apache 2.4, allow one IP |
<Files xmlrpc.php> Require ip 203.0.113.10 </Files> |
| Nginx | location = /xmlrpc.php { deny all; } |
| Nginx, drop the connection without a response | location = /xmlrpc.php { return 444; } (444 is an Nginx-only code) |
Protect sensitive files and stop PHP in uploads
| Goal | Apache 2.4 | Nginx |
|---|---|---|
| Deny wp-config.php |
<Files wp-config.php> Require all denied </Files> |
location = /wp-config.php { deny all; } |
| Deny .htaccess, .htpasswd and other dotfiles |
<FilesMatch "^\.ht"> Require all denied </FilesMatch> (most Apache builds already ship this in the main config) |
location ~ /\.(?!well-known) { deny all; } (keeps /.well-known/ open for certificates and security.txt) |
| Deny backup and log files |
<FilesMatch "\.(bak|sql|log|old|orig|swp)$"> Require all denied </FilesMatch> |
location ~* \.(bak|sql|log|old|orig|swp)$ { deny all; } |
| Deny debug.log |
<Files debug.log> Require all denied </Files> |
location = /wp-content/debug.log { deny all; } |
| No PHP in uploads | In wp-content/uploads/.htaccess:
<FilesMatch "\.(?i:php[0-9]?|phtml|phar)$"> Require all denied </FilesMatch> |
location ~* ^/wp-content/uploads/.*\.(php[0-9]?|phtml|phar)$ { deny all; } Place it above the generic location ~ \.php$ block, because the first matching regex wins. |
| No directory listings | Options -Indexes |
Off by default. Only autoindex on; turns it on. |
Old vs new Apache access syntax
| Apache 2.2 (mod_access_compat) | Apache 2.4 (mod_authz_core) |
|---|---|
Order allow,deny plus Deny from all |
Require all denied |
Order allow,deny plus Allow from all |
Require all granted |
Allow from 203.0.113.10 |
Require ip 203.0.113.10 |
Deny from 203.0.113.10 |
<RequireAll> Require all granted Require not ip 203.0.113.10 </RequireAll> |
Do not mix the two styles in one context. Apache 2.4 only understands the old lines when mod_access_compat is loaded, and mixing them gives confusing results.
Security headers
Values below are a starting baseline. CS-12 explains each header and its WordPress caveats. Test Content-Security-Policy in Report-Only mode first.
Apache (needs mod_headers)
<IfModule mod_headers.c> Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" Header always set X-Content-Type-Options "nosniff" Header always set X-Frame-Options "SAMEORIGIN" Header always set Referrer-Policy "strict-origin-when-cross-origin" Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()" </IfModule>
Nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
| Detail | Explanation |
|---|---|
Header always set vs Header set |
always also adds the header to error and redirect responses (non-2xx). Without it, Apache only adds it to successful responses. |
set vs append vs merge |
set replaces, append adds a comma-separated value, merge appends only if the value is not already present. |
Nginx always |
Without it, add_header only applies to 200, 201, 204, 206, 301, 302, 303, 304, 307 and 308 responses. |
| Nginx inheritance gotcha | add_header lines are inherited from the outer level only if the current level has no add_header of its own. One add_header inside a location (for example a Cache-Control header on images) silently removes every server-level security header from that location. Fix: repeat them, or keep them in a snippet and include it in each location. Nginx 1.29.3 (mainline, October 2025) and later also offer add_header_inherit merge;; older and stable-branch builds reject the directive, so check nginx -v first. |
| Duplicate headers | If WordPress, a plugin or a CDN also sets a header, you can end up with two values. Check with curl -sI and set each header in one place only. |
Browser caching
Apache: mod_expires plus Cache-Control
<IfModule mod_expires.c> ExpiresActive On ExpiresDefault "access plus 1 hour" ExpiresByType text/html "access plus 0 seconds" ExpiresByType text/css "access plus 1 year" ExpiresByType application/javascript "access plus 1 year" ExpiresByType text/javascript "access plus 1 year" ExpiresByType image/webp "access plus 1 year" ExpiresByType image/avif "access plus 1 year" ExpiresByType image/jpeg "access plus 1 year" ExpiresByType image/png "access plus 1 year" ExpiresByType image/svg+xml "access plus 1 year" ExpiresByType font/woff2 "access plus 1 year" </IfModule>
mod_expires sets both the Expires header and Cache-Control: max-age. A one-year lifetime is only safe for files whose URL changes when the content changes (WordPress adds ?ver= to enqueued CSS and JS). To add directives such as immutable, use mod_headers:
<FilesMatch "\.(css|js|woff2|webp|avif|jpe?g|png|svg)$"> Header set Cache-Control "public, max-age=31536000, immutable" </FilesMatch>
Nginx
location ~* \.(css|js|woff2|webp|avif|jpe?g|png|gif|svg|ico)$ {
expires 1y;
add_header Cache-Control "public, immutable";
# repeat security headers here (inheritance gotcha)
try_files $uri =404;
}
expires 1y; sets Expires and Cache-Control: max-age=31536000. expires off; leaves the headers alone, and expires epoch; marks the response as already expired.
Compression
| Server | Rule | Notes |
|---|---|---|
| Apache gzip (mod_deflate) |
<IfModule mod_deflate.c> AddOutputFilterByType DEFLATE text/html text/css text/plain text/xml application/javascript text/javascript application/json application/xml image/svg+xml </IfModule> |
Do not compress JPEG, PNG, WebP, AVIF or WOFF2. They are already compressed. |
| Apache brotli (mod_brotli, 2.4.26+) |
<IfModule mod_brotli.c> AddOutputFilterByType BROTLI_COMPRESS text/html text/css application/javascript application/json image/svg+xml </IfModule> |
Only if the module is installed. Clients that do not send br in Accept-Encoding get gzip or nothing. |
| Nginx gzip |
gzip on; gzip_types text/css application/javascript application/json image/svg+xml text/plain text/xml application/xml; gzip_vary on; gzip_min_length 1024; |
text/html is always compressed once gzip is on, and listing it gives a duplicate warning. gzip_vary on adds Vary: Accept-Encoding. |
| Nginx brotli | brotli on; brotli_types ...; |
Needs the third-party ngx_brotli module. Not in stock Nginx builds. |
CORS for web fonts
Browsers block cross-origin font loads (for example from a CDN hostname or a second domain) unless the font response carries Access-Control-Allow-Origin.
| Server | Rule |
|---|---|
| Apache |
<FilesMatch "\.(woff2?|ttf|otf|eot)$"> Header set Access-Control-Allow-Origin "https://www.example.com" </FilesMatch> |
| Nginx | location ~* \.(woff2?|ttf|otf|eot)$ { add_header Access-Control-Allow-Origin "https://www.example.com" always; } |
The header takes one origin or *, never a list. For several origins, echo back the matched Origin and add Vary: Origin so caches keep the variants apart.
IP allow and deny
| Goal | Apache 2.4 | Nginx |
|---|---|---|
| Block one IP |
<RequireAll> Require all granted Require not ip 203.0.113.10 </RequireAll> |
deny 203.0.113.10; |
| Block a range | Require not ip 203.0.113.0/24 inside the same RequireAll |
deny 203.0.113.0/24; |
| Allow only your IP to wp-login.php |
<Files wp-login.php> Require ip 203.0.113.10 </Files> |
location = /wp-login.php { allow 203.0.113.10; deny all; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass unix:/run/php/PHP-VERSION-fpm.sock; } |
Nginx checks allow and deny in order and stops at the first match. Behind a CDN or proxy, the server sees the proxy IP: configure mod_remoteip (Apache) or the realip module (Nginx, set_real_ip_from and real_ip_header) with the proxy’s published ranges first, or you may block the whole CDN.
Basic auth for a staging site
| Step | Apache | Nginx |
|---|---|---|
| Create the password file outside the web root | htpasswd -c /PATH/TO/.htpasswd USERNAME |
Same file format. Use htpasswd (from apache2-utils or httpd-tools) or openssl passwd -apr1. |
| Protect the site |
AuthType Basic AuthName "Staging" AuthUserFile /PATH/TO/.htpasswd Require valid-user |
auth_basic "Staging"; auth_basic_user_file /PATH/TO/.htpasswd; |
| Let your office IP skip the prompt | Wrap in <RequireAny> with Require ip 203.0.113.10 and Require valid-user |
satisfy any; allow 203.0.113.10; deny all; plus the auth lines |
-c creates the file and overwrites an existing one. Leave it off when adding a second user. Basic auth sends the password with every request, so only use it over HTTPS. It also blocks WordPress loopback requests and WP-Cron on that site, which Site Health will report.
Upload and PHP limits
Where you set PHP values depends on how PHP runs. Check the Server API line in Tools, Site Health, Info, Server, or in a phpinfo() page (delete it afterward).
| PHP handler | Where to set values | Syntax |
|---|---|---|
| mod_php (Apache 2.0 Handler) | .htaccess |
php_value upload_max_filesize 64M php_value post_max_size 64M php_value memory_limit 256M php_value max_execution_time 120 |
| PHP-FPM or CGI/FastCGI | .user.ini in the web root |
upload_max_filesize = 64M post_max_size = 64M |
| Server-wide (VPS) | php.ini or the FPM pool file |
Pool file: php_admin_value[upload_max_filesize] = 64M. Restart PHP-FPM after. |
| LiteSpeed (LSAPI) | Often .htaccess or .user.ini, depending on host setup |
Confirm with your host which file is honored. |
| Nginx | Server config, as well as PHP | client_max_body_size 64m; (default 1m, which gives a 413 error) |
| Rule | Why |
|---|---|
php_value under PHP-FPM gives a 500 error |
Apache does not recognize the directive when mod_php is not loaded. Remove it and use .user.ini. |
.user.ini is cached |
PHP rereads it every user_ini.cache_ttl seconds (300 by default), so changes can take five minutes. |
post_max_size at least upload_max_filesize |
The upload is part of the POST body. If the body limit is smaller, the upload fails. |
LiteSpeed notes
| Topic | Detail |
|---|---|
| Compatibility | LiteSpeed Enterprise reads Apache .htaccess files, including mod_rewrite rules, so the Apache rules above usually work unchanged. |
| OpenLiteSpeed | The free edition reads only mod_rewrite rules from .htaccess (when rewrite and “Auto Load from .htaccess” are enabled); other Apache directives such as Header, Require or php_value are ignored, and RewriteBase is not supported. Changes need a graceful restart. Test after every edit. |
| LiteSpeed Cache plugin blocks | The plugin writes its own marked sections, such as # BEGIN LSCACHE and # END LSCACHE. Do not edit inside them. Change the plugin settings instead. |
<IfModule LiteSpeed> |
Wraps rules meant only for LiteSpeed servers, such as cache rewrite rules. |
Testing
| Command | What it checks |
|---|---|
apachectl configtest (or apache2ctl configtest, httpd -t) |
Syntax of the main Apache config. It does not read .htaccess files: those are parsed per request, so test them with a browser or curl. |
nginx -t |
Syntax and file references of the whole Nginx config. Run before every reload. |
systemctl reload nginx |
Applies the config without dropping connections. Only reload after nginx -t passes. |
nginx -T |
Tests and prints the full merged config, which is useful for finding which file sets a header. |
curl -sI http://EXAMPLE.COM/ |
Shows the status code and Location header of the first response. |
curl -sIL http://EXAMPLE.COM/ | grep -iE "^(HTTP|location)" |
Follows the chain so you can count the hops. |
curl -sI https://WWW.EXAMPLE.COM/wp-content/uploads/test.php |
Should return 403 once PHP in uploads is blocked. |
Common recipes
| Task | Apache | Nginx |
|---|---|---|
| Redirect a whole folder | RewriteRule ^old-folder/(.*)$ /new-folder/$1 [R=301,L] |
location ^~ /old-folder/ { rewrite ^/old-folder/(.*)$ /new-folder/$1 permanent; } |
| Maintenance page except for your IP |
RewriteCond %{REMOTE_ADDR} !^203\.0\.113\.10$
RewriteCond %{REQUEST_URI} !^/maintenance\.html$
RewriteRule ^ /maintenance.html [R=302,L]
|
Inside location /: if ($remote_addr != 203.0.113.10) { return 503; }, plus error_page 503 /maintenance.html; and a separate location = /maintenance.html { } so the page itself is not blocked. |
| Send uploads requests to production (staging copy without media) |
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^wp-content/uploads/(.*)$ https://www.example.com/wp-content/uploads/$1 [R=302,L]
|
location /wp-content/uploads/ { try_files $uri @prod; }
location @prod { return 302 https://www.example.com$request_uri; }
|
| Block a bad bot by user agent |
RewriteCond %{HTTP_USER_AGENT} BADBOTNAME [NC]
RewriteRule ^ - [F]
|
if ($http_user_agent ~* BADBOTNAME) { return 403; } |
Gotchas
| Problem | Cause and fix |
|---|---|
Rules in .htaccess do nothing |
AllowOverride None in the server config, mod_rewrite not loaded, or the server is Nginx. Put a deliberate typo in the file: if the site does not throw a 500, the file is not being read. |
| Redirect loop after forcing HTTPS | TLS ends at a CDN or proxy. Test X-Forwarded-Proto, or set the CDN to Full (strict) TLS mode so it connects to the origin over HTTPS. |
| Old redirect still happens after you removed it | Browsers cache 301s. Test with curl or a private window, and use 302 until the rule is final. |
Redirect and RewriteRule fight |
mod_alias and mod_rewrite run independently, whatever their order in the file. Pick one module per site; with WordPress, that means RewriteRule. |
Redirect 301 /a /b catches /about |
Prefix matching. Use RedirectMatch 301 ^/a/?$ or a RewriteRule. |
| Security headers vanish on some Nginx paths | The add_header inheritance rule. Any add_header in a location drops the outer ones. |
| Rules disappear after saving permalinks | They were inside the WordPress markers. Move them above # BEGIN WordPress. |
if in Nginx behaves strangely |
Inside a location, only return and rewrite ... last are reliably safe. Use map, try_files or separate locations where possible. |
| Nginx changes have no effect | Not reloaded, edited a file that is not included, or a more specific location wins. Check with nginx -T. |
Did this fix it?
Still stuck? Start again from what you can see in the Emergency Room.
Need the full fix?
Cheat sheets are quick reference. The field guides walk you through each emergency step by step.
Open the Emergency Room