External Publication
Visit Post

Cloudflare Tunnel with Custom Status page on the same domain!

Sayed's Blog June 14, 2026
Source
  1. 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/

  1. 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:

  1. 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

  1. 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

Loading comments...