DocupDocup
Esc

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.

Documentation written with Docup