How I ship a static Astro app on Cloudflare
The exact boundary I use between prerendered pages, browser-side calculations, Worker routes, assets, cache headers, and release checks.
“Static app” sounds contradictory until you separate the page from the action.
The explanation, labels, inputs, and examples can be plain HTML. A calculator can run a small function in the browser. Only the pieces that truly need persistence, private data, or a third-party request need server-side logic. That boundary is the architecture behind this site.
Start with the page that does not need a server
Astro’s output: 'static' mode prerenders pages during the build. The output is HTML, CSS, scripts, and other assets that can be served without rebuilding the page for every visitor. Astro’s configuration reference describes static output as the default mode and notes that pages are prerendered unless they explicitly opt out.
That is the correct default for:
- an app directory;
- technical documentation;
- calculator explanations and FAQs;
- privacy, contact, and about pages;
- the shell around a browser-side calculator.
The important part is not the framework name. It is refusing to put request-time work on the critical path when the request does not change the answer.
Put calculations next to the inputs
The contractor calculators on this site do their math in the browser. Length, width, pitch, waste factor, and unit choices do not need to cross a network just to calculate an estimate.
That gives the interface three useful properties:
- the result updates immediately;
- the calculation still works during a brief network interruption; and
- the server never needs to receive the job dimensions.
The page is still fully rendered HTML. The calculator is a focused interactive island attached to that page, not a client-rendered shell that waits for JavaScript before it can explain what the tool does.
Use a Worker only at the real boundary
The Site Check-up is different. A browser cannot reliably inspect another public site’s headers, markup, and performance signals because of browser security boundaries. That action goes through a same-origin endpoint and into the audit service.
The Worker is also where this project handles:
- same-origin API routing;
- release identity;
- permanent URL handoffs;
- security headers on Worker-generated responses; and
- environment-aware preview links.
Cloudflare supports deploying a Worker and its static assets together. Its Static Assets documentation explains that matching assets can be served directly while selected routes run through Worker code. That is the useful middle ground: static by default, programmable where the product actually needs it.
Treat cache behavior as part of the release
Static does not mean “cache forever.” HTML needs a path to freshness. Content-hashed CSS and JavaScript can be cached for a long time because a changed file receives a new name. Stable HTML URLs should be revalidated so a new release does not sit behind an old document.
Cloudflare’s static asset header reference documents the platform’s default revalidation behavior and notes an easy-to-miss detail: when a Worker handles the response, custom headers often need to be attached in the Worker instead of relying only on a _headers file.
This repository therefore makes the boundary explicit:
- hashed build assets: long-lived and immutable;
- fonts and images: cached with a controlled lifetime;
- HTML: allowed to revalidate;
- Worker responses: security and release headers applied in code.
Make a deployment identify itself
A green build is not proof that production serves that build.
Every production response can carry a short release identifier, and GET /__version can return the same identity. After promotion, the smoke check compares the expected commit with the live response. That catches the uncomfortable class of failure where the code is correct, the deployment command succeeded, and the domain still serves something else.
The release sequence is deliberately boring:
- build and test the candidate;
- deploy that exact commit to preview;
- test the public preview routes;
- verify performance and accessibility in a real browser;
- promote the same version;
- compare the production release identity;
- purge only what is actually stale.
The rule I keep using
If everyone receives the same content, build it once. If the browser can perform the action privately and instantly, keep it there. If the action requires shared state, protected credentials, or cross-origin inspection, give that specific path to the Worker.
That keeps the page understandable, the application responsive, and the infrastructure proportional to the job.
See the pattern in the app catalog, run the Site Check-up, or inspect the interaction model in the contractor calculators.