Opinionated Astro 6 boilerplate for content-heavy websites.
Stack: Astro 6 (SSG by default, hybrid SSR via DEPLOY_TARGET),
TypeScript strict, Tailwind CSS v4, Preact for islands, pnpm, Lefthook,
ESLint v9, Prettier, Umami analytics, VPS with Caddy.
- Node.js 22+
- pnpm. Node ships Corepack but leaves it inactive. Run
corepack enableonce and it will pick up thepackageManagerfield frompackage.jsonand use the matching pnpm version automatically. If you'd rather not use Corepack,npm install -g pnpm@latestworks too. - Image optimization (
sharp). sharp 0.33+ ships prebuiltlibvipsbinaries for glibc Linux (x64/arm64), macOS, and Windows, so a standard Debian/Ubuntu box needs no extra build tools. On Alpine/musl the@img/sharp-linuxmusl-*package is selected automatically. Ifpnpm installis run with--no-optionalor a hoisting quirk drops the platform binary, runpnpm rebuild sharpto fix it.
pnpm install
pnpm dev # http://localhost:4321The dev server reads .env (see .env.example). At minimum set
SITE_URL. UMAMI_WEBSITE_ID is optional and only used when the
analytics proxy is wired up via Caddy (see deploy/Caddyfile.static).
| Command | Purpose |
|---|---|
pnpm dev |
Local dev server |
pnpm build |
Static production build → dist/ |
pnpm build:static |
Explicit static build (DEPLOY_TARGET=static) |
pnpm build:node |
Hybrid SSR build for @astrojs/node standalone |
pnpm build:cloudflare |
Build for Cloudflare Workers |
pnpm preview |
Serve the built site locally |
pnpm lint |
ESLint |
pnpm format |
Prettier --write |
pnpm typecheck |
astro check |
pnpm check |
typecheck + lint |
@astrojs/node and @astrojs/cloudflare are not listed in
package.json by default. Install whichever you need before running the
corresponding build:
pnpm add @astrojs/node # for DEPLOY_TARGET=node
pnpm add @astrojs/cloudflare # for DEPLOY_TARGET=cloudflarePrimary target: a Linux VPS behind Caddy. The GitHub Actions workflow
in .github/workflows/deploy-vps.yml runs on every push to main
(and can be triggered manually via workflow_dispatch), builds
statically, and rsyncs dist/ over SSH. It expects these repository
secrets: SSH_PRIVATE_KEY, SSH_KNOWN_HOSTS, SSH_HOST, SSH_USER,
DEPLOY_PATH, SITE_URL.
Caddy fragments live in deploy/ and expect your central Caddyfile to
already define these snippets: common_config, security_headers,
content_security_policy, www_to_naked. Ready-to-import versions of
all four — matching the setup this boilerplate assumes — are provided in
deploy/Caddyfile.snippets; paste them
into your central Caddyfile or import the file once. The umami_proxy
snippet provided here serves Umami first-party at /s.js and /api/send.
Cloudflare Workers is available as an alternative via
DEPLOY_TARGET=cloudflare + deploy/wrangler.jsonc.
.github/workflows/deploy-preview.yml deploys a per-PR preview to the
same VPS. On opened, synchronize, and reopened events it builds
statically and rsyncs dist/ to /var/www/previews/pr-<number>/, then
sticky-comments the preview URL on the PR. When the PR is closed (merged
or not), the cleanup job removes that directory. It reuses the same SSH
secrets as the main deploy workflow.
Before using it, edit the PREVIEW_URL / SITE_URL host in
deploy-preview.yml (currently preview-<N>.example.com) and configure
Caddy to serve /var/www/previews/pr-* under a wildcard host.
Previews must not be discoverable by search engines. The workflow
itself does not enforce this — configure it at the Caddy layer for the
preview host, for example by sending X-Robots-Tag: noindex, nofollow
on all responses and/or putting the preview host behind HTTP basic auth.
A robots.txt alone is not sufficient. If you work alone and don't
share PR previews with third parties, you can simply delete
deploy-preview.yml.
Umami is proxied first-party through Caddy so no third-party script tag
is loaded. Set UMAMI_WEBSITE_ID in the environment. The
<Analytics /> component only emits the script in production builds
and only when the ID is present. To register a new site in Umami, add
it in the Umami admin UI, copy the generated website ID, and store it
as the UMAMI_WEBSITE_ID secret (or .env value) for this project.
See CLAUDE.md for the full list of conventions,
architectural rules, and things not to add. The same content in a
tool-agnostic form lives in AGENTS.md.
See LICENSE.