Building a Modern Blog with Astro and Tailwind CSS
Building a Modern Blog with Astro and Tailwind CSS
When I set out to build “Raw Thoughts,” I wanted a blog that was fast, developer-friendly, and easy to maintain. After evaluating various options, I settled on a modern stack that combines Astro for the framework and Tailwind CSS for styling. Here’s why this combination works so well and how I implemented it.
Why Astro?
Astro caught my attention for several compelling reasons:
Zero JavaScript by Default
Unlike traditional React or Vue applications, Astro ships zero JavaScript to the browser by default. This means lightning-fast page loads and excellent Core Web Vitals scores. For a blog where content is king, this performance boost is invaluable.
Content Collections API
Astro’s Content Collections provide a type-safe way to manage blog posts. Here’s how I’ve structured my content:
// src/content.config.ts
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
loader: glob({ base: "./src/content/blog", pattern: "**/*.{md,mdx}" }),
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: image().optional(),
}),
});
const tech = defineCollection({
loader: glob({ base: "./src/content/tech", pattern: "**/*.{md,mdx}" }),
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: image().optional(),
}),
});
export const collections = { blog, tech, journal };
This setup gives me three distinct content types - blog posts, tech articles, and journal entries - all with validated frontmatter.
File-Based Routing
Astro’s file-based routing makes site structure intuitive. My blog uses dynamic routes like [...slug].astro
to handle all post types:
---
// src/pages/tech/[...slug].astro
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('tech');
return posts.map((post) => ({
params: { slug: post.id },
props: post,
}));
}
---
Tailwind CSS: Utility-First Styling
Tailwind CSS proved perfect for this project because:
Rapid Development
Instead of writing custom CSS, I can style components directly in markup:
<header class="m-0 px-6 py-8 bg-gray-900">
<nav class="max-w-4xl mx-auto flex items-center">
<h2 class="m-0">
<a href="/" class="font-bold text-2xl no-underline text-white">
{SITE_TITLE}
</a>
</h2>
<div class="flex space-x-0 ml-auto px-0">
<HeaderLink href="/blog">blog</HeaderLink>
<HeaderLink href="/tech">tech</HeaderLink>
<HeaderLink href="/journal">journal</HeaderLink>
</div>
</nav>
</header>
Consistent Design System
Tailwind’s design tokens ensure consistency. I’ve extended the base configuration for custom typography:
/* src/styles/global.css */
.prose h1 {
@apply text-5xl font-bold text-gray-900 mb-2 leading-tight;
}
.prose h2 {
@apply text-4xl font-bold text-gray-900 mb-2 leading-tight;
}
.prose code {
@apply px-1 py-0.5 bg-gray-100 rounded text-sm;
}
.prose pre {
@apply p-6 bg-gray-800 text-white rounded-lg overflow-x-auto;
}
Custom Font Integration
I’m using the Atkinson font family for a unique look:
@font-face {
font-family: "Atkinson";
src: url("/fonts/atkinson-regular.woff") format("woff");
font-weight: 400;
font-style: normal;
font-display: swap;
}
body {
font-family: "Atkinson", sans-serif;
}
Architecture Decisions
Content Organization
The blog uses a clear content hierarchy:
/blog
- Personal thoughts and general topics/tech
- Technology articles and reviews/journal
- Daily reflections and diary entries
Layout System
I’ve built reusable layouts for different content types:
---
// src/layouts/BlogPost.astro
import BaseHead from '../components/BaseHead.astro';
import Header from '../components/Header.astro';
export interface Props {
content: {
title: string;
description: string;
pubDate?: string;
updatedDate?: string;
heroImage?: string;
};
}
const {
content: { title, description, pubDate, updatedDate, heroImage }
} = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<BaseHead title={title} description={description} />
</head>
<body>
<Header />
<main>
<article>
<h1>{title}</h1>
<slot />
</article>
</main>
</body>
</html>
Performance Optimizations
Image Optimization
Astro automatically optimizes images when using the built-in Image
component:
---
import { Image } from 'astro:assets';
---
{heroImage && (
<Image
src={heroImage}
alt={title}
width={1200}
height={630}
format="webp"
/>
)}
RSS and Sitemap Generation
The build process automatically generates RSS feeds and sitemaps:
// src/pages/rss.xml.js
import { getCollection } from "astro:content";
export async function GET() {
const posts = await getCollection("blog");
// RSS generation logic
}
Development Experience
The combination of Astro and Tailwind creates an excellent developer experience:
- Hot Reload: Changes reflect instantly during development
- Type Safety: Content collections provide full TypeScript support
- Build Performance: Astro’s static site generation is incredibly fast
- Deployment: Static files deploy anywhere (Vercel, Netlify, etc.)
Lessons Learned
What Works Well
- Content Collections: Type-safe content management is a game-changer
- Tailwind Utilities: Rapid styling without CSS overhead
- Static Generation: Excellent performance out of the box
- File Structure: Intuitive organization that scales well
Areas for Improvement
- CSS Bundle Size: Tailwind can generate large CSS files if not purged properly
- Learning Curve: Utility-first CSS requires mindset adjustment
- Component Reusability: Need to balance utility classes with component abstraction
Conclusion
Building “Raw Thoughts” with Astro and Tailwind CSS has been a rewarding experience. The stack delivers on its promises: fast performance, great developer experience, and maintainable code.
For anyone considering a similar setup, I’d recommend this combination for content-focused websites where performance and developer productivity are priorities. The learning investment pays dividends in both build speed and runtime performance.
The complete source code structure and implementation details are documented in the blog proposal if you want to dive deeper into the technical architecture.