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:

  1. Hot Reload: Changes reflect instantly during development
  2. Type Safety: Content collections provide full TypeScript support
  3. Build Performance: Astro’s static site generation is incredibly fast
  4. 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.