# Docs hosting

How one Worker serves every docs site: host parsing, the proxy rewrite, live and draft loading, and generated routes.

Docup does not deploy a site per customer. One Worker serves the app and every docs site, and the host of the request decides which.

## From host to site

The host helper defines the root domains `docup.dev` and `localhost`. The parsed address comes from the request's own host when it is exactly one label under a root domain:

| Host | Result |
| --- | --- |
| `acme.docup.dev` | `acme` |
| `acme.localhost:3000` | `acme` |
| `docup.dev`, `localhost:3000` | null (the app) |
| `www.docup.dev`, `app.docup.dev`, `api.docup.dev`, `cname.docup.dev` | null (reserved) |
| `a.b.docup.dev` | null (more than one label) |
| a `workers.dev` preview URL | null |

Because the address comes from the request's own host, every screen shows a link that works wherever it is viewed, including `acme.localhost:3000` in development.

If the request is on a custom domain (any host that is not Docup's own root domains or a `workers.dev` preview), the Worker resolves the hostname to its site subdomain. Each isolate caches the resolved subdomain in memory for 60 seconds. On an isolate cache miss, the Worker queries Redis, which caches the resolved subdomain for 600 seconds, falling back to the database.

To prevent search engines from indexing the same site under multiple domains, the Worker redirects GET and HEAD requests on the Docup domain to the active custom domain using a 301 permanent redirect (cached in browsers for 3,600 seconds). The `/docup-turnstile` path is exempt from this redirect so that bot checks can run on the Docup domain.

## The proxy rewrite

The proxy middleware runs on every request that is not a framework file, an API route, or an asset/social card path. For a docs host it rewrites the URL:

| Request | Rewritten to |
| --- | --- |
| `/robots.txt`, `/sitemap.xml`, `/llms.txt`, `/llms-full.txt` | `/sites/<address>/<file>` |
| `/guides/editor.md` | `/sites/<address>/docup-md/guides/editor` |
| Any other path with a dot | Left alone, as a static file |
| `/` | `/sites/<address>` |
| `/guides/editor` | `/sites/<address>/guides/editor` |

Requests to `_next/`, `api/`, `docup-asset/` and `docup-og` are never rewritten, because a rewritten response loses its cache-control.

## Subdomain routes

| Route | Serves |
| --- | --- |
| Layout | The site frame: header, navigation, theme, brand styles, favicon and the Ask AI widget |
| Path page | A documentation page, with metadata, structured data and the Markdown body |
| Markdown page | A page as Markdown |
| `/docup-turnstile` | The Ask AI bot check (exempted from custom domain redirects) |
| Generated files | Sitemap, robots, LLMs manifest files |
| Not found | The page for a path that does not exist |

Each route first calls a verification helper, which checks that the host really belongs to the address in the path. Because the Worker deletes any user-supplied `x-docup-site` header and overrides it with the parsed address, a site cannot be read by requesting `/sites/<address>` on the wrong host.

## Loading a site

Loading a live site returns the site's project id, custom domain, navigation, configuration and `version`, or null when the site does not exist, was never published, or has no live pages. It is wrapped in React's cache, so the layout, page and metadata share one result per request. Loading a published page then reads the text of one page, so a page and each prefetched link stay small.

Navigation uses the live path and position, not the draft ones. If a value is missing, the draft values stand in for them.

If the site is private, hidden or missing, the route answers `404`.

## Draft preview

The preview route renders the same frame from the draft columns for people in the workspace. A session check runs first, and draft loaders check that the project belongs to the workspace. The preview carries a `noindex, nofollow` robots tag and an amber banner.

## Assets and social cards

Uploaded images are stored in R2 under site asset directories and served on the site's host with a one-day cache. Open Graph paths render a 1200 by 630 card for a page. When the owner chose a custom image, it is used instead.

## Views

A view recording function runs after every response on a docs host, via Cloudflare's `waitUntil` mechanism. See [Analytics](/guides/analytics).
