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