Subdomain Routing with Cloudflare Pages Middleware
Learn how to host branded subdomains on a single static site deployment using Cloudflare Pages edge middleware to prevent duplicate content penalties and handle clean routing.
Table of Contents6 sections

When you manage a static publication that includes standalone interactive tools, you eventually face a hosting dilemma. You want your users to access a focused tool on a clean, memorable address like timer.example.com, but you do not want to maintain a separate codebase, configure an independent CI pipeline, or burn extra build minutes. Pointing a subdomain to the same Cloudflare Pages project seems like an easy fix, but it introduces immediate architectural challenges. Search engines can index the exact same content under both domains, which splits your ranking authority and triggers duplicate content penalties. Naive edge redirects can also cause infinite loops or accidentally break static assets like JavaScript bundles and CSS files. For a related implementation, see Automated Content Syndication Canonical Seo Protection.
This article explains how to use Cloudflare Pages edge middleware to host branded subdomains on a unified static repository. You will learn how to intercept incoming requests by inspecting the Host header, pass through static assets, rewrite root paths for your sub-apps, and issue permanent 301 redirects for any unassociated pages back to your apex domain.
The Multi-App Monorepo Dilemma
Many engineering teams split every sub-project into its own repository or separate Pages project. While this isolates deployment concerns, it increases maintenance friction. Shared design tokens, typography styles, and utility functions must be published as internal packages or copy-pasted across repos. For smaller teams or solo developers, a monorepo built with a modern static site generator is much easier to maintain.
When you point multiple subdomains to a single Cloudflare Pages project, the platform treats all incoming requests as if they belong to the primary deployment. Without interception, visiting timer.example.com/about would serve the main site about page instead of protecting your canonical domain structure. You need a layer that runs before static file resolution to inspect the request and decide its fate.
Cloudflare Pages Edge Middleware Architecture
Cloudflare Pages supports middleware through a file named functions/_middleware.ts. This script executes at edge locations globally before the CDN attempts to match static files on disk. Because the middleware runs in the Workers runtime, execution takes only a few milliseconds and incurs negligible overhead.
The middleware intercepts the incoming Request object, reads the headers, and either passes the request through to the static assets, rewrites the URL internally, or returns an HTTP redirect response. This architecture lets you handle routing rules directly at the network edge without provisioning dedicated origin servers. For a related implementation, see Abi Filters Architecture.
Implementing the Four Routing Rules
To manage a dedicated subdomain like timer.example.com while keeping the rest of your site on example.com, your middleware must follow a strict evaluation order. The following implementation uses TypeScript to handle host header matching, asset preservation, root normalization, in-app routing, and canonical bounces.
export const onRequest: PagesFunction = async (context) => {
const url = new URL(context.request.url);
const host = context.request.headers.get("host") || "";
// Fast exit for apex domain traffic
if (!host.startsWith("timer.")) {
return context.next();
}
// Rule 1: Allow static assets and workers to pass through
if (
url.pathname.startsWith("/assets/") ||
url.pathname.startsWith("/timer-") ||
url.pathname === "/robots.txt" ||
url.pathname === "/favicon.ico"
) {
return context.next();
}
// Rule 2: Rewrite root requests to the sub-app directory
if (url.pathname === "/") {
const targetUrl = new URL("/timer/", url.origin);
return context.env.ASSETS.fetch(targetUrl);
}
// Rule 3: Allow internal app routes to render
if (url.pathname.startsWith("/timer/")) {
return context.next();
}
// Rule 4: Canonical 301 bounce for unrelated paths
const canonicalUrl = new URL(`https://example.com${url.pathname}${url.search}`);
return Response.redirect(canonicalUrl.toString(), 301);
};
Let us break down how each rule operates within this function:
-
Apex Fast Exit: The middleware checks the Host header immediately. If the request is not destined for a subdomain starting with timer., it calls context.next() and lets the platform handle the request normally. This ensures your primary domain traffic experiences zero performance penalty.
-
Asset Passthrough: Web applications rely on static assets, CSS, and worker scripts. If a browser requests /assets/main.js, the middleware matches Rule 1 and passes the request through. Without this step, your stylesheet requests might trigger redirects and fail to load.
-
Root Normalization: When a user visits timer.example.com/ directly, the path is /. Rule 2 intercepts this and fetches the internal static path /timer/ without changing the user’s visible address bar if combined with a fetch rewrite, or it normalizes the entry point.
-
Canonical Bounce: If a user attempts to navigate to an unrelated section of the main site from the subdomain, such as timer.example.com/articles/foo, Rule 4 issues a permanent 301 redirect to the apex domain. Notice how it appends url.search to preserve query parameters, ensuring tracking tags and user filters are never lost.
Unit Testing Edge Logic Locally
Testing edge routing configurations traditionally required deploying code to a staging environment. You can avoid this by using Node.js built-in test runners to simulate Request objects against your middleware function locally.
import test from "node:test";
import assert from "node:assert";
import { onRequest } from "./_middleware";
test("redirects non-app paths to apex with query params", async () => {
const request = new Request("https://timer.example.com/articles/guide?ref=twitter", {
headers: { host: "timer.example.com" }
});
const context = {
request,
next: async () => new Response("ok"),
env: { ASSETS: { fetch: async () => new Response("asset") } }
};
const response = await onRequest(context as any);
assert.strictEqual(response.status, 301);
assert.strictEqual(
response.headers.get("location"),
"https://example.com/articles/guide?ref=twitter"
);
});
This test verifies that incoming requests on the subdomain targeting invalid paths receive the expected 301 status code while preserving query parameters. Running these checks in your continuous integration pipeline catches routing regressions before they reach production.
Conclusion on Subdomain Edge Routing
Hosting branded subdomains on a single static site deployment requires careful management of edge request lifecycles. By implementing targeted middleware that evaluates the Host header, bypasses static assets, and enforces canonical redirects, you can provide dedicated micro-tool experiences without sacrificing SEO equity or duplicating your codebase. Testing these rules locally gives you confidence before deploying changes to your production infrastructure.
Continue Exploring
You Might Also Like

Prevent Private URLs From Leaking Into Static Site Output
Stop internal hostnames, staging URLs, and private network details from leaking into static-site HTML, JavaScript, feeds, source maps, and metadata.

Safe Multi-Environment Database Orchestration
Learn how to manage PostgreSQL databases across local, staging, and production tiers securely without schema drift or credential leaks.

Post-Deploy Sanity Checks: Verify Production Without Re-Running Your Test Suite
A practical guide to designing small post-deploy sanity checks that verify the live release, critical dependencies, and rollback signals without duplicating CI.