D
P
0

WordPress & PHP

WordPress Staging on a Subpath 301-Redirects to Production? Two Rewrite Blocks Stacked in `.htaccess`

July 26, 2026·5 min read
WordPress Staging on a Subpath 301-Redirects to Production? Two Rewrite Blocks Stacked in `.htaccess`

A client had a production WordPress site and asked for a staging clone so I could rebuild the theme without touching the live site. I used WP Staging, and since the hosting was a single domain, the clone landed on a subpath: client-site.com/staging/. The plugin reported success. I opened client-site.com/staging/, and instead of the clone, the browser jumped 301 straight to client-site.com/, production. The address bar rewrote itself, and what rendered was clearly the production theme and content, not the clone I had just built.

The more I clicked, the stranger the pattern got:

  • client-site.com/staging/about/ 301'd to client-site.com/about/, the prod version.
  • client-site.com/staging/wp-json/ returned 404. The staging REST API was dead.
  • The custom post type archive client-site.com/staging/products/ also 404'd.
  • Slugs that genuinely did not exist 404'd directly, not into a staging 404.
  • But client-site.com/staging/wp-admin/ worked flawlessly. I could log into the staging dashboard with no trouble.

So the dashboard was alive, but the entire front end behaved as if it belonged to production. That is what nagged me: if the clone had failed outright, wp-admin should have broken too. This was half alive.

The first two guesses that missed

My first guess is the classic WP Staging one: siteurl and home in the clone database still point at the production URL. If the search-replace during cloning is incomplete, WordPress keeps normalizing URLs back to the prod domain. I checked wp_options on the staging database:

SELECT option_name, option_value
FROM wp_options
WHERE option_name IN ('siteurl', 'home');

Both were correct, pointing at https://client-site.com/staging. So not that. Second guess: maybe the content search-replace was dirty, some absolute prod URL left behind. But that would not explain a 301 at the earliest request level, before PHP renders anything. A 301 that fires before the theme even loads smells like rewrite, not content.

The wp-admin that stayed alive was actually the biggest clue. The wp-admin/ directory is a real folder that physically exists on disk. If WordPress's front controller is aimed at the wrong place but the physical folder still opens normally, the problem is in the rewrite rules, not the database or PHP.

Root cause: two rewrite blocks stacked

I opened .htaccess at the staging root (/staging/.htaccess), and there was the answer. Two WordPress rewrite blocks were stacked in one file:

# bare block inherited from production, cloned along, NO RewriteBase
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
 
# BEGIN WordPress  (the WP Staging block, this one is correct)
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /staging/
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /staging/index.php [L]
</IfModule>
# END WordPress

The top block is the leftover production .htaccess that got copied during cloning. It has no RewriteBase and sends everything to /index.php at the root, the production front controller. And the killer is the [L] flag on RewriteRule . /index.php [L]. [L] means last: stop processing rules after this one. For any request that is not a physical file or directory, the top block matches first, sends it to prod /index.php, and [L] halts the chain. The WP Staging block below with RewriteBase /staging/ never gets its turn.

Once the request hits production's index.php, prod WordPress does not recognize /staging/about/ as its own, redirect_canonical() kicks in, and it 301s to the prod canonical form: /about/. That is the redirect. REST and the CPT archive 404 because both get handed to the prod instance that has no such route in a subpath context. And wp-admin survives precisely because it is a physical directory: RewriteCond %{REQUEST_FILENAME} !-d fails (the folder exists), so the rule is skipped and Apache serves wp-admin/ directly, never touching the rewrite at all.

The fix

The fix is trivial once the mechanism is clear: delete the inherited bare block on top, keep only the # BEGIN WordPress block that has RewriteBase /staging/.

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /staging/
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /staging/index.php [L]
</IfModule>
# END WordPress

Save it, then verify from the command line, not just the browser:

# -I headers only, -L follow redirects, -s silent
curl -sIL "https://client-site.com/staging/about/" | grep -i "^HTTP\|^location"

What you want now: 200, with no 301 jumping to the prod domain. If you still see a 301 whose Location points at the root without /staging/, the bare block is not fully removed.

One trap almost convinced me the fix had failed: browsers cache 301s very aggressively, sometimes as if permanently. After I corrected .htaccess, my browser still jumped to prod because it remembered the old 301. Test in a fresh browser or incognito, or use curl, which caches nothing. Otherwise you will blame a .htaccess that is already correct.

One last note: this bug reappeared identically when I built a second staging clone on a different subpath. The stacked-block pattern is inherent to WP Staging cloning onto a subpath, not a one-off. If you clone to a subdomain (staging.client-site.com) instead of a subpath, the problem never shows up, because each subdomain has its own document root and does not inherit the prod .htaccess.

Takeaways

  • If a staging URL 301s to production before PHP renders anything, suspect rewrite, not the database or content.
  • wp-admin staying alive while the front end breaks is a clue: a physical directory bypasses rewrite, so the problem is in the front-controller rewrite rules.
  • Open the staging .htaccess and look for a duplicate WordPress block. A bare inherited prod block with [L] wins first and halts the chain.
  • Keep only the # BEGIN WordPress block whose RewriteBase points at the staging subpath.
  • Verify with curl -sIL, not the browser, because 301s are cached aggressively. Re-test in incognito.
  • If you can, clone to a subdomain, not a subpath. A subdomain has its own document root and does not inherit the prod .htaccess.