Static / Jamstack sites delivered behind a hardened, self-hosted edge. I treat the edge — WAF, TLS, headers — as part of the project, not someone else's problem.
Runnable examples — the build, the content layer, the deploy, and the hardened edge:
| File | What it shows |
|---|---|
examples/astro.config.mjs |
Astro static build config (sitemap, hashed assets) |
examples/content.config.ts |
Typed content collection (Astro 5 Content Layer) |
examples/projects.astro |
The read side — render the collection to zero-JS HTML |
.github/workflows/deploy.yml |
Push-to-deploy — build + rsync to the edge over SSH |
examples/bunkerweb-compose.yml |
The hardened edge — BunkerWeb WAF + TLS + rate limit, as code |
examples/security-headers.conf |
HSTS / CSP / nosniff headers |
Placeholders throughout; deploy host/user/key come from repo secrets — nothing sensitive is committed.
Astro 5's Content Layer API, defining a typed content collection from local files:
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders'; // Astro 5 Content Layer
export const collections = {
projects: defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/projects' }),
schema: z.object({ title: z.string(), date: z.date(), tags: z.array(z.string()) }),
}),
};And the part people skip — the edge actually being secure:
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Content-Security-Policy "default-src 'self'" always;Push-to-deploy on top: a commit triggers a GitHub Action that builds and rsyncs the output to
the edge — no container rebuild, rollback is just a redeploy.
- Static-first, islands for interactivity. Ship HTML; keep JavaScript surgical.
- The edge is part of the project — WAF, automatic TLS, HSTS/CSP, rate limiting, built in.
- Immutable, reviewable deploys. Every change goes through CI.
- Own the delivery path — a self-hosted edge means no mystery layer between the page and the user.
- A bind-mounted single FILE plus
rsyncgoes silently stale. rsync replaces the file via an atomic rename — a new inode — but the container stays pinned to the old inode.nginx -thappily validates the stale file and routing quietly goes wrong. Fix: recreate the container so it re-binds, or deploy withrsync --inplace. Directory mounts are fine; only single-file mounts pin the inode. (Cost me a real debugging cycle.) - Your own WAF will 403 your own probes. My home egress IP tripped the per-IP rate limit and
returned phantom 403s to my
curlchecks while the site was perfectly fine for everyone else. Now I verify from off-network. - A WAF blocks large uploads and WebDAV until you carve exceptions — disabling the rule per-location for the upload path and raising the body limit is what finally let big files through.
- Dec 3, 2024: Astro 5 ships the Content Layer API and Server Islands — Astro becomes the fastest-growing framework for content sites.
- Jan 2026: Astro 6 beta (workerd dev server, dev/prod parity) and Cloudflare acquires Astro (still MIT/open-source), tightening the static-to-edge story.