Pattern: a content site is just files (no worker)

When to reach for this. A portfolio, a landing page, a photo essay, a docs site — pages and images, no per-request logic. If nothing needs to run on a request, you don't need a worker at all.

The stance. Start static. Files under public/ serve straight off R2 with zero cold-start latency, independent of any runtime — the simplest, fastest, most durable thing a space can be. Reach for a worker (unfolder.routing) only when you actually need dynamics (a form, per-request rendering, gating). Don't build an App DO to serve a homepage.

The shape

public/
  index.html      # → served at /
  about.html      # → served at /about (requesting /about.html 307-redirects there)
  style.css       # → served at /style.css

Anything under public/ is a static asset, served with the public/ prefix stripped. (At the workspace root, anything that isn't .ts/.tsx/.js/.jsx/ .json is a static asset too — but public/ is the unambiguous home for web text.) Drop the files with write_files and host.publish():

// in run_code
async () => {
  await state.writeFile("/public/index.html", "<!doctype html><title>Ada</title><h1>Ada Lovelace</h1>");
  await state.writeFile("/public/style.css", "body{font:16px system-ui;max-width:40rem;margin:4rem auto}");
  return host.publish();
}

Binary belongs in media, web text in the workspace

This is the one rule that keeps serving unambiguous:

  • Images, video, audio, fonts, PDFs → media. Upload them out-of-band (never base64 through the model): get_urls(slug) → PUT to the sessionUpload URL, or media.write(...) in run_code. Reference them by their site-root URL (/hero.jpg). The media bucket rejects code-like uploads (html/css/js) — they belong in the workspace. See media.
  • html/css/js → the workspace public/. Built and deployed atomically with your space, served off R2.

Your Terms — a space is private until you open it up

Until you author a robots.txt, the platform serves a deny-everyone default (User-agent: * / Disallow: /) — so a half-built site isn't indexed before it's ready. When production is real and you want it found, open up by writing your own public/robots.txt (it always wins):

User-agent: *
Allow: /
Sitemap: https://<your-site-host>/sitemap.xml

(<your-site-host> is the site URL get_urls returns — your subdomain or custom domain.)

Add Content-Signal: lines to declare AI-use intent, and a sitemap.xml, the same way (static files under public/). Full guide: seo.

When to graduate

The moment you need a form, a visitor count, gated content, or a page rendered from data, keep the static files and add an App worker for just those routes — the worker only sees what the static tier didn't answer. Start with unfolder.routing.