Static sites
A static site is a directory of files, served as they are to the people you share the app with. None of your code runs on a request: a platform Worker serves the files, behind the same sign-in as every other app.
What counts as a static site
Section titled “What counts as a static site”Detection makes a directory a static site when either:
- it has an
index.htmland no package.json, or - its package.json depends on a site builder (
@docusaurus/core,vitepress,@11ty/eleventy,react-scripts,astro,viteorparcel) and on no Node server.
A builder with a Cloudflare adapter, such as Astro with @astrojs/cloudflare, is a Worker instead, because pages render on a request.
Deploy one
Section titled “Deploy one”A plain directory of HTML goes up as it is:
jiayang deploy acme/handbook ./handbook --createa static site, served by Workers because it has index.html and nothing to buildDeploying 14 files (212 KiB) from ./handbookUploaded 14 files.Deployed version 1 (6f1d0c9a2b47).https://handbook--acme.jiayang-apps.cloudA site with a builder is built first, on your machine:
a static site, served by Workers (Vite) because package.json depends on vite, builds with `npm run build`, deploys what the build writes install: npm ci build: npm run buildInstalling with `npm ci` ...Building with `npm run build` ...Deploying 9 files (148 KiB) from ./distThe build tool’s own output follows each step, on stderr. Files go up named by their contents, so a redeploy sends only the files that changed.
Which directory is served
Section titled “Which directory is served”After the build, the first of these that exists is served:
dist, build, out, _site, .output/public, .vitepress/dist, docs/.vitepress/dist
public is never picked after a build. Builders copy it into their own output, and on a fresh checkout it’s often the only one there, so serving it would publish your sources instead of your site. With nothing to build, public counts, and after it the app’s directory itself.
A build that writes somewhere else stops the deploy rather than serving the project:
error: `npm run build` ran, but none of dist, build, out, _site, .output/public, .vitepress/dist, docs/.vitepress/dist is there to deploy. Name the directory it writes as build.output in jiayang.json.Name it in jiayang.json:
{ "version": 1, "build": { "output": "site" } }jiayang deploy --no-build serves what’s already built. With nothing built yet, it says what to run:
error: nothing here is built yet: run `npm run build`, or if it writes somewhere other than dist, build, out, _site, .output/public, .vitepress/dist, docs/.vitepress/dist, name that directory as build.output in jiayang.json.What’s left out
Section titled “What’s left out”These never go up:
- files and directories whose names start with a dot, except
.well-known; node_modules;- symlinks;
_worker.js,_worker.js.mapand_routes.json, as files or directories;- whatever
.assetsignorenames.
.assetsignore sits in the served directory, the same file wrangler reads. Each line is a name, a path, or a pattern with a leading or trailing *. Blank lines and lines starting with # are skipped.
*.mapdraftsnotes.txtA file named like a credential stops the deploy, because everything in the directory is served to everyone the app is shared with. A dotfile counts, though it wouldn’t be served:
error: .env looks like a credential, and everything here is served to everyone who can open the app. Take it out of the directory you're deploying.A key inside a file stops it too, and names what it found:
error: assets/config.js has what looks like a Stripe key in it, and everything here is served to everyone who can open the app. Take it out, or keep it in `jiayang secret set`.Neither check looks at symlinks, at what .assetsignore names, or inside a directory that’s left out: one whose name starts with a dot (except .well-known), node_modules, _worker.js, _worker.js.map or _routes.json.
How files are served
Section titled “How files are served”- Paths. A page is found with or without
.htmland a trailing slash:/aboutreachesabout.htmlorabout/index.html. A request that names the file, like/about.html, is redirected to/aboutwith a 307. - Paths with no file. A request for a path that has no file gets your
index.html, so a front-end router works. A site with a404.htmlat its top gets that page instead, with a404status, unless its package.json depends on a router that runs in the browser (React Router, Vue Router, TanStack Router, Angular’s router and the like) or the404.htmlis a copy ofindex.html. The deploy says when it does. - Content types. Each file is served with the type its extension implies. An extension the platform doesn’t know is served as
application/octet-stream, never guessed as HTML. - Caching. Every response comes back
Cache-Control: private. Apublic,s-maxageorproxy-revalidatedirective is removed, andCDN-Cache-Controland similar headers are dropped, so no shared cache keeps a page meant for signed-in people. A browser asks for every file again on each page load, unless you let it keep hashed files.
_headers and _redirects
Section titled “_headers and _redirects”Both are read from the top of the served directory as configuration, never served as files, and checked when you deploy. They apply to the site’s files: a redirect is followed even where a file is at that path, and neither touches what a Worker’s own code answers.
_headers sets response headers by path, in Cloudflare’s format:
/assets/* X-Content-Type-Options: nosniffIt may not set Set-Cookie, Service-Worker-Allowed, Content-Length, Transfer-Encoding, Connection, Upgrade, or any X-Jiayang-* or CF-* header:
error: _headers sets Set-Cookie, which the platform decides. Take that line out.It holds up to 100 rules, with lines of up to 2,000 characters.
_redirects has one rule per line, from to [status], with 302 when the status is left out:
/old-handbook /handbook 301/team /about 200A 200 serves another path in place rather than redirecting. Write its target the way the page is reached, without .html: Cloudflare sends a request for /about.html on to /about with a 307, and a 200 to /about.html gets that 307 too, so the browser ends up at /about after all. The same goes for /index.html, which is sent to /.
A 200 only works for a path on the same app:
error: _redirects serves https://example.com/team in place, which only works for a path on this app. Use a redirect status, or fetch it in the app's own code.The rule single-page apps are often given, /* /index.html 200, isn’t needed: a path with no file already gets your index.html. Cloudflare refuses that rule as a loop, so the platform leaves it out when it sends your rules and turns on that fallback instead, even when the site has code of its own. With not_found_handling set to anything else in jiayang.json, the deploy stops and says the two disagree.
Any other rule that sends a path to an index.html or /index under the path it matched is refused the same way, since Cloudflare sends /index.html on to its directory and the rule matches again:
error: _redirects has a rule Cloudflare refuses as a loop: /app/* /app/index.html 200. /app/index.html is sent on to its directory, which the rule matches again. Point it at the directory itself, or take it out.A rule’s target is a path or an https:// URL, each path is matched by one rule only, and a trailing # note is a comment. It holds up to 2,000 rules without a * or :name and 100 with, counting every rule after the first one with either, on lines of up to 1,000 characters. A refused rule stops the deploy before it gets a version number.
Let browsers keep hashed files
Section titled “Let browsers keep hashed files”By default, files come back with max-age=0, must-revalidate, so a browser asks about each one again on every page load. The answer is a 304 with no body, but each of those requests goes through the edge, which checks access and writes an audit entry before it answers, and each one counts as a request.
Most builders put a hash of a file’s contents in its name, like index-4f9a1c2e.js, so a changed file gets a new name. A browser can keep those for a year. Say so in _headers:
/assets/* Cache-Control: max-age=31536000, immutableA page that loads 20 hashed files then makes one request on a return visit instead of 21. The edge adds private, as it does to every response, so only the person’s own browser keeps a copy.
| Built with | Hashed files |
|---|---|
| Vite, VitePress, Docusaurus | /assets/* |
| Astro | /_astro/* |
| Create React App | /static/* |
_headers has to end up in the directory that’s served. A builder copies its public directory there as it is (Docusaurus copies static), so that’s where it goes.
Rules that match the same path add up. A /* rule that also sets Cache-Control is joined to this one, and a no-cache from it still makes the browser ask every time. Keep Cache-Control off wider rules.
Leave HTML, and any file without a hash in its name, as it is. Those keep their names from one deploy to the next, so the browser has to ask to see a new one. A file marked this way without a hash stays as it was in a browser that has it, for up to a year.
Taking someone’s access away refuses their next request, but a file their browser already holds stays there. Keep the long max-age for code, styles and images, not for files that hold data.
Limits
Section titled “Limits”| Files in one deploy | 20,000 |
| One file | 25 MiB |
| One version, all files | 256 MiB |
| Files stored for one app, across versions | 1 GiB |
| Storage for a workspace, shared with its apps’ databases | 1 GB on Free, 10 GB on Team, 100 GB on Business |
Every file any version uploaded counts toward the stored totals until the app is deleted.
A file over 25 MiB is refused before anything uploads:
error: video/tour.mp4 is over 25 MiB, which is more than a site file can beLimits lists the rest of each plan’s limits.
Values a build bakes in
Section titled “Values a build bakes in”A site has no code running on a request, so its build is the only thing that reads the app’s environment variables. The build runs with them, and a front end bakes in the ones it exposes, such as VITE_*. Changing one takes effect on the next jiayang deploy.
- Share an app to let people in.
- Versions and rollback to put an earlier site back.