sdlcnext.com
← All tutorials
Astro Tailwind CSS Static Sites

Build a Blog with Astro and Tailwind CSS

A step-by-step guide to building a fast, content-focused blog using Astro 4.x, Tailwind CSS, and Markdown. Deploy to any static host.


Scaffold the project

Start with the official Astro CLI. It sets up the project structure, TypeScript config, and a minimal astro.config.mjs in one command.

npm create astro@latest my-blog -- --template minimal --typescript strict --install --git
cd my-blog

The minimal template gives you a clean slate: no example pages, no extra integrations. That is exactly what you want — you will add only what the blog needs.

Verify the dev server starts:

npm run dev

Open http://localhost:4321. You should see a plain “Hello, Astro!” page.


Configure Tailwind

Add the official Astro Tailwind integration:

npx astro add tailwind

This installs @astrojs/tailwind and tailwindcss, creates tailwind.config.mjs, and updates astro.config.mjs automatically.

Open tailwind.config.mjs and add the typography plugin for prose styling in blog posts:

import typography from '@tailwindcss/typography';

/** @type {import('tailwindcss').Config} */
export default {
  content: ['./src/**/*.{astro,html,js,jsx,md,mdx,ts,tsx}'],
  theme: { extend: {} },
  plugins: [typography],
};

Install the plugin:

npm install -D @tailwindcss/typography

Create src/styles/global.css with base styles and CSS custom properties:

@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --bg: #0a0a0f;
  --text: #f1f1f4;
  --accent: #6366f1;
}

body {
  background: var(--bg);
  color: var(--text);
}

Import it in src/layouts/BaseLayout.astro (you will create this next).


Define the content schema

Astro’s Content Collections give you type-safe frontmatter. Create src/content/config.ts:

import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().optional().default(false),
  }),
});

export const collections = { blog };

Zod validates every post’s frontmatter at build time — a typo in a date or a missing title becomes a build error, not a runtime surprise.

Create your first post at src/content/blog/hello-world.md:

---
title: "Hello, World"
description: "The first post on my new Astro blog."
pubDate: 2026-03-20
tags: ["meta"]
---

This is the first post. More to come.

Build the blog index

Create src/pages/blog/index.astro:

---
import { getCollection } from 'astro:content';

const posts = (await getCollection('blog', ({ data }) => !data.draft))
  .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---

<html lang="en">
  <head><title>Blog</title></head>
  <body>
    <h1>Blog</h1>
    <ul>
      {posts.map((post) => (
        <li>
          <a href={`/blog/${post.slug}`}>{post.data.title}</a>
          <time>{post.data.pubDate.toLocaleDateString()}</time>
        </li>
      ))}
    </ul>
  </body>
</html>

getCollection returns all entries passing the optional filter. Sorting by pubDate.valueOf() (milliseconds) puts the newest post first.


Build the post page

Create src/pages/blog/[slug].astro:

---
import { getCollection } from 'astro:content';

export async function getStaticPaths() {
  const posts = await getCollection('blog', ({ data }) => !data.draft);
  return posts.map((post) => ({
    params: { slug: post.slug },
    props: { post },
  }));
}

const { post } = Astro.props;
const { title, description, pubDate, tags } = post.data;
const { Content } = await post.render();
---

<html lang="en">
  <head><title>{title}</title></head>
  <body>
    <a href="/blog">← All posts</a>
    <h1>{title}</h1>
    <p>{description}</p>
    <time>{pubDate.toLocaleDateString()}</time>
    <article class="prose prose-invert prose-lg max-w-none">
      <Content />
    </article>
  </body>
</html>

getStaticPaths tells Astro which slugs to generate at build time. post.render() compiles the Markdown to a component you drop in anywhere.

Run npm run build and verify every post page is generated with no errors.


Deploy

Build the static output:

npm run build

Astro writes everything to dist/. Upload that folder to any static host.

AWS S3 + CloudFront (the pattern used on this site):

aws s3 sync dist/ s3://your-bucket-name --delete
aws cloudfront create-invalidation --distribution-id YOUR_DIST_ID --paths "/*"

Netlify / Vercel: connect your GitHub repo and set the build command to npm run build with publish directory dist. Both platforms detect Astro automatically.

You now have a fully static, type-safe blog. Add posts by dropping Markdown files into src/content/blog/ — the index and all post pages update on the next build.

Comments

Loading comments…

Leave a comment