Docs API
Site Worker Routes
The endpoints the public site Worker answers itself — the media proxy, the API forwarders, the registry, and the static JSON the front end fetches.
Everything on the other API pages is served by site-api. This page covers the
routes the public site Worker owns and answers itself: a media proxy, four
forwarders that hand a request to site-api, and a handful of static JSON files
the front end fetches lazily.
The split matters when you are debugging. A 404 from /static/… came from a
different deploy than a 404 from /api/…, and only one of them is in this
repository. See who answers a request.
Media proxy
GET /static/<encoded-target-url>
HEAD /static/<encoded-target-url>
Fetches an image, video, audio file, or font from an allowlisted upstream and re-serves it from this origin. It exists so Telegram-hosted mood media and YouTube thumbnails can be embedded without leaking a visitor’s IP to those hosts, and without the mixed-origin CSP problem that comes with hotlinking.
Rate limit: 240 requests / 60s. Successful responses get
Cache-Control: public, max-age=86400, s-maxage=86400 unless the upstream sent
its own.
Allowlisted hosts are the Telegram family (t.me, telegram.org,
telegram.me, telegram.dog, telesco.pe, cdn-telegram.org,
cdn1–cdn5.telegram-cdn.org) plus i.ytimg.com for YouTube posters and
yt3.googleusercontent.com / yt3.ggpht.com for channel avatars. Redirects are
followed at most three deep, and every hop is re-checked against the
allowlist — an allowlisted host cannot bounce the proxy to somewhere else.
Every response carries a deliberately boring set of headers:
access-control-allow-origin: *
content-disposition: inline
content-security-policy: default-src 'none'; sandbox
x-content-type-options: nosniff
set-cookie is stripped from the upstream response, as are hop-by-hop headers.
Only image/*, video/*, audio/*, and font/* content types are passed
through; anything else is 415 with an empty body. That is the property worth
understanding: this is a media proxy, not a general-purpose fetcher, and it
will not return you an HTML page or a JSON document no matter what you point it
at.
Errors: 400 Invalid target URL. (not allowlisted, or unparseable);
403 Invalid static proxy signature.; 429 Too Many Requests.;
502 Upstream fetch failed.; 415 for a disallowed content type. All are
plain text, not JSON.
Request signing
Proxy URLs are signed. STATIC_PROXY_MODE decides how strictly that is
enforced:
| Mode | Unsigned URL | Invalid signature |
|---|---|---|
observe (default) |
Served, logged | Served, logged |
accept-both |
Served, logged | 403 |
enforce |
403 |
403 |
The staged rollout is intentional: observe lets an already-published page’s
old unsigned URLs keep working while new ones ship signed. Build proxy URLs
through the site’s own helper rather than hand-assembling them, or they will
break the day the mode advances.
YouTube metadata
GET /static/youtube/<11-char-video-id>/metadata.json
A special case on the same route — not a proxy fetch but a resolved lookup.
Returns {"channelName": "…", "channelUrl": "…"|null} with
access-control-allow-origin: * and public, max-age=86400, s-maxage=86400.
502 YouTube channel avatar unavailable. if the lookup fails. The path must
carry no query string, or it is treated as a normal proxy target instead.
API forwarders
Four routes exist only to hand a request to site-api:
| Route | Behavior |
|---|---|
/api/* |
Forwards over the API service binding (deploy/preview) or HTTP to API_DEV_ORIGIN (dev). All methods. |
/oauth, /oauth/* |
Same forwarder, for the OAuth hub. All methods. |
/v2/* |
Not a proxy — a 308 redirect to /api/v2/*. |
/v2/* catching people out is worth calling out: on the public site the
canonical path is /api/v2/…, and a bare /v2/… only redirects there. A client
that does not follow redirects, or that downgrades POST on redirect, will see
a request that appears to vanish.
When neither the service binding nor a dev origin resolves, the forwarder
answers 503 {"error":"API service binding unavailable"}. In production none
of this runs at all — Cloudflare route patterns send /api/* straight to
site-api and the site Worker never sees the request.
/oauth/login is the one exception: it is answered locally, not forwarded. It
302s to the ?next= path with Cache-Control: no-store, max-age=0, defaulting
to /dev/portal. next is rejected unless it is a same-site absolute path —
values starting //, containing a backslash, or naming another origin fall back
to the default rather than redirecting off-site.
Dev portal
ALL /dev → 302 /dev/portal
ALL /dev/portal/api/* → forwarded to site-api, admin paths only
The forwarder is registered as a catch-all but narrows itself: only paths under
admin are forwarded (to site-api’s /api/admin/*), and anything else is
404 {"error":"Not found"} with no-store. It is a narrow window onto the
admin API, not a second general proxy — see Internal Endpoints.
Static JSON
Prerendered at build time and served as static assets from the edge. No auth, no rate limits, no query parameters.
| Path | Contents |
|---|---|
/docs/search.json |
The docs search index — every non-draft page’s title, description, group, H2/H3 headings, and up to 4000 characters of body text. Fetched whole on first open of the docs search dialog. |
/palette.json |
{"posts":[{"title","path"}]} — the four newest blog posts, for the site-wide command palette. Separate from the page so /mood never pays for a Ghost fetch during SSR. |
/r/<name> and /r/<name>.json |
Component registry items, in the shadcn registry format, so a component on /components can be installed by URL. Both paths serve the identical document; the .json variant re-exports the other. Built from the components content collection, non-draft entries whose install.type is registry, plus a generated utils item. |
/mood/rss.xml is the exception in this group — it is server-rendered per
request (public, max-age=0, s-maxage=300, up to 50 items, read from the D1
archive), not prerendered, because the mood feed changes between deploys. It
returns a plain-text 500 Failed to generate RSS feed. if the archive read
throws. The blog feed, llms.txt, and the sitemap are all build-time static —
see Feeds & Machine Output.