A minimal, fast, dark-mode-first engineering blog built with Astro 5, React, Tailwind CSS v4, and Pagefind. Clone it, edit one config file, and you have your own blog.
- Static output — fully pre-rendered, deployable to Vercel, Netlify, Cloudflare Pages, or any CDN
- Pagefind search — client-side full-text search, zero backend required
- MDX content — write posts in Markdown with JSX component support
- Blog + Series — standalone posts and ordered multi-part series, each with their own routing
- Reading progress bar — thin indicator at the top of every post
- Table of contents — sticky sidebar with active-section tracking via IntersectionObserver
- OG image generation — per-post Open Graph images generated at build time with Satori
- RSS feed — auto-generated from published posts
- Sitemap — auto-generated with customisable changefreq and priority
- Dark / light theme — persisted in localStorage, no flash on load
- Custom cursor — smooth cursor with spring lerp, respects
prefers-reduced-motion - Contact form — connects to any REST API via
PUBLIC_CONTACT_API_URLenv var - View Transitions — page-to-page transitions via Astro's built-in View Transitions API
Want to contribute? See the Contributing Guide and Coding Guidelines.
git clone https://github.com/your-username/astro-cody-blog.git
cd astro-cody-blog
npm install
cp .env.example .env.development
npm run devOpen http://localhost:4321.
All personal information lives in one file: src/config/site.ts
export const siteConfig = {
author: 'Your Name',
siteUrl: 'https://your-blog.example.com',
siteName: 'blog.yourname',
siteTitle: 'blog.yourname — Engineering Blog',
description: 'A blog about ...',
social: {
github: 'https://github.com/your-handle',
githubHandle: '@your-handle',
linkedin: 'https://www.linkedin.com/in/your-handle/',
twitter: '@yourhandle',
email: 'you@example.com',
website: 'https://yoursite.com/', // shown as "About" nav link; set to '' to hide
},
contact: {
timezone: 'UTC+0:00',
responseTime: '24-48h',
},
rss: {
title: 'Your Name — Blog',
description: 'Writing about ...',
},
};No other files need to change to personalise the blog.
Copy .env.example to .env.development (local) and .env.production (deployment):
PUBLIC_CONTACT_API_URL=https://your-api.example.com
The contact form POSTs to ${PUBLIC_CONTACT_API_URL}/v1/contact with { name, email, message, origin: 'blog' }. If the variable is not set, the form silently succeeds so the page still renders during development.
Create src/content/blog/<category>/<slug>.mdx:
---
title: 'Your Post Title'
date: '2026-01-15'
pubDate: 2026-01-15
category: Engineering
author: Your Name
summary: 'A one-line summary for cards and OG tags.'
featured: false
draft: false
tags: [typescript, performance]
relatedPosts: []
---
Your content here.- Create
src/content/series/<series-slug>/index.md— the series index:
---
title: 'Series Title'
description: 'What the series covers.'
category: Engineering
postOrder:
- part-1-slug
- part-2-slug
---- Create one MDX file per part:
src/content/series/<series-slug>/<part-slug>.mdx:
---
title: 'Part 1: Getting Started'
date: '2026-01-15'
pubDate: 2026-01-15
category: Engineering
summary: 'Part summary.'
tags: []
---Categories are defined in src/config/categories.ts. Add or remove entries there to change the filter tabs on the blog and series pages.
| Command | Action |
|---|---|
npm run dev |
Start dev server at localhost:4321 |
npm run build |
Build to ./dist/ and run Pagefind indexing |
npm run preview |
Preview the production build locally |
Push to GitHub and import the repo in Vercel. Add PUBLIC_CONTACT_API_URL in Project → Settings → Environment Variables.
Run npm run build — output is in dist/. The build also runs Pagefind to index the built HTML. The dist/ folder is self-contained and can be served from any static host.
src/
├── config/
│ ├── site.ts ← edit this to personalise
│ └── categories.ts ← blog/series category definitions
├── content/
│ ├── blog/ ← blog posts (MDX)
│ └── series/ ← series posts + index files (MDX/MD)
├── layouts/
│ ├── BaseLayout.astro ← <head>, meta tags, global scripts
│ ├── BlogLayout.astro ← post layout (TOC, prev/next, OG image)
│ └── PageLayout.astro ← general page layout (navbar, footer)
├── pages/
│ ├── index.astro
│ ├── blog/
│ ├── series/
│ ├── contact.astro
│ └── rss.xml.ts
├── components/
│ ├── astro/ ← Navbar, Footer, ThemeToggle
│ └── react/ ← interactive UI components
└── styles/
└── global.css ← Tailwind v4 + theme tokens + prose styles
| Layer | Technology |
|---|---|
| Framework | Astro 5 |
| UI components | React 19 |
| Styling | Tailwind CSS v4 |
| Content | MDX |
| Search | Pagefind |
| OG images | Satori + @resvg/resvg-js |
| Icons | Lucide React |
Contributions are welcome. Please read the Contributing Guide for the development workflow and pull request process, and the Coding Guidelines for code style, naming conventions, and architectural rules.
MIT