The Specific Way WordPress Rewrites Fail on Subdirectory Installs (And the Fix)

Server rack with glowing cables, representing the internal routing logic of WordPress rewrites

WordPress rewrite rules sit on top of Apache’s mod_rewrite or Nginx’s try_files. They turn pretty permalinks into the index.php?query_vars guts that actually find your content. On a root install, this is dead simple. The .htaccess or Nginx config lives at the domain apex. Any request that isn’t a real file or directory gets handed straight to index.php. But shove WordPress into a subdirectory—say, /blog/ or /content/—and the rewrite engine suddenly has to deal with an extra layer of path resolution. The web server has to strip that subdirectory prefix before WordPress can even start its internal rewrite logic. When that prefix handling goes sideways, you get 404s, redirect loops, or the classic “Nothing Found” on posts you know are there. This article walks through the exact failure modes, the rewrite rule anatomy behind them, and the surgical fixes that don’t involve guessing.

Why Subdirectory Rewrites Break Differently Than Root Installs

On a root install, a request for /about/ hits the server. Apache looks for a physical file or directory called about. It finds nothing, falls through to the .htaccess rules, and hands the request to index.php. WordPress then uses its WP_Rewrite class to parse the request URI, match it against the rewrite rules array, and set the query variables. The physical path and the request URI are basically the same thing.

A subdirectory install splits that apart. The request /blog/about/ comes in. The server has to recognize that /blog/ is the WordPress base, not a literal directory to serve. If the server’s rewrite config doesn’t strip that base correctly, WordPress gets a request URI of /blog/about/ but its internal rules are built expecting /about/. The mismatch means WP::parse_request() can’t match any rewrite rule, and WordPress spits out a 404. That’s the core failure: a desync between the server-level rewrite base and the WordPress site URL setting.

The Anatomy of a Subdirectory Rewrite Failure

Let’s trace a specific request to see exactly where things break. Assume WordPress is installed in /blog/ and the site URL is set to https://example.com/blog. A visitor requests /blog/2024/12/sample-post/.

Step 1: Server-Level Rewrite Execution

Apache reads the .htaccess file inside the /blog/ directory. A typical subdirectory .htaccess generated by WordPress looks like this:

# BEGIN WordPress

RewriteEngine On
RewriteBase /blog/
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /blog/index.php [L]

# END WordPress

The RewriteBase /blog/ directive is the linchpin. It tells Apache to prepend /blog/ to relative substitution paths. The final RewriteRule grabs any request that isn’t a real file or directory and rewrites it to /blog/index.php. Apache then sets the REQUEST_URI server variable to the original /blog/2024/12/sample-post/ and passes control to index.php.

If the .htaccess file is missing, or if RewriteBase is incorrectly set to /, the rewrite can fail silently. Apache might serve a literal 404, or it might pass a mangled URI to WordPress. On Nginx, the equivalent misconfiguration is a missing or incorrect try_files directive inside the subdirectory location block.

Step 2: WordPress Internal Rewrite Parsing

WordPress receives the request and calls WP::parse_request(). This method takes the REQUEST_URI and strips the subdirectory path using the home_url() and site_url() settings. If the site URL is https://example.com/blog, WordPress expects the URI to begin with /blog. It removes that prefix, leaving /2024/12/sample-post/. Then it matches this against the rewrite rules stored in the rewrite_rules option.

The failure happens when the prefix stripping is off. Common causes:

  • Site URL mismatch: The siteurl and home options in the wp_options table don’t include the subdirectory, or one includes it and the other doesn’t.
  • Hardcoded URLs in wp-config.php: Defining WP_HOME and WP_SITEURL without the subdirectory path forces WordPress to build rewrite rules for the root, but the physical files live in the subdirectory.
  • Reverse proxy interference: A proxy like Nginx in front of Apache may strip the subdirectory prefix before Apache sees it, causing WordPress to receive a root-relative URI while still expecting the subdirectory prefix.
  • Sunrise or multi-network configurations: When using WordPress multisite in subdirectory mode, the sunrise.php drop-in can alter the site URL detection, leading to incorrect prefix stripping.

Diagnosing the Exact Break Point

Before you slap on a fix, figure out whether the failure is at the server level or the application level. Drop this debug snippet into a must-use plugin or temporarily into your active theme’s functions.php:

add_action('parse_request', function($wp) {
    error_log('Request URI: ' . $_SERVER['REQUEST_URI']);
    error_log('Matched Rewrite Rule: ' . ($wp->matched_rule ?? 'none'));
    error_log('Matched Query: ' . ($wp->matched_query ?? 'none'));
    error_log('Query Vars: ' . print_r($wp->query_vars, true));
}, 1);

Check the debug log after visiting a failing URL. If REQUEST_URI shows the subdirectory path but matched_rule is none, the server passed the request correctly, but WordPress couldn’t match it. That points to a site URL or rewrite rules issue. If REQUEST_URI is missing the subdirectory, the server-level rewrite is broken. If REQUEST_URI is correct but the matched rule is something unexpected, the rewrite rules array may be corrupted or filtered incorrectly.

Fixing the Server-Level Rewrite Base

For Apache, the fix is usually explicit in the .htaccess file. Make sure the RewriteBase matches the subdirectory path relative to the domain root. If WordPress is in /blog/, the directive must be RewriteBase /blog/. The final RewriteRule must point to the subdirectory’s index.php using an absolute path or a correct relative path with the base applied.

If the server uses Nginx, the location block must isolate the subdirectory and pass requests to the correct index.php. A minimal working Nginx configuration for a subdirectory install looks like this:

location /blog {
    try_files $uri $uri/ /blog/index.php?$args;
}
location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
}

The try_files directive inside the /blog location block ensures that any request not matching a physical file or directory is sent to /blog/index.php with the original query string. Without this explicit location block, Nginx may pass the request to the root index.php, which doesn’t know about the subdirectory install.

Correcting the WordPress Site URL and Home URL

WordPress stores two critical values in the wp_options table: siteurl and home. The siteurl is the address where WordPress core files reside. The home is the address visitors use to reach the site. In a subdirectory install, both must include the subdirectory path. For example, if WordPress is in /blog/ and the site is accessed at https://example.com/blog, both options should be https://example.com/blog.

If the site is accessed at the domain root but WordPress files are in a subdirectory, the siteurl should include the subdirectory, while home should be the root URL. This is a common pattern for “giving WordPress its own directory.” The rewrite rules must then map root requests to the subdirectory’s index.php. The official WordPress documentation covers this setup, but the rewrite rule details are often glossed over. The root .htaccess needs a rule like:

RewriteRule ^(.*)$ /blog/$1 [L]

This passes all requests to the subdirectory, where WordPress’s own .htaccess takes over. Without this, the root-level server never invokes WordPress, and every request returns a literal 404.

Close-up of network patch cables, symbolizing the connection between server rewrite rules and WordPress internals

When the Rewrite Rules Array Itself Is Corrupted

Even with correct server configuration and correct site/home URLs, the internal rewrite rules array can become stale or corrupted. WordPress builds this array from the registered post types, taxonomies, and endpoints, then flushes it to the rewrite_rules option in the database. If a plugin or theme registers a post type with an incorrect rewrite argument, or if the option becomes bloated with orphaned rules, matching can fail.

The rewrite array is a massive associative array mapping regex patterns to query strings. For a subdirectory install, every pattern must account for the subdirectory prefix. WordPress handles this automatically if the site URL is correct, but a manual flush is sometimes necessary. Visit Settings > Permalinks and click Save Changes without altering anything. This triggers flush_rewrite_rules(), which deletes the option and rebuilds it from scratch. On high-traffic sites, avoid calling flush_rewrite_rules() on every page load—it’s an expensive operation that rebuilds the entire array and can cause performance degradation. Instead, flush once after any structural change (new post type, taxonomy, or permalink structure) and then remove the call.

Debugging the Rewrite Array Directly

To inspect the rewrite rules as WordPress sees them, use a plugin like Query Monitor or add a temporary debug snippet:

add_action('init', function() {
    if (current_user_can('administrator') && isset($_GET['show_rules'])) {
        $rules = get_option('rewrite_rules');
        echo '<pre>';
        print_r($rules);
        echo '</pre>';
        die();
    }
});

Visit yoursite.com/blog/?show_rules=1 to dump the array. Look for patterns that don’t include the subdirectory prefix. If you see rules like ^about/?$ instead of ^blog/about/?$, the site URL is likely misconfigured. Also check for duplicate or conflicting rules that might capture the request before the correct rule is reached.

Multisite Subdirectory Rewrite Failures

WordPress multisite in subdirectory mode adds another layer of complexity. The main site’s .htaccess must pass requests for subsites to index.php without redirecting them to the main site’s root. The default multisite rewrite rules look like:

RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
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]

These rules handle the subdirectory for each subsite dynamically. The failure mode here is often a missing or incorrect RewriteBase when the multisite install itself lives in a subdirectory. If the entire network is at /network/, the .htaccess must be inside that directory, and the RewriteBase must be /network/. Additionally, the wp-config.php constants DOMAIN_CURRENT_SITE and PATH_CURRENT_SITE must reflect the subdirectory. A mismatch causes WordPress to generate rewrite rules for the wrong base path.

Reverse Proxy and Load Balancer Interference

When a reverse proxy like Nginx sits in front of Apache, the proxy may modify the REQUEST_URI or SCRIPT_NAME before Apache sees it. If the proxy strips the subdirectory prefix, Apache serves the request as if it were a root request, but WordPress still expects the subdirectory. The fix is to ensure the proxy passes the original URI unchanged. In Nginx, this means using proxy_pass without a URI component, or explicitly setting headers:

location /blog/ {
    proxy_pass http://backend;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

If the proxy must rewrite the path, WordPress needs to be informed via the $_SERVER['HTTP_X_FORWARDED_PREFIX'] header or by filtering home_url and site_url to reconstruct the original path. This is fragile and should be avoided when possible.

Server room with organized cabling, illustrating the structured approach needed for rewrite configuration

Edge Case: Permalink Structure Contains the Subdirectory Name

A rare but devastating failure occurs when the permalink structure itself includes a term that matches the subdirectory name. For example, if WordPress is in /blog/ and the permalink structure is set to /blog/%postname%/, the resulting URLs become /blog/blog/sample-post/. WordPress will strip the first /blog/ as the subdirectory prefix, leaving /blog/sample-post/ to match against rewrite rules. The rules expect /sample-post/, so the match fails. The fix is to never include the subdirectory name in the permalink structure. Use a structure like /%category%/%postname%/ or simply /%postname%/.

When “Nothing Found” Masks a Rewrite Failure

A “Nothing Found” message on a valid post URL is often a rewrite failure in disguise. WordPress’s template loader falls back to the 404.php template when no query vars match a post. But the underlying cause is that the query never resolved to a post ID. This can happen even when the rewrite rules are correct if the post’s slug conflicts with a taxonomy term, a pagination rule, or a date archive rule. The rewrite engine matches the first rule that fits, and if a date archive rule captures /2024/12/ before the post rule can capture /2024/12/sample-post/, the post is never found. This is more common in subdirectory installs because the prefix stripping can alter the regex matching order.

To diagnose, temporarily set the permalink structure to the plain ?p=123 format. If the post loads correctly, the rewrite rules are the culprit. Then switch back to the desired structure and use the rewrite rule dump to identify which rule is capturing the request prematurely. You may need to adjust the permalink structure or add a custom rewrite rule with a higher priority.

Permanent Fix: A Subdirectory Rewrite Checklist

When deploying a subdirectory install, run through this checklist before launch. It prevents the majority of rewrite failures that surface days or weeks later when traffic hits the site.

  1. Verify siteurl and home in the database or wp-config.php. Both must include the full subdirectory path if WordPress is accessed at that path.
  2. Inspect the .htaccess or Nginx config for the correct RewriteBase or try_files directive. The base must match the subdirectory exactly, including the trailing slash.
  3. Flush rewrite rules after setting the permalink structure. Do this once, not on every page load.
  4. Test with a plain permalink structure to isolate rewrite failures from other issues.
  5. Check for conflicting slugs between post types, taxonomies, and the subdirectory name itself.
  6. Audit any reverse proxy configuration to ensure the original request URI is preserved.
  7. For multisite, confirm DOMAIN_CURRENT_SITE and PATH_CURRENT_SITE match the actual network location.

FAQ

Why do my subdirectory posts return 404 only on Nginx?

Nginx doesn’t read .htaccess files. The rewrite logic must be explicitly defined in the server block. If the try_files directive inside the subdirectory location block is missing or incorrectly points to the root index.php, Nginx won’t pass the request to WordPress. Add a location block for the subdirectory with try_files $uri $uri/ /subdirectory/index.php?$args; and ensure the PHP handler can execute files from that path.

Can I move an existing root install to a subdirectory without breaking rewrites?

Yes, but the process requires updating both the server configuration and the WordPress site URL. After moving the files, update siteurl and home in the database or wp-config.php. Then update the .htaccess or Nginx config to reflect the new subdirectory. Finally, flush the rewrite rules by visiting the Permalinks settings page. If you skip the URL update, WordPress will generate rewrite rules for the old root path, and every link will break.

What’s the difference between a 404 and a “Nothing Found” page in WordPress?

A true 404 is served when the server can’t find a physical file and the rewrite rules don’t match any WordPress content. A “Nothing Found” page is served when WordPress matches a rewrite rule but the resulting query returns no posts. The former is a server or rewrite configuration problem; the latter is often a content or query issue. In subdirectory installs, a misconfigured rewrite base can cause a true 404, while a corrupted rewrite rules array can cause a “Nothing Found” on valid URLs. Use the debug snippet earlier in this article to determine which you’re dealing with.

How do I prevent rewrite rules from breaking after a plugin update?

Plugins that register custom post types or taxonomies often flush rewrite rules on activation. If the plugin’s activation hook runs before its post type registration, or if it flushes rules with an incorrect base path, the rewrite array can become corrupted. After updating any plugin that modifies rewrite rules, visit the Permalinks settings page and save to force a clean flush. For high-traffic sites, consider using a version-controlled deployment process that flushes rules as part of the release script, not on every page load.

For a related failure mode that often appears alongside rewrite issues, see What to Fix First When a New WordPress Site Says Nothing Found.