First Boot — Scaffolding an Astro v6 Project
Turning the Astro and Cloudflare decisions into a running project — from bun create astro to first deploy, and knowing when to stop.
The previous post covered the reasoning behind choosing Astro and Cloudflare Workers. This one covers what happened next: turning those decisions into a running project.
The goal was narrow on purpose. Get a dev server running, get the deployment target wired up, and stop. No design, no content system, no styling — just confirm that the toolchain works end to end before building on top of it.
The scaffolding command
Astro has a CLI scaffolding tool, and I used Bun as my package manager from the start:
bun create astro
The interactive wizard walks you through a few choices. I went with:
- Empty project — no starter template. I wanted to understand every file in the repo, not inherit someone else’s opinions.
- TypeScript, strict mode — on a personal blog this is less about necessity and more about reinforcing habits I rely on in larger projects. It catches real bugs in component props and content schemas, and it gives AI assistants more to work with. They generate better code when they have types to anchor on.
- No sample files — just
astro.config.mjs,tsconfig.json,package.json, and a singlesrc/pages/index.astro.
The result is a project with about five files, running Astro v5 (stable). Since I’d already decided to build on v6, the next step was upgrading.
Neither bun create astro@latest nor npx create astro -- --ref next installed the v6 beta. Both gave me stable v5. The scaffolder doesn’t have a beta channel. The solution was straightforward: scaffold with stable, then upgrade:
bun add astro@beta
This pulls in the latest v6 beta and updates package.json accordingly. The Astro v6 upgrade guide covers what’s changed. From here on out, everything I build is on the version Cloudflare will be optimizing for. The same bet I outlined in the previous post.
The beta added a two-step process: scaffold stable, then upgrade. Minor, but it cost me about thirty minutes I didn’t expect.
Adding the Cloudflare adapter
The Cloudflare decision from the previous post needed to connect to real config. That meant adding the adapter early, before building anything else:
bun add @astrojs/cloudflare@beta
Since the project is on Astro v6 beta, the adapter needs to match. The stable release targets Astro v5. The Cloudflare adapter docs cover the full configuration reference.
And in astro.config.mjs:
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'static',
adapter: cloudflare(),
});
Two things worth noting here. First, output: 'static'. Astro defaults to static rendering, and that’s what I want. The adapter handles deploying the static output to Cloudflare Workers. Second, I’m adding the adapter before I have anything to deploy. This is deliberate. I’ve seen too many projects build for weeks against a local dev server and then scramble to sort out deployment late in the process. Wiring up the target environment early, even when there’s nothing meaningful to deploy, surfaces integration issues when they’re easier to fix.
The Wrangler config is similarly minimal. A wrangler.jsonc that names the project, points at the build output, and pins the runtime version:
{
"name": "gtw-astro",
"main": "@astrojs/cloudflare/entrypoints/server",
"compatibility_date": "2025-01-01",
"compatibility_flags": ["nodejs_compat"],
"assets": {
"directory": "./dist",
"html_handling": "drop-trailing-slash"
}
}
The compatibility_date locks the Workers runtime behavior to a known version, which matters for reproducibility. And html_handling: "drop-trailing-slash" tells Cloudflare to serve clean URLs (/about instead of /about/), which is a small detail worth getting right early. The production domain and route configuration will come later.
First boot
bun run dev
Astro’s dev server starts, Vite spins up, and there’s a page at localhost:4321 with whatever I put in index.astro. At this point that’s a single <h1> tag. Not much to look at, but it confirms the toolchain works: Bun resolves dependencies, Vite serves the project, Astro renders the page, and TypeScript is checking types.
A working dev server means the foundation is solid. Everything after this builds on something real.
First build
Before deploying anywhere, I wanted to verify the production build locally:
bun run build && bun run preview
astro build generates the static output into dist/, and astro preview serves it locally. Catching build-only issues locally is cheaper than discovering them in production.
First deploy
With the build verified, I added a deploy script to package.json:
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"deploy": "astro build && wrangler deploy"
}
Then the first deploy was a single command:
bun run deploy
Wrangler comes along with the Cloudflare adapter, so there’s nothing additional to install. Astro builds the static output and Wrangler pushes the result to Cloudflare Workers. The site goes live at a .workers.dev subdomain. Not a real domain yet, but proof that the full pipeline works from source to production. No GitHub integration needed at this point, just a manual push from my machine to Cloudflare’s edge. I’ll likely experiment with hybrid or server Astro setups in the future, but static is the right starting point.
I’m skipping over the Cloudflare account setup and Wrangler authentication here. Both are straightforward and well-documented.
Even with Astro v6 beta, the total setup was five config files and three commands (scaffold, build, deploy), and the site is live at the edge. The beta adds some version-matching friction, but the core workflow is clean.
Connecting GitHub
With the manual deploy confirmed, the next step was reducing friction for future updates. I connected the GitHub repo to Cloudflare’s deployment integration. Commits to main now trigger automatic deploys.
This isn’t CI/CD in any serious sense. There are no tests, no linting, no checks beyond what astro build does. A broken commit goes live immediately. At some point I’ll want quality gates: type checking, maybe accessibility checks, maybe Lighthouse scores. But fast feedback loops matter more at this stage. I’ll add guardrails later.
What the project looks like at this point
After scaffolding, the project is sparse:
├── astro.config.mjs
├── package.json
├── tsconfig.json
├── wrangler.jsonc
└── src/
└── pages/
└── index.astro
No components, no layouts, no styles, no content directory. Just config and a single page. I want to add each layer intentionally, understanding what it does and why it’s there. That’s a luxury of a personal project — at work, scaffolding decisions often get made quickly and lived with for years. Here, I can be deliberate.
What I’m deferring
A few decisions to recap:
- Static output only — the Cloudflare adapter supports server-side rendering, but starting with
output: 'static'keeps things simple. The path to SSR is there if/when I need it later. - No real domain yet — the site initially lives at a
.workers.devURL. The deployment pipeline works end to end, which is what mattered at this stage. Domain, DNS, and production routing are a separate set of decisions. - No quality gates — commits to
mainauto-deploy with no checks beyondastro build. Fast feedback loops now, guardrails when there’s something worth guarding.
The next post covers what I did before writing any components or content: setting up the project documentation that shapes every AI-assisted building session.