Building a High-Performance Static Blog with Astro
Software engineer at Angel, building tools that make development faster and more enjoyable.
Why We Chose Astro for Our Tech Blog
When we set out to build a new engineering blog at Angel, performance was non-negotiable. Every millisecond of load time matters, especially when your audience is developers who notice the difference. After evaluating several frameworks, we landed on Astro 6 for one compelling reason: it ships zero JavaScript to the browser by default.
That single architectural decision cascades into measurable wins across every performance metric. Our pages load in under one second on a 3G connection. Our Lighthouse scores hit 100 across the board. And our infrastructure costs dropped because static files are cheap to serve.
The Islands Architecture
Astro popularized the concept of islands architecture, where the vast majority of your page is static HTML and only small, isolated interactive components hydrate with JavaScript. This means a typical blog post page weighs less than 50KB total, with zero client-side JavaScript unless the reader interacts with a search widget or theme toggle.
Compare that to a traditional single-page application where even a simple blog post might ship 200KB or more of framework code before a single word of content renders.
Setting Up the Content Pipeline
Our content pipeline is built on Astro’s Content Collections, which provide type-safe frontmatter validation at build time. Every blog post is an MDX file with a Zod-validated schema, so we catch errors before they reach production.
Here is how we define the schema for our posts collection:
import { defineCollection, reference, z } from "astro:content";
import { glob } from "astro/loaders";
const posts = defineCollection({
loader: glob({ pattern: "**/*.mdx", base: "src/content/posts" }),
schema: z.object({
title: z.string(),
description: z.string().max(160),
date: z.coerce.date(),
author: reference("authors"),
tags: z.array(z.string()),
image: z.object({
src: z.string(),
alt: z.string(),
}),
draft: z.boolean().default(false),
}),
});
This schema ensures every post has a title, a description that fits within meta tag limits, a valid date, a reference to a real author entry, and an image with alt text for accessibility.
Type Safety from Content to Template
One of the most powerful aspects of this approach is end-to-end type safety. When we query the collection in our Astro pages, TypeScript knows the exact shape of every field. There are no runtime surprises, no missing fields, and no type coercion bugs.
The build fails fast if a post has invalid frontmatter, which means our CI pipeline catches content errors just like it catches code errors.
Performance Optimizations
Static Generation
Every page is pre-rendered at build time into plain HTML files. There is no server-side rendering, no edge functions, and no serverless compute. We serve the output from nginx with aggressive caching headers and gzip compression. The result is sub-100ms time-to-first-byte from our CDN edge nodes.
Image Optimization
Astro’s built-in <Image> component handles responsive image generation automatically. It produces multiple sizes in modern formats like AVIF and WebP, with appropriate srcset attributes so the browser downloads only what it needs for the current viewport.
CSS Strategy
We use Tailwind CSS 4 for styling, which means our CSS is automatically purged of unused utilities at build time. A typical page ships less than 10KB of CSS. Combined with the typography plugin for prose content, we get beautiful, readable articles without writing custom CSS for each post.
Accessibility First
Performance without accessibility is incomplete. Every page on our blog includes skip navigation links, proper ARIA landmarks through semantic HTML, and visible focus indicators for keyboard navigation. We enforce alt text on every image through both our content schema and a custom remark plugin that fails the build if an image lacks a description.
Color Contrast
Our design tokens are tested against WCAG 2.2 AA standards, ensuring a minimum 4.5:1 contrast ratio for body text and 3:1 for large text. Both our light and dark themes pass these requirements.
Reduced Motion
We respect the prefers-reduced-motion media query throughout the site. Animations and transitions are disabled for users who prefer reduced motion, ensuring the reading experience is comfortable for everyone.
Deployment Architecture
Our blog runs in a Docker container on AWS EKS, managed by ArgoCD for GitOps-style deployments. The Docker image is a two-stage build: Node.js builds the static files, then nginx serves them. The production image contains no Node.js runtime, just nginx and HTML files.
// Example: build verification script
interface BuildMetrics {
totalPages: number;
totalAssetSize: number;
buildDuration: number;
lighthouseScores: {
performance: number;
accessibility: number;
bestPractices: number;
seo: number;
};
}
function validateBuild(metrics: BuildMetrics): boolean {
const { lighthouseScores } = metrics;
return (
lighthouseScores.performance >= 95 &&
lighthouseScores.accessibility === 100 &&
lighthouseScores.bestPractices === 100 &&
lighthouseScores.seo === 100
);
}
This architecture means deployments are fast, rollbacks are instant, and our serving infrastructure is as simple as it gets.
What We Learned
Building with Astro taught us that the best JavaScript is no JavaScript. By defaulting to static HTML and only adding interactivity where it genuinely improves the user experience, we ended up with a blog that is faster, more accessible, and easier to maintain than anything we have built before.
The key takeaway: start with the simplest possible architecture and add complexity only when you have a clear reason. For a content-heavy site like a blog, static generation with islands for interactivity is hard to beat.