Skip to content

Deploy Astro to the edge

An Astro site deploys in one of two ways:

  • As static files, Astro's default output: every page is rendered at build time and served from Cloudflare's network, like a Vite app. See Static output.
  • Rendered on demand, with the @astrojs/cloudflare adapter: pages that opt out of prerendering run in a Worker, next to your prerendered pages and static files. See Server rendering.

Detection

A project is treated as Astro when astro is in dependencies or devDependencies, or when an astro.config.mjs, .js, .ts, .mts or .cjs file sits at its root. Astro is checked before Vite, which it runs on.

Defaults

SettingDefault
Install commandFrom your lockfile, see Frameworks
Build commandnpx astro build
Output directorydist

If your config sets outDir, set the same output directory on the project.

Server rendering

Add Cloudflare's adapter:

Terminal
npx astro add cloudflare
astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
 
export default defineConfig({
  adapter: cloudflare({ imageService: 'compile' }),
});

Pages stay prerendered by default. A page with export const prerender = false (or every page, with output: 'server') is rendered on each request. The build command doesn't change: astro build writes the Worker, your prerendered pages and your static files, and Hiraiship publishes them all. The output directory setting is ignored, since the build says where everything is.

Prerendered pages and static files are served without running your Worker. Every other path runs it, and Astro answers unknown ones with your 404 page.

A build with another server adapter (@astrojs/node, @astrojs/vercel…) fails with an error naming the Cloudflare adapter.

Reading variables

Your runtime variables reach the Worker as bindings. Read them from cloudflare:workers in an on-demand page or an endpoint:

src/pages/status.astro
---
export const prerender = false;
import { env } from 'cloudflare:workers';
---
<p>{env.API_TOKEN ? 'configured' : 'missing'}</p>

On a project whose latest deployment rendered on the server, a new variable in the dashboard is both a build and a runtime variable by default. A secret is runtime only, so it never ends up in your pages.

vars in wrangler.jsonc are ignored, and the build log says so: set those values as the project's variables instead. .dev.vars isn't read either.

Images and sessions

The adapter declares two bindings of its own, which Hiraiship doesn't provide. The build log says so, and the build goes on:

  • IMAGES, the Cloudflare Images binding the default image service uses to transform images on request. Images on prerendered pages are still optimized at build time, but a transformed image requested from an on-demand page (/_image) fails. Set imageService: 'compile', as above, so images are only optimized at build time.
  • SESSION, a KV namespace for Astro's sessions. Astro.session isn't available.

A SESSION namespace you declare yourself in wrangler.jsonc is refused like any other binding.

What isn't supported

Hiraiship provides your Worker with its variables and its static files, nothing else. A build whose wrangler.jsonc declares other bindings fails, naming each one: KV namespaces, D1 databases, R2 buckets, Durable Objects, queues, service bindings, Workers AI, Hyperdrive, Analytics Engine, cron triggers. A Worker that read env.DB and got nothing would fail on every request.

A _headers or _redirects file isn't applied to a server-rendered site yet (the adapter writes a _headers file for long-lived caching of /_astro/*): the build log says so, and the file isn't published. WebAssembly, text and data modules aren't supported either: the Worker must be JavaScript only.

CPU time

Each on-demand page runs your Worker within your plan's CPU time per request:

FreeStandardProPlatformEnterprise
CPU per request15 ms30 ms50 ms50 ms50 ms

That's enough for a typical page. A heavy render that goes past it fails with Cloudflare's exceededResources error. See Limits.

Static output

Without an adapter, Astro builds a static site. A build with another on-demand rendering adapter (@astrojs/node…) has a server half, dist/server, and fails with an error rather than publishing pages that would never render. Remove the adapter, and output: 'server' if you set it, to build a static site:

astro.config.mjs
import { defineConfig } from 'astro/config';
 
export default defineConfig({});

Environment variables

These are the build variables every Astro site has. A server-rendered site also reads runtime variables, see Reading variables.

Astro inlines import.meta.env.PUBLIC_* values into your pages and scripts at build time. Add them as build variables: in the dashboard, a new variable on a static Astro project is a build variable by default.

src/pages/index.astro
---
const response = await fetch(`${import.meta.env.PUBLIC_API_URL}/products`);
const products = await response.json();
---
<ul>{products.map((product) => <li>{product.name}</li>)}</ul>

Variables without the PUBLIC_ prefix are readable while the site builds (in the frontmatter of a page, through import.meta.env), but never reach the browser's scripts. Whatever ends up in the pages is downloaded by every visitor, which is why a secret can't be a build variable.

For one deployment only, pass them to the CLI:

Terminal
hiraiship deploy --build-env PUBLIC_API_URL=https://staging-api.example.com

404 page

A src/pages/404.astro page becomes 404.html, served with a 404 status for any path with no matching page. On a static site without one, unknown paths get your index.html. See Static sites.