Cloudflare Tunnel with Custom Status page on the same domain!
- What is it?
Ans: When you expose a local port to the internet using cloudflare tunnel you are binding that domain to the local port directly! For example, I setup a writefreely instance in my windows PC. Local Port: localport:1159(any port you assigned can be used) , Domain: https://domain.tld/
- How to setup those? a. Cloudflare tunnel: Setup is easy. You will get exact commands and instruction from cloudflare tunnel setup page. b. Writefreely:
writefreely config start
writefreely keys generate
writefreely
Now the main things. This post is not about how to setup or run writefreely instance or cloudflare tunnel. Its about how to serve the tunnel site and status page of that tunnel site in the** same domain**!
When user visit: https://domain.tld it check the tunnel domain here: https://internal.domain.tld(another sub-domain), Then, if the tunnel domain will be serving http status under 400 its relayed to the root domain. If not, shows custom page I built with Astro(you can just just a simple html here) here. When the local server at 1159 port not running:
When the Cloudflared Agent is not connected the proxy worker sees this 530 status but if you manually check at https://internal.domain.tld it will show status 1033:
- What about SEO? I also added logic to automatically change the server's HTTP response status to match the exact error code (e.g., 521, 502) or fallback to 503 Service Unavailable. When search engines detect a 5xx server error code, they know the downtime is temporary and will deliberately pause indexing and come back later to check on the real site.
Here id the main middleware which handles everything!
import { defineMiddleware } from "astro:middleware";
import { env } from "cloudflare:workers";
export const onRequest = defineMiddleware(async (context, next) => {
// Get the origin host from environment variable
const originHost = (env as Record<string, string>)?.ORIGIN_HOST;
// If no ORIGIN_HOST configured, just render the Astro page (error page mode via ?code=)
if (!originHost) {
return next();
}
// Build origin URL โ same path/query, different host
const originUrl = new URL(context.request.url);
originUrl.protocol = "https:";
originUrl.hostname = originHost;
originUrl.port = "";
try {
// Proxy the request to the origin server (through the tunnel)
const originResponse = await fetch(
new Request(originUrl.toString(), {
method: context.request.method,
headers: context.request.headers,
body:
context.request.method !== "GET" && context.request.method !== "HEAD"
? context.request.body
: undefined,
redirect: "manual",
})
);
// If origin responds successfully, pass the response through
if (originResponse.status < 400) {
return new Response(originResponse.body, {
status: originResponse.status,
statusText: originResponse.statusText,
headers: originResponse.headers,
});
}
// Origin returned an error โ store the code and render the error page
const errorCode = originResponse.status;
const errorUrl = new URL(context.request.url);
// Rewrite to index page with error code
errorUrl.pathname = "/";
errorUrl.searchParams.set("code", errorCode.toString());
// Rewrite the request internally
context.locals.errorCode = errorCode;
return next();
} catch (e) {
// Fetch completely failed โ tunnel/PC is down
context.locals.errorCode = 521;
return next();
}
});
Here is my wrangler.jsonc This ORIGIN_HOST is the key!
{
"compatibility_date": "2026-06-14",
"compatibility_flags": [
"global_fetch_strictly_public"
],
"name": "error",
"main": "@astrojs/cloudflare/entrypoints/server",
"assets": {
"directory": "./dist",
"binding": "ASSETS"
},
"observability": {
"enabled": true
},
// Set this to your tunnel hostname to enable proxy mode.
// e.g. "tunnel.domain.tld" or "<uuid>.cfargotunnel.com"
// Leave empty/remove to use standalone mode with ?code= query params.
"vars": {
"ORIGIN_HOST": "internal.domain.tld"
}
}
Example Repository to deploy
Example Statuspage: https://cf-status.sayed.app
- Miscellaneous
You can serve the Writefreely local installation on the Fediverse! Here is the proof(๐):
Thanks for reading! Shoot if you have any questions!
Discussion in the ATmosphere