Skip to content
Outer
Esc
↑↓navigate↵open⌘Jpreview
On this page

Website guide

Prerendered Astro pages, Ilha islands, one server route on D1 — the website template.

The website template (Templates) is an Astro site with Ilha islands, deployed the Void way on celld. Pages prerender to static HTML; the Worker only runs the server routes you opt out of prerendering.

Routes are files

src/pages/ is the router: index.astro → /, blog/index.astro → /blog, api/newsletter.ts → /api/newsletter. A new page is a new file. The blog reads Markdown from src/content/posts/ through the collection in src/content.config.ts — each file needs title, description, date; draft: true stays out of the build.

Islands

Interactive parts are plain Ilha components in src/islands/, hydrated where the page says:

---
import NewsletterForm from "../islands/NewsletterForm.tsx";
---
<NewsletterForm client:load />

client:load for above-the-fold, client:idle or client:visible below it. Island props are JSON only — never forward an Astro <slot /> into an island. Until hydration, handlers don’t exist: keep controls usable or disabled, never silently dead.

Server routes

Pages prerender by default — no server runs for them. A route that needs bindings opts out. The starter’s src/pages/api/newsletter.ts validates and saves signups; a read-only route looks like this:

// src/pages/api/subscribers.ts
import type { APIRoute } from "astro";
import { env } from "cloudflare:workers";

export const prerender = false;

export const GET: APIRoute = async () => {
  const { DB } = env;
  const { results } = await DB.prepare("SELECT id FROM newsletter").all();
  return Response.json({ count: results.length });
};

export const prerender = false is the whole switch: without it, the route renders once at build time, where bindings don’t exist. Read bindings only through import { env } from "cloudflare:workers" — Astro 6 removed Astro.locals.runtime.env, which now throws at runtime. Keep server routes few and small: one D1 database, one R2 bucket per site.

Pages build as files (blog.html, blog/hello.html) and links use the no-slash spelling (/blog, /blog/hello); the template sets build.format: "file" and trailingSlash: "never" for that.

Database

D1 is on by default (void.config.ts, d1_databases); Outer provisions it on first deploy. Schema changes ride the deploy as committed migration files in db/migrations/. Outer applies pending migrations before the new code goes live; a failing migration fails the deploy and the previous version keeps serving. Never edit an applied migration — add a new one. The route’s CREATE TABLE IF NOT EXISTS stays as a local-dev backstop, with the same statement as the migration.

Build and deploy

bun run build prerenders the pages, bundles the Worker, and derives dist/wrangler.json from the adapter’s dist/server/wrangler.json (celld-config in astro.config.mjs: allowlisted keys only, main → server/entry.mjs, assets → client, vars → {}). Outer deploys dist/ with celld. Never hand-write dist/wrangler.json.

Was this page helpful?