First Site
Build a content site with layouts, pages, components, and a content collection. This walks through a practical example, not a toy hello-world.
1. Create the project
npm create astro@latest -- --template minimal my-site
cd my-site
npx astro add mdx tailwind
npm run dev
2. Create a layout
Layouts are regular Astro components that wrap pages:
---
// src/layouts/Base.astro
interface Props {
title: string;
description?: string;
}
const { title, description = 'My Astro site' } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<title>{title}</title>
</head>
<body class="min-h-screen bg-white text-gray-900">
<header>
<nav>
<a href="/">Home</a>
<a href="/blog">Blog</a>
<a href="/about">About</a>
</nav>
</header>
<main>
<slot />
</main>
<footer>
<p>© {new Date().getFullYear()} My Site</p>
</footer>
</body>
</html>
3. Create pages
---
// src/pages/index.astro
import Base from '../layouts/Base.astro';
---
<Base title="Home">
<h1>Welcome</h1>
<p>This is an Astro site.</p>
</Base>
---
// src/pages/about.astro
import Base from '../layouts/Base.astro';
---
<Base title="About" description="About this site">
<h1>About</h1>
<p>Built with Astro.</p>
</Base>
4. Set up a content collection
Define the schema:
// 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.date(),
tags: z.array(z.string()).default([]),
}),
});
export const collections = { blog };
Create content files:
---
# src/content/blog/hello-world.md
title: "Hello World"
description: "My first blog post"
pubDate: 2026-04-18
tags: ["intro"]
---
This is my first blog post built with Astro.
## Why Astro?
It ships zero JavaScript by default.
5. Create collection pages
Blog index:
---
// src/pages/blog/index.astro
import Base from '../../layouts/Base.astro';
import { getCollection } from 'astro:content';
const posts = (await getCollection('blog'))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---
<Base title="Blog">
<h1>Blog</h1>
<ul>
{posts.map(post => (
<li>
<a href={`/blog/${post.id}`}>
<h2>{post.data.title}</h2>
<time>{post.data.pubDate.toLocaleDateString()}</time>
<p>{post.data.description}</p>
</a>
</li>
))}
</ul>
</Base>
Individual post page:
---
// src/pages/blog/[slug].astro
import Base from '../../layouts/Base.astro';
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<Base title={post.data.title} description={post.data.description}>
<article>
<h1>{post.data.title}</h1>
<time>{post.data.pubDate.toLocaleDateString()}</time>
<div class="prose">
<Content />
</div>
</article>
</Base>
6. Add a reusable component
---
// src/components/TagList.astro
interface Props {
tags: string[];
}
const { tags } = Astro.props;
---
<ul class="flex gap-2">
{tags.map(tag => (
<li class="px-2 py-1 bg-gray-100 rounded text-sm">{tag}</li>
))}
</ul>
Use it in the blog post page:
import TagList from '../../components/TagList.astro';
<!-- inside the article -->
<TagList tags={post.data.tags} />
7. Add an interactive island
For a theme toggle that needs client-side JS:
// src/components/ThemeToggle.jsx
import { useState, useEffect } from 'react';
export default function ThemeToggle() {
const [dark, setDark] = useState(false);
useEffect(() => {
document.documentElement.classList.toggle('dark', dark);
}, [dark]);
return (
<button onClick={() => setDark(!dark)}>
{dark ? '☀️' : '🌙'}
</button>
);
}
Add it to the layout as a client island:
---
import ThemeToggle from '../components/ThemeToggle.jsx';
---
<nav>
<a href="/">Home</a>
<ThemeToggle client:load />
</nav>
Only this one component ships JavaScript. Everything else is static HTML.
8. Build
npm run build
Output goes to dist/. Every page is a static HTML file. The theme toggle island has a tiny JS bundle. Everything else is zero JS.