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>&copy; {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.