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 thesessionUploadURL, ormedia.write(...)inrun_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.