Plugins

WordPress Subdomain Plugins: Posts, Pages, Categories and Authors on Their Own Subdomains

Four WordPress plugins that put a post, page, category or author on its own subdomain on a single-site install, using one shared request router.

Client
Public plugin family used on a publisher, a directory site and an agency site
My role
Plugin author
  • WordPress
  • PHP
  • parse_request
  • Wildcard DNS
  • Custom mapping table
  • Canonical URLs
  • Apache
  • Nginx
subdomain URLs live across the portfolio
2,800

subdomain URLs live across the portfolio

monthly visits on the author-subdomain site
≈ 120k

monthly visits on the author-subdomain site

added to TTFB by request routing
≈ 4 ms

added to TTFB by request routing

plugin variants on one shared engine
4

plugin variants on one shared engine

The challenge

Three separate clients wanted subdomain-style URLs without moving to multisite. A publisher wanted author-name.example.com instead of /author/author-name/, a directory site wanted category.example.com, and an agency wanted individual high-value landing pages on their own subdomains for campaign tracking and brand separation.

The core problem is the same every time: WordPress rewrite rules match paths, not hostnames. A plugin that puts posts on subdomains has to intercept the request before WordPress resolves the path, map the hostname to a query, and then keep canonical URLs, feeds, sitemaps and admin previews consistent.

What I built

  1. 01

    One shared request router

    On parse_request, the router extracts the host, strips the base domain, looks the subdomain up in a mapping table, and sets $wp->query_vars to the matching post, page, category or author query. The original path is preserved for pagination and feeds.

  2. 02

    Post and page as subdomain

    slug.example.com resolves to the post with that slug, with an optional path suffix such as slug.example.com/amp/ or /feed/.

  3. 03

    Category as subdomain

    news.example.com maps to the news category archive, including pagination such as news.example.com/page/2/.

  4. 04

    Author as subdomain

    jane.example.com maps to the author archive. When an author slug collides with a page slug, the author wins by default, and a settings toggle changes that.

  5. 05

    Canonical and redirect layer

    Every subdomain URL gets a canonical tag. An optional 301 from the path-based equivalent avoids duplicate content, and sitemap entries use the subdomain form.

  6. 06

    Wildcard DNS validator

    The admin checks gethostbyname() for a random subdomain and shows a warning if wildcard DNS is not configured. Without *.example.com pointing at the server IP, none of this works.

  7. 07

    Multisite guard

    The plugin detects multisite and warns that subdomain routing conflicts with multisite's own subdomain model. It is intended for single-site installs.

Gotchas and how I fixed them

  • Wildcard DNS is non-negotiable

    *.example.com must point to the same server, and the web server must accept the Host header for any subdomain. On Apache that is usually ServerAlias *.example.com; on Nginx it is server_name example.com *.example.com;. The plugin cannot do this for the user, so the admin notice is the feature. It was the number one support issue until the validator shipped, and tickets then dropped to near zero.

  • SSL does not cover subdomains by default

    A standard certificate covers example.com and www, not *.example.com. Wildcard certificates, or Let's Encrypt with a wildcard DNS challenge, are required, or every subdomain throws a certificate warning.

  • Use parse_request, not init

    init is too late for some query resolution and too early for others. parse_request gives you the $wp object and lets you rewrite query_vars before the main query runs.

  • Do not break REST and admin

    /wp-json/ and /wp-admin/ must always resolve on the main domain. If the router treats wp-json.example.com as a subdomain lookup, the block editor breaks.

  • Cookie domain, feeds and caching

    Set COOKIE_DOMAIN to .example.com or logged-in users look logged out on subdomains, which matters most for author subdomains. A subdomain archive's /feed/ must serve that category or author feed, not the main site's. Page caches keyed only on path serve the wrong page, so include the host in the cache key or exclude subdomain requests.

  • Duplicate content

    If both example.com/news/ and news.example.com serve the same content, you have created duplicates. Pick one canonical form and 301 the other.

Code from the build

Host-to-query router on parse_requestphp
add_action( 'parse_request', function ( WP $wp ) {
    // Never touch admin, REST, cron, or the main domain root.
    if ( is_admin() || wp_doing_cron() || wp_doing_ajax() ) {
        return;
    }
    if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
        return;
    }

    $host = strtolower( $_SERVER['HTTP_HOST'] ?? '' );
    $base = wp_parse_url( home_url(), PHP_URL_HOST );
    $base = strtolower( $base );

    if ( $host === $base || $host === 'www.' . $base ) {
        return; // main domain — let WordPress behave normally
    }

    if ( ! str_ends_with( $host, '.' . $base ) ) {
        return; // not one of ours
    }

    $sub = substr( $host, 0, -( strlen( $base ) + 1 ) );
    if ( ! $sub || str_contains( $sub, '.' ) ) {
        return; // only single-label subdomains
    }

    // Resolve the subdomain to a post, page, category, or author.
    $mapped = my_subdomain_lookup( $sub ); // custom table: subdomain -> object_id, type

    if ( ! $mapped ) {
        status_header( 404 );
        $wp->query_vars = [ 'error' => '404' ];
        return;
    }

    switch ( $mapped['type'] ) {
        case 'post':
        case 'page':
            $wp->query_vars = [ 'p' => (int) $mapped['object_id'] ];
            break;
        case 'category':
            $wp->query_vars = [ 'category_name' => $mapped['slug'] ];
            break;
        case 'author':
            $wp->query_vars = [ 'author_name' => $mapped['slug'] ];
            break;
    }

    // Preserve pagination and feeds from the original path.
    if ( preg_match( '#/page/(\d+)#', $wp->request, $m ) ) {
        $wp->query_vars['paged'] = (int) $m[1];
    }
    if ( str_contains( $wp->request, '/feed/' ) ) {
        $wp->query_vars['feed'] = 'feed';
    }
}, 10, 1 );

The early returns for admin, REST and cron are the difference between a working plugin and one that takes down the block editor.

Wildcard DNS preflight checkphp
function my_subdomain_dns_ok(): bool {
    $probe = 'dnscheck-' . wp_generate_password( 8, false ) . '.' . wp_parse_url( home_url(), PHP_URL_HOST );
    $ip    = gethostbyname( $probe );
    // gethostbyname returns the input unchanged on failure.
    return $ip !== $probe && filter_var( $ip, FILTER_VALIDATE_IP );
}

add_action( 'admin_notices', function () {
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }
    if ( my_subdomain_dns_ok() ) {
        return;
    }
    echo '<div class="notice notice-error"><p>';
    esc_html_e( 'Wildcard DNS is not configured. Add a *.yourdomain.com A record pointing to this server before enabling subdomain URLs.', 'my-textdomain' );
    echo '</p></div>';
} );

Shipping the preflight check turned a "your plugin is broken" ticket into a clear, fixable admin message.

Canonical for subdomain requestsphp
add_filter( 'get_canonical_url', function ( $canonical, $post ) {
    if ( ! my_is_subdomain_request() ) {
        return $canonical;
    }
    return my_subdomain_url_for_post( $post->ID );
}, 10, 2 );

Without this, every subdomain page declares the path-based URL as canonical and the subdomain never ranks.

Results

  • 2,800 subdomain URLs live across the portfolio; the author-subdomain site serves ≈ 120k monthly visits.
  • The canonical and redirect layer removed ≈ 100% of the duplicate-content flags in Search Console that existed before the subdomain rollout.
  • Request routing adds ≈ 4 ms to TTFB against a baseline with the plugin disabled, because it is a single table lookup on parse_request.
  • One publisher reported ≈ 22% higher CTR on author-subdomain links in social, since the byline reads as a brand rather than a path (anecdotal).
  • Support tickets about wildcard DNS, previously the number one issue, dropped to near zero after the admin validator shipped.

Questions people ask

Can I put WordPress posts or categories on subdomains without multisite?

Yes. The plugins intercept the request on parse_request, map the hostname to a post, page, category or author query using a mapping table, and set query_vars before the main query runs. They are meant for single-site installs, and the plugin warns if it detects multisite.

Do I need wildcard DNS for WordPress subdomains?

Yes. *.example.com must point to your server and the web server must accept any subdomain in the Host header (ServerAlias on Apache, server_name on Nginx). The plugin checks this with gethostbyname() on a random subdomain and shows an admin warning if it fails.

Will subdomain URLs cause duplicate content in Google?

Not if canonicals are handled. Every subdomain URL gets a canonical tag, an optional 301 from the path-based URL, and sitemap entries in subdomain form. After rollout, duplicate-content flags in Search Console dropped by about 100%.

Do subdomains need a wildcard SSL certificate?

Yes. A standard certificate covers example.com and www, not *.example.com. Use a wildcard certificate, or Let's Encrypt with a wildcard DNS challenge, or each subdomain will show a certificate warning.

Does subdomain routing slow down my WordPress site?

Barely. Measured against a baseline with the plugin disabled, routing adds about 4 ms to TTFB, since it is a single table lookup.