Static site generation, no database, no PHP, no CMS. The blog is markdown files in a Git repository, built locally, rsynced to a small VPS, served by Caddy. Faster than any WordPress, immune to the WordPress vulnerability class, and the archive is plain text on disk.

Astro 6 + Caddy: The Static-First AI Blog Stack

The sovgrid blog is markdown files in a Git repository, built by Astro 6.3.7 into static HTML, rsynced to a small VPS, and served by Caddy. No database, no CMS, no PHP, no runtime application server. The pages load fast, the archive survives any single point of failure, and the deployment pipeline is short enough to fit in a single shell script.

Quick Take

  • The stack: Astro 6.3.7 (static-site generator), markdown source files, MDX for interactive components, Caddy as the static-file server, rsync as the deploy mechanism, GitHub-style git as the source-of-truth, no database anywhere in the production path.
  • Why static first: the AI blog has content that needs to survive ten years of operator changes, server changes, and dependency drift. Markdown files in a Git repo survive all of that.
  • Why Astro 6.3.7 specifically: islands architecture for the few places that need interactivity, file-based routing, MDX support, built-in RSS, sitemap, and SEO defaults. Newer than Hugo, less opinionated than Next.js.
  • The build/deploy time: roughly 30 seconds for a full rebuild of the ~90-article corpus on a local laptop, plus another 30 seconds for rsync to the VPS.
  • The cost: Astro’s learning curve is small but non-zero. The Hugo or Eleventy alternatives produce similar output with simpler tooling for operators who do not need MDX components.

Why static first

The blog has content that needs to survive ten years of changes. WordPress at year three is a maintenance nightmare. Static sites at year ten are still readable.

The threat model for a long-running publishing operation includes: the CMS vendor goes out of business; the database engine version is no longer supported; the PHP runtime has unpatched vulnerabilities; the operator forgets the admin credentials. Static sites face none of these threats because there is no runtime application beyond a file server. That is why the sovgrid stack has no database in the production path.

The recovery model is also simple. The Git repository is the source of truth. If the production VPS dies, a fresh VPS, a git clone, an npm run build, and a Caddy install are enough to reconstitute the site within an hour. No database restore, no CMS reinstall, no plugin compatibility check.

The performance model is the cheapest possible: the file server reads the file and sends it. No template rendering, no database query, no plugin chain. Sub-100-millisecond response times across the corpus, including in the cold-cache case.

For an AI blog specifically, static-first matters for a second reason: the content itself is generated by a model that may be replaced. The text lives in plain markdown, which means any future operator, tool, or pipeline can read, audit, and regenerate it. A database-backed CMS adds a second layer that can drift out of sync with the model generation pipeline. This prevents that class of divergence.

Caveat: static-first breaks auth flows. If you need login-gated content, paywalled articles, or per-user personalization, a static site cannot deliver that at the file-server level. You need a backend, a JWT layer, or an edge function. The sovgrid blog has no paywalled content, which is why this limitation does not apply here. For operators who do need it, Next.js with a serverless backend is the more honest choice.

Caveat: real-time content does not fit. A live inference dashboard, a streaming token counter, or a chat interface cannot live as static HTML. Those belong as islands (client-side JS components) or as separate services. The static site can link to them; it cannot host them natively.

Astro 6.3.7 specifically

Astro is the static-site generator that handles content-heavy sites well in 2026. The competition includes Hugo (faster builds, simpler templating, mature), Eleventy (lighter, JavaScript-based, flexible), and Next.js in SSG mode (heavier, more JavaScript-runtime, more dynamic features). Each is reasonable; Astro fits the sovgrid use case best for three specific reasons.

MDX support. Most blog posts are pure markdown, but a few need interactive components: a small calculator, a model-throughput visualizer, a Lightning-tip widget. MDX (markdown with JSX components inline) handles these cleanly. Hugo’s shortcodes are an alternative but feel awkward; Eleventy’s component story is similar to Astro’s but less integrated.

Islands architecture. Astro renders to static HTML by default and adds JavaScript only for the components that explicitly need it (an “island” of interactivity in a static page). The default is fast static HTML; the exceptions are explicit. This matches the sovgrid posture of “static unless there is a reason for it not to be.”

File-based routing. Pages live in src/pages/, blog posts in src/content/blog/. The URL structure mirrors the file structure. No router configuration to maintain.

The cost: Astro’s API has changed across major versions, and the build configuration has some opinionated defaults that take a few hours to learn. Worth it for the sovgrid scale; debatable for a smaller site.

Why Astro rather than Hugo or Eleventy. Hugo builds faster (sub-second for 120 articles compared to Astro’s 30 seconds) and has zero Node.js dependency, which is why it wins on server-side simplicity. The sovgrid stack chose Astro instead because the MDX component story is native, the TypeScript integration is first-class, and the content collection schema validation catches frontmatter errors at build time rather than silently at runtime. For a pure writing blog with no interactive components, Hugo is the leaner pick. For a blog that embeds model visualizers and inference widgets, Astro is the better fit.

Why Caddy rather than nginx for the static server. nginx serves static files faster at extreme concurrency, but it requires a separate process (certbot or acme.sh) for TLS renewal, manual reload after certificate rotation, and a non-trivial config file format. Caddy handles ACME certificate issuance and renewal internally, reloads without downtime, and the Caddyfile for this use case is 10 lines. That is why Caddy is the right choice for a single-operator VPS: the TLS maintenance surface drops to zero.

Migrating from Astro 5 to Astro 6

The sovgrid blog migrated from Astro 5 to Astro 6.3.7 in roughly 30 minutes. The three breaking changes that actually required code edits:

Content collection loader syntax. Astro 6 requires the explicit loader: glob() call in defineCollection. The old implicit directory scan is gone. In src/content/config.ts, the collection definition changed from:

// Astro 5 : implicit scan, no loader required
const blog = defineCollection({ schema: blogSchema });

to:

// Astro 6 : explicit loader
import { glob } from 'astro/loaders';
const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
  schema: blogSchema,
});

render() call shape. In Astro 5, rendering a content entry used entry.render() (method on the entry object). In Astro 6, it moved to a top-level import: import { render } from 'astro:content' and then const { Content } = await render(entry). This change touched 8 files in the sovgrid codebase, specifically every page template that rendered markdown body content.

entry.slug renamed to entry.id. The slug field was removed from the content entry type; entry.id is the canonical identifier now. It contains the filename without the .md extension, which is the same value entry.slug held before. A grep-and-replace across the codebase found all 8 occurrences; the migration was mechanical.

The Astro 5-to-6 migration guide (astro.build/docs) covers all three. The actual work was reading the guide once, running npm run build, fixing the 3 error types the compiler surfaced, rebuilding clean.

Caveat: Astro 6 vs Next.js 15 for content-heavy sites. Next.js 15 in SSG mode produces equivalent static output and has a larger ecosystem of third-party components. The tradeoff is bundle size: a Next.js blog page typically ships 60-120 KB of JavaScript even with no interactive content, because the Next.js runtime ships with the page. Astro ships 0 KB of JavaScript for a pure markdown page. For a blog where page weight and cold-load performance matter, that difference is concrete. For a team already on React, Next.js is the more practical choice because the component knowledge transfers.

The repository layout

sovereign-blog/
├── src/
│   ├── content/
│   │   ├── blog/        # The markdown corpus
│   │   └── pages/
│   ├── components/      # MDX-usable components
│   ├── layouts/         # Page layouts
│   └── pages/           # Routes
├── public/              # Static assets (images, fonts)
├── astro.config.mjs     # Build configuration
├── package.json
└── scripts/
    ├── deploy.sh        # Production deployment
    ├── factcheck.py     # Quality gate
    └── rescore_local.py # Quality scoring

The content directory is the single most important. Every blog post is a markdown file with YAML frontmatter at the top. The frontmatter holds the metadata (title, date, tags, status, quality score). The body is the article.

The build reads the content directory, applies layouts, generates the navigation, and outputs to dist/. The dist/ directory is what gets rsynced to the VPS.

The deploy script

#!/bin/bash
set -euo pipefail

# Pre-build quality gates
python3 scripts/preflight.sh
python3 scripts/factcheck.py --all

# Build
npm run build

# Verify the build
test -f dist/index.html
test -d dist/blog
PAGES=$(find dist -name "*.html" | wc -l)
if [ "$PAGES" -lt 80 ]; then
    echo "ERROR: only $PAGES pages built, expected ≥80"
    exit 1
fi

# Deploy
rsync -av --delete dist/ floki:/srv/sovgrid/

# Live-check
sleep 5
curl -sS -o /dev/null -w "%{http_code}\n" https://sovgrid.org/ | grep -q 200 \
    || (echo "ERROR: live site returns non-200"; exit 1)

# Self-heal phase
python3 scripts/factcheck.py --all
echo "deploy complete at $(date +%F\ %T)"

The script has three phases. The pre-build phase runs the quality gates (factcheck verifies every Docker image and PyPI version is reachable on its upstream registry, see Fixes: Self-Healing Pipeline Gaps). The build phase produces the static output and verifies the page count. The deploy phase rsyncs to the VPS and confirms the live site is responsive.

The self-heal phase at the end runs the quality gates against the live site again, in case anything drifted during deploy. For the broader self-heal pattern, see Strategy: Self-Hosted AI Electricity Cost and Solar (which uses the same pattern for adjacent operations).

The VPS side

Floki VPS runs Caddy and serves the static files from /srv/sovgrid/. The Caddyfile is short:

sovgrid.org, www.sovgrid.org {
    root * /srv/sovgrid
    encode gzip zstd
    file_server

    redir https://sovgrid.org{uri} permanent

    log {
        output file /var/log/caddy/sovgrid-access.log
        format json
    }
}

The redir line forces canonical URLs (no www, always HTTPS). The encode line applies gzip and zstd compression where the client supports it. The file_server is Caddy’s default static-file handler.

Caddy handles TLS automatically via ACME (Let’s Encrypt by default; the Cloudflare DNS plugin if the site sits behind Cloudflare). The TLS certificate renews automatically every 60 days; the operator does not have to think about it.

For the broader Floki VPS setup, see Setup: Floki VPS Setup. For the Caddy + Cloudflare integration, see Caddy + Cloudflare Tunnel: The Reliability Pattern.

What this stack does not give you

Comments. No comment system. Static sites do not support comments without an external service (Disqus, Commento, or a custom backend). Sovgrid has no comments; readers reach me via Nostr or email.

Real-time content. Static sites rebuild on deploy. Content that needs to update minute-by-minute (a live dashboard, a real-time metric) belongs on a separate dynamic page or as an island within the static page.

Admin UI. No CMS dashboard. Writing a post means editing a markdown file. This is faster for technical operators and slower for non-technical ones. Pick the stack that matches the operator’s habits.

Image processing. Astro 6 has built-in image optimization but it is conservative. For a heavy image pipeline (the sovgrid hero-image generation via FLUX.1-schnell), the processing happens outside Astro (see Setup: ComfyUI FLUX Setup).

Where this fits

For the reverse-proxy layer in front of Caddy, see Caddy + Cloudflare Tunnel: The Reliability Pattern. For the deploy quality-gate context, see Strategy: Content Quality Manifest Evaluation. For the broader publishing-stack reasoning, see Two Days from Localhost to Production and the multi-part Setup: Sovereign Blog Setup Part 1 and Part 2.

Follow the build-pipeline deep dive

The follow-up article walks through the factcheck.py and rescore_local.py quality-gate scripts in detail, including the rules they enforce and the rationale for each. Follow cipherfox@sovgrid.org on Nostr or subscribe to the RSS feed at /rss.xml for updates.