Static Shield builds the release; your webserver decides who ever reaches WordPress. On Apache and LiteSpeed the plugin can write that rule itself. On nginx, Caddy and IIS you paste it in once.
Your build is written to a tulapp-static/ directory inside your web root. The
rule you install tells the webserver: for a public request, if a matching file exists under
tulapp-static/, serve that file and stop. PHP is never started, so WordPress
is not merely hidden — for that request it does not run at all.
Four things are always carved out and still reach WordPress:
/wp-admin/ and /wp-login.php — your own access/wp-json/ — including the Shield Bridge, your way back in/wp-cron.php — scheduled jobs, including allowlist sync.phpThose carve-outs are what the access protections then narrow further. The webserver rule is the floor; Trusted IPs and the protection modes sit on top of it.
Everything below is generated for your specific site — real paths, real PHP-FPM version, your own build secret — in Static Shield → Server in wp-admin. The plugin detects which server you are on and shows the matching snippet. Copy from there rather than from this page; the examples here use placeholders.
Both honour .htaccess, so the plugin writes the rules for you using the same
marker mechanism WordPress core uses for its own permalink block. You will see this appear
in your .htaccess:
# BEGIN Tulapp Static Shield
<IfModule mod_rewrite.c>
...
</IfModule>
# END Tulapp Static Shield
Our block must sit above WordPress's block. mod_rewrite stops at the
first matching [L] rule, and WordPress's catch-all
(RewriteRule . /index.php [L]) matches everything. If our block ended up
below it, WordPress would win every request and nothing would ever be served statically.
The plugin handles this — it strips any previous copy and re-inserts at the top — but if
you reorder .htaccess by hand, keep that ordering.
Requirements: mod_rewrite enabled, and AllowOverride set to
permit FileInfo in your vhost. If .htaccess is not writable the
plugin will tell you and fall back to showing you the block to paste manually.
Two things worth knowing about the Apache path. The "Configured" status is
a marker-presence check — it confirms the block is there, not that its rules are
still correct, so a hand-edited or truncated block still shows as configured. Use the
self-check below for the real answer. And the setup wizard takes a one-time backup of your
original .htaccess before its first write, restorable via Restore
Previous Configuration; clicking Configure Automatically on the
Server tab directly does not take one.
On a subdirectory install where WordPress had not already written its own block, the generated rules assume a root install. Check the paths before relying on them.
PHP cannot edit nginx: it has no access to the config in the general case and no privilege
to reload the service even if it did. So this one is manual. Paste the generated snippet
into your server { } block, replacing any existing location /:
location / {
set $tulapp_bypass 0;
if ($http_x_tulapp_static_build = "YOUR_BUILD_SECRET") {
set $tulapp_bypass 1;
}
if ($tulapp_bypass = 1) {
rewrite ^ /index.php?$args last;
}
try_files /tulapp-static$uri/index.html /tulapp-static$uri @wordpress;
}
location @wordpress {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
root /var/www/your-site;
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
Then test and reload — never reload without testing first:
sudo nginx -t && sudo systemctl reload nginx
Check the socket path in fastcgi_pass matches your actual PHP-FPM version. The
plugin fills in the version it is running under, which is usually but not always the same
one your pool listens on.
The snippet also blocks dotfiles and backup-file patterns outright, in case one ever ends up mirrored into the static release:
location ~ /\.(git|env|htaccess|htpasswd)(/|$) {
deny all;
return 403;
}
location ~ \.(bak|backup|old|orig|save|sql)(\.gz)?$|~$ {
deny all;
return 403;
}
Turning on Zero-WordPress Mode generates an additional nginx snippet that replaces
your location ~ \.php$ block, denying PHP entirely except for the Shield Bridge
and your trusted addresses. It is generated from your real current allowlist, so re-copy it
after you change that list.
location ~ \.php$ {
set $tulapp_zero_wp_allow 0;
if ($uri ~ ^/wp-json/tulapp-shield/) { set $tulapp_zero_wp_allow 1; }
if ($remote_addr = 203.0.113.42) { set $tulapp_zero_wp_allow 1; }
if ($tulapp_zero_wp_allow = 0) { return 403; }
try_files $uri =404;
...
}
Use $uri, not $request_uri, and don't "fix" it.
$request_uri is the raw request line; $uri is the normalised
path that try_files actually serves. Deciding on the raw value while serving
the normalised one means a request for
/wp-json/tulapp-shield/../../wp-login.php passes the allow test and then
gets served /wp-login.php — a complete bypass of the gate from any address.
The check and the serve must read the same variable.
Both are supported with generated copy-paste snippets in the Server tab: a
route { } block for Caddy, and two <rule> elements for the
IIS URL Rewrite module to drop into web.config.
Both are second-tier support, and we'd rather say so. The Caddy snippet has not been validated against a live Caddy instance — the syntax should be right for this pattern, but verify with the self-check below before relying on it. The IIS rule is also weaker than the Apache and nginx equivalents. And neither Caddy nor IIS has a Zero-WordPress Mode variant, so that feature is effectively Apache, LiteSpeed and nginx only. If you are choosing a stack for this, choose one of those three.
Every snippet contains one line matching the header X-Tulapp-Static-Build
against a secret. That is how the plugin reaches live WordPress through your own front door
while everyone else gets static — needed for crawling during a build, rendering previews,
and the self-check.
Note it is stored in plaintext in whichever config file you put it in. On Apache that is
.htaccess, which sits inside your web root — the plugin's own deny rules block
it, as does stock Apache config, but it is worth knowing.
There is no rotate button yet. If you need to rotate the secret, delete the
tulapp_static_shield_internal_build_secret option; the plugin generates a new
one and rewrites .htaccess itself on the next wp-admin request. On nginx, Caddy
and IIS you must re-copy the snippet by hand afterwards, or static serving breaks.
In Static Shield → Server, press Test server integration. It fetches your homepage twice — once normally, once with the bypass header — and compares the results. If routing is wired up correctly the two differ: one is the static release, the other is live WordPress. Identical responses mean the rule is not taking effect.
The test needs a build to compare against. If you see "No static build exists yet — run a build first, then test again", that is the expected message on a fresh install, not a configuration fault.
By hand, the same check is:
# should be the static release
curl -s https://your-site.example/ | grep -c wp-content
# should be live WordPress
curl -s -H "X-Tulapp-Static-Build: YOUR_BUILD_SECRET" https://your-site.example/ | grep -c wp-content
Neutral Asset Paths republishes CSS and JS under hashed, theme-agnostic paths like
/assets/css/<hash>.css. Those files are written inside
tulapp-static/, so the same try_files / literal-file check already
in your snippet resolves them. There is nothing extra to add.
If assets 404 after enabling the feature, the cause is almost always an outdated snippet from before you installed the current version — recopy it from the Server tab.
location / is winning.root in your .php block doesn't match your actual web root, or the build hasn't run yet.fastcgi_pass socket path is wrong for your PHP-FPM version..htaccess does not.See also: Trusted IPs · Recovering access with SSH or WP-CLI