Server integration

Webserver configuration

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.

What the rule actually does

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:

Those 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.

Find your snippet

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.

Apache and LiteSpeed — automatic

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.

nginx — copy and paste

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.

Deny rules

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; }

Zero-WordPress Mode adds a second gate

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.

Caddy and IIS

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.

The build bypass header

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.

Verify it works

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

Assets need no extra configuration

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.

Common problems

See also: Trusted IPs · Recovering access with SSH or WP-CLI