Jamdesk Documentation logo

Cloudflare Workers

Proxy /docs requests through a Cloudflare Worker to your Jamdesk documentation site. Covers Worker setup, route patterns, and caching configuration.

A Cloudflare Worker intercepts requests at /docs on your domain, rewrites them to your Jamdesk subdomain, and returns the response, all at the edge with no origin server required. You can scaffold the Worker automatically with npx jamdesk deploy-proxy cloudflare or set it up manually below.

How It Works

The Worker forwards requests to your Jamdesk subdomain and passes along your domain in the X-Jamdesk-Forwarded-Host header, which Jamdesk uses to verify the domain and apply its settings. It's a one-time setup — if you later change your domain or configuration in the dashboard, the Worker doesn't need updating.

Prerequisites

  • A Cloudflare account with your domain configured
  • Wrangler CLI v3.0+ installed
  • Your Jamdesk subdomain (found in dashboard settings)
  • Your custom domain added to your project in the Jamdesk dashboard (the Worker returns a 403 until the domain is registered and verified)
  • A proxied (orange cloud) DNS record on the hostname serving your docs. Workers only run on proxied records, so a domain that hosts nothing else still needs one — add a placeholder AAAA record pointing at 100:: and set it to proxied.

Quick Setup with CLI

The fastest way to set up your Cloudflare Worker:

npx jamdesk deploy-proxy cloudflare

This interactive command will:

  1. Check that wrangler 3.0+ is installed
  2. Verify your Cloudflare account and show available domains
  3. Resolve your Jamdesk subdomain from the project linked in docs.json
  4. Let you select your target domain from your Cloudflare zones
  5. Generate all required files
  6. Optionally deploy to Cloudflare

If you have access to multiple Cloudflare accounts (common for agencies or teams), the CLI prompts you to choose one before zone selection. Pick the account that owns the domain you're deploying to — Workers routes can only be created for domains in the selected account.

Non-interactive setup

To run without prompts — in CI, or from a script — pass the answers as flags:

jamdesk deploy-proxy cloudflare --slug myproject --domain example.com --yes

--yes generates the Worker files and stops. It never deploys: the zone is assumed from your domain rather than confirmed against your Cloudflare account, so pushing that config live stays an explicit step. Finish with:

cd cloudflare-worker
npx wrangler deploy

With --yes the CLI can't prompt, so it needs to know your subdomain. It reads it from the project linked in docs.json; if that link is missing (run jamdesk deploy once to add it) pass --slug explicitly, or the command stops rather than guess.

If the output directory already exists, --yes stops rather than replace it. Add --force to overwrite, or --output-dir to write somewhere else.

OptionDescription
--slugYour Jamdesk subdomain, the X in X.jamdesk.app (skips auto-detection)
--domainTarget domain (e.g., yoursite.com)
--pathPath prefix; must match your dashboard subpath exactly (default: /docs)
--output-dirOutput directory (default: cloudflare-worker/)
--skip-deploySkip the "deploy now?" prompt in an interactive run (--yes never deploys)
--forceOverwrite the output directory if it already exists
--yesAnswer every prompt with its default (CI mode). Never deploys, and never overwrites an existing directory — combine with --force for that

If you prefer manual setup, continue with the steps below.


Manual Setup

Step 1: Create a Worker

Create a new directory for your Worker and initialize it:

mkdir docs-proxy && cd docs-proxy
npm init -y

Step 2: Add the Worker Code

Create index.js with the following code:

index.js
/**
 * Jamdesk Documentation Proxy Worker
 *
 * Generated by: jamdesk deploy-proxy cloudflare
 * Proxies /docs/* requests AND their assets to YOUR_SLUG.jamdesk.app
 *
 * Assets under /_jd/* (images, fonts, branding, analytics) must also be proxied
 * since they use absolute paths in the HTML.
 */

const JAMDESK_HOST = "YOUR_SLUG.jamdesk.app";

// Paths that are always proxied to Jamdesk
const PROXY_PATHS = [
  "/docs",  // Documentation pages
  "/_jd/",  // All Jamdesk assets (images, fonts, branding, analytics)
];

function shouldProxy(pathname) {
  return PROXY_PATHS.some(prefix => {
    // For the docs path, require exact match or prefix with slash (not /docs.json)
    if (prefix === "/docs") {
      return pathname === "/docs" || pathname.startsWith("/docs/");
    }
    return pathname.startsWith(prefix);
  });
}

function proxyToJamdesk(request, url) {
  // Rewrite the request to Jamdesk
  const proxyUrl = new URL(request.url);
  proxyUrl.hostname = JAMDESK_HOST;
  // Pin the scheme: an http:// visitor proxied as http:// gets a 308 from
  // Vercel's edge pointing at JAMDESK_HOST, and redirect:"manual" hands that
  // redirect straight to the browser — bouncing the visitor off this domain.
  proxyUrl.protocol = "https:";
  // Setting .protocol leaves a non-default .port in place, so :8787 (wrangler
  // dev) or Cloudflare's alternate http ports (8080, 8880, 2052…) would follow
  // us to the https upstream and fail there.
  proxyUrl.port = "";

  // Clone headers and add proxy headers
  const headers = new Headers(request.headers);
  headers.set("Host", JAMDESK_HOST);
  headers.set("X-Forwarded-Host", url.hostname);
  headers.set("X-Forwarded-Proto", "https");
  // Custom header for domain verification (Vercel strips standard forwarding headers)
  headers.set("X-Jamdesk-Forwarded-Host", url.hostname);

  // Don't follow redirects — let the browser handle them so the URL updates.
  // Without this, redirects happen internally and the browser URL doesn't change,
  // which causes the sidebar to mis-highlight the active page.
  const proxyRequest = new Request(proxyUrl, {
    method: request.method,
    headers,
    body: request.body,
    redirect: "manual",
  });

  // Cache all content types at Cloudflare edge (CF doesn't cache HTML by default).
  // Cache duration is controlled by upstream Cache-Control headers.
  // Never cache redirects or errors — they must always hit origin.
  return fetch(proxyRequest, {
    cf: {
      cacheEverything: true,
      cacheTtlByStatus: { "300-399": 0, "400-499": 0, "500-599": 0 },
    },
  });
}

export default {
  async fetch(request) {
    const url = new URL(request.url);

    if (shouldProxy(url.pathname)) {
      return proxyToJamdesk(request, url);
    }

    // /_next/ is ambiguous: the customer's root site may itself be a Next.js app
    // serving its own /_next/ assets. Try the origin first and fall back to
    // Jamdesk when the origin doesn't have it (404) or can't answer at all
    // (5xx — a docs-only domain has no real origin, so Cloudflare returns 522).
    // Hashed asset filenames never collide between the two apps.
    if (url.pathname.startsWith("/_next/")) {
      let originResponse;
      try {
        originResponse = await fetch(request);
      } catch (err) {
        // Log rather than swallow: a genuine platform fault and a domain with
        // no origin at all are indistinguishable in `wrangler tail` otherwise.
        console.warn("origin fetch threw, serving from Jamdesk:", err);
        return proxyToJamdesk(request, url);
      }
      if (originResponse.status !== 404 && originResponse.status < 500) {
        return originResponse;
      }
      return proxyToJamdesk(request, url);
    }

    return fetch(request);
  },
};

Replace YOUR_SLUG with your actual Jamdesk subdomain (e.g., acme if your docs are at acme.jamdesk.app).

If you use a custom subpath from the dashboard instead of the default /docs, replace "/docs" with your subpath in PROXY_PATHS and in the exact-match check inside shouldProxy(). The CLI's --path flag does this for you when it generates the file, but only against an unmodified template; regenerating overwrites any custom entries you've added to PROXY_PATHS by hand. If you've customized this Worker, edit the /docs entries in place instead.

The X-Jamdesk-Forwarded-Host header is required, and a missing header fails quietly — requests still succeed, but pages serve noindex with canonical links pointing at YOUR_SLUG.jamdesk.app instead of your domain, so search engines never index your docs. A 403 is the opposite problem: the header is present, but names a domain that isn't registered and active for this project.

Step 3: Configure wrangler.toml

Create wrangler.toml to configure your Worker:

wrangler.toml
name = "docs-proxy"
main = "index.js"
compatibility_date = "2024-01-01"

# Serve only via the routes below, not on the public <name>.workers.dev URL —
# that URL is a second way into the same proxy and is worth closing.
workers_dev = false

# Single catch-all route; the worker handles path filtering internally
routes = [
  { pattern = "yoursite.com/*", zone_name = "yoursite.com" },
]

If your Cloudflare login has access to more than one account, add account_id = "<your account id>" as well — otherwise wrangler deploy stops rather than guess which account to deploy into. npx wrangler whoami lists your account IDs.

If your site also serves traffic on www.yoursite.com, add a second route so the Worker handles both:

routes = [
  { pattern = "yoursite.com/*", zone_name = "yoursite.com" },
  { pattern = "www.yoursite.com/*", zone_name = "yoursite.com" },
]

Step 4: Deploy

Deploy your Worker to Cloudflare:

npx wrangler deploy

Step 5: Verify

Visit https://yoursite.com/docs to confirm your documentation is being served correctly.

Troubleshooting

If you renamed your subpath in the dashboard (say, /docs/help) but your Worker's PROXY_PATHS still only lists /docs, requests to /help/* never reach Jamdesk: they fall through to fetch(request) and 404 at your own origin. /docs/* keeps working in the meantime (Jamdesk serves both prefixes), which is exactly why this is easy to miss.

Fix: add your new subpath to PROXY_PATHS. Re-run jamdesk deploy-proxy cloudflare --path <subpath> if the Worker is still an unmodified template, or edit the array by hand if you've customized it.

Wrangler prefers CLOUDFLARE_API_TOKEN over its OAuth login, and it can't start an OAuth login while that token is set. This command needs OAuth to list the zones in your account, so it stops with the token's location instead of failing inside wrangler.

Wrangler also reads .env from the directory you run in, so the token can be set for wrangler without being in your shell — echo $CLOUDFLARE_API_TOKEN printing nothing doesn't rule it out. Check for a .env in the current directory too.

Fix: either grant the token account:read and zone:read and re-run, or run without it:

env -u CLOUDFLARE_API_TOKEN jamdesk deploy-proxy cloudflare

If the token comes from a .env, env -u won't help — run the command from a directory that has no such .env, or move the file aside for the run.

The CLI shows your available domains before zone selection. If you see "No domains found":

  1. Verify you're logged into the correct Cloudflare account
  2. Check that your domain is added and active in the Cloudflare dashboard
  3. Run the CLI again and select "No" when asked to continue with the current account to switch accounts

If you have multiple Cloudflare accounts:

  1. Run jamdesk deploy-proxy cloudflare
  2. When prompted to select an account, choose the one with your domain
  3. If you need a completely different login, select "Switch to different login"
  4. The CLI will log you out and prompt you to log in with the correct credentials

This error means the selected zone doesn't match your Cloudflare account. Either:

  • You selected a zone that belongs to a different account
  • The zone was removed from Cloudflare

Fix: Re-run the CLI and select the correct zone from the list, or switch to the account that owns the zone.

Ensure your route pattern uses a catch-all: yoursite.com/* (not just yoursite.com/docs*). The Worker's internal shouldProxy() function handles path filtering.

Two common causes:

  1. Worker not running. Ensure your DNS record is set to Proxied (orange cloud) in Cloudflare. Workers only run on proxied records.
  2. Missing X-Forwarded-Host header. The Worker must set this header so Jamdesk generates correct asset URLs.

If you see "Domain is not authorized to serve this content":

  1. Verify your domain is registered in the Jamdesk dashboard
  2. Complete DNS verification (TXT record) for your domain
  3. Ensure the X-Jamdesk-Forwarded-Host header is set in your Worker code
  4. Check that your domain maps to the correct project

The domain must be verified before the Worker can serve documentation.

Workers only run on proxied (orange cloud) DNS records. If your A record is set to "DNS only" (gray cloud), requests go straight to the origin and skip the Worker entirely.

Fix: Toggle the A record to proxied (orange cloud) in Cloudflare DNS. Same applies to subdomains: any record with a Worker route must be proxied.

Jamdesk verifies ownership by reading your DNS record values directly. Cloudflare's proxy (orange cloud) masks these values, so verification can't complete.

Fix:

  1. Set the DNS record to DNS only (gray cloud)
  2. Wait for verification to complete (status changes to active in the dashboard)
  3. Switch back to Proxied (orange cloud) so the Worker runs

Short version: gray cloud to verify → orange cloud to serve.

Jamdesk serves documentation HTML with Cache-Control: no-store, so Cloudflare doesn't cache pages at the edge (cf-cache-status: BYPASS). Every request renders the current version, and published changes appear immediately with no cache delay.

Static assets under /_next/ and /_jd/ (JavaScript, CSS, fonts, images) are served with long-lived immutable cache headers, so Cloudflare caches them at the edge. Their filenames are content-hashed, so each build produces new URLs and updated assets are picked up automatically. No purge required.

cacheEverything: true lets Cloudflare cache those static assets on the proxied route; it doesn't override the no-store on HTML. To clear the edge cache manually, use Cloudflare's Purge Cache (Caching → Configuration → Purge Everything).

The CLI requires wrangler 3.0+. Update with:

npm install -g wrangler@latest

What's Next?

Custom Domain Only

Stop your subdomain from answering directly

Custom Domains

Verify DNS and troubleshoot

Subpath Hosting

Serve docs at /docs