I wanted any subdomain that doesn’t exist, like hello.example.com, to show my own 404 page instead of a “site can’t be reached” error, without touching the subdomains that already run real apps. Three Cloudflare details made it work.
1. A wildcard record never beats a real one
A proxied AAAA record for *.example.com pointing to 100:: (Cloudflare’s placeholder for “handled at the edge”) catches every name that has no record of its own. Explicit records always win, so existing subdomains resolve exactly as before.
2. A route with no Worker is a valid route
A Worker route *.example.com/* sends all those requests to one Worker. To keep an app out of it, add a more specific route like app.example.com/* and attach no Worker. The most specific route wins, so app traffic never reaches the Worker at all.
3. A static-assets Worker can run code first
Setting run_worker_first: true in wrangler.jsonc gives the script every request before the static files are served, so it can decide by hostname:
const SITE_HOSTS = new Set(["example.com", "www.example.com"]);
const APP_HOSTS = new Set(["app.example.com"]);
export default {
async fetch(request, env) {
const host = new URL(request.url).hostname;
if (SITE_HOSTS.has(host)) return env.ASSETS.fetch(request);
if (APP_HOSTS.has(host)) return fetch(request); // safety net
const page = await env.ASSETS.fetch(new URL("/404.html", request.url));
return new Response(page.body, { status: 404, headers: page.headers });
},
};
One thing to remember: the catch-all is greedy. Every new app subdomain needs its own no-Worker route, or it gets the 404 page too.