# Serving a published static site at a custom domain — recipe + known gaps

Notes captured 2026-06-01 during the getmaxy.com apex setup. The existing runbook
(`manual-setup.md`) covers getting a hostname to *reach* the server, but not the
application-layer step of making that hostname *serve a published site at its root*.
These are the gaps that cost time, and the recipe that works.

---

## Gap 1 — an alias hostname serves the PUBLIC-AGENT SPA at `/`, not a published site

Adding a hostname to `alias-domains.json` (so `isPublicHost()` is true) makes the
platform serve the **public chat-agent landing** (`brandedPublicHtml`) at `/`. It does
**not** serve a published static site. Published sites live at `/sites/<slug>/` and are
reachable under any alias host at that path, but the bare root always renders the agent SPA.

There is **no per-domain → landing-site mapping** in the platform. `resolveDefaultSlug()`
reads `account.defaultAgent` (a chat agent), never a site slug. Confirmed by reading
`server/server.js` `app41.get("/")` and the `/sites` route.

**Implication:** "point getmaxy.com at the getmaxy site" is two jobs, not one:
1. Routing (DNS + tunnel ingress + alias-domains) — gets the host to the server.
2. Root-serving (below) — makes `/` render the site instead of the agent SPA.

State this split to the operator up front. Do not imply step 1 alone will show the site.

---

## Recipe — serve `/sites/<slug>/` at a custom domain root

1. **Ingress** — add the hostname(s) to `${CFG_DIR}/config.yml` (per `manual-setup.md`
   multi-ingress section), validate, reload the brand service.
2. **alias-domains.json** — add the hostname(s) so the public path filter allows `/sites/`.
3. **DNS** — apex via dashboard CNAME (`@` → `<UUID>.cfargotunnel.com`, proxied); `www` via
   dashboard too if the cert is not authorised for the zone (see Gap 2).
4. **URL Rewrite Rule** (Cloudflare dashboard) — rewrite every path on the host to the site
   subtree, invisibly (visitor URL stays clean):
   - **Rules → Overview → Create rule → URL Rewrite Rule** (or **Create from template →
     "Rewrite path of moved section"**).
   - If — Custom filter expression: `(http.host eq "<domain>") or (http.host eq "www.<domain>")`
   - Then — Path → Rewrite to → **Dynamic**: `concat("/sites/<slug>", http.request.uri.path)`
   - Leave query string unchanged. Deploy.
   - Effect: `<domain>/` → origin `/sites/<slug>/` → `index.html`; `<domain>/foo.webp` →
     `/sites/<slug>/foo.webp`. Works because `/sites/` is in `PUBLIC_ALLOWED_PREFIXES`.

No server restart needed for step 4. Verify: `curl -s https://<domain>/ | head` shows the
site markup, not the SPA `<div id="root">`.

---

## Gap 2 — `tunnel route dns` misroutes ANY hostname whose zone the cert isn't authorised for

`manual-setup.md` Step 4 warns about apex misrouting. The same failure hits **non-apex**
hostnames too: if the login cert is authorised for zone A (e.g. `maxy.bot`) but you route a
host in zone B (e.g. `www.getmaxy.com`), `cloudflared` appends zone A and silently creates
`www.getmaxy.com.maxy.bot` under the wrong zone — exit 0, looks successful.

**Rule:** the CLI can only route hostnames in a zone the cert was authorised for at
`tunnel login`. For a hostname in any other zone (apex OR subdomain), create the CNAME in
the dashboard, or re-run `cloudflared tunnel login` and authorise the new zone first. Do not
trust exit 0 — check the emitted record name in the output (`Added CNAME <name>`); if it has
an extra zone suffix, it misrouted and the stray record needs dashboard deletion.

---

## Gap 3 — dashboard labels are stale in `dashboard-guide.md`

- URL rewrites are **not** called "Transform Rules" in the current UI. They are under
  **Rules → Overview → Create rule → URL Rewrite Rule**, with templates via **Create from
  template** (the relevant one: **"Rewrite path of moved section"**).
- **Page Rules is the wrong tool** for transparent rewrites — it only does forwarding
  (HTTP 301/302 redirects) with a visible URL change. Do not suggest Page Rules when the
  requirement is an invisible origin-path rewrite.
