Install
npm create astro@latest my-site
# — or with Bun (see /tutorials/bun-nodejs-replacement.html) —
bun create astro@latest my-site
cd my-site
npm run dev # or: bun dev
The setup wizard prompts for: project template (empty, blog, portfolio, minimal), TypeScript strictness, and whether to install dependencies. The result is a working project with a dev server on http://localhost:4321.
Project structure
my-site/
src/
pages/ <-- file-based routing
index.astro
about.astro
blog/
[slug].astro <-- dynamic route
components/ <-- reusable Astro/React/Vue/Svelte components
layouts/ <-- page shells
content/ <-- content collections (Markdown / MDX / JSON / YAML)
public/ <-- copied as-is to the output (favicons, fonts)
astro.config.mjs
tsconfig.json
Routing is file-based: every .astro or .md file under src/pages/ becomes a route at its path. [slug].astro is a dynamic route; it exports getStaticPaths() to list which slugs to pre-render.
An Astro component
---
// src/pages/index.astro
// Frontmatter: runs at build time, server-side
const title = "Hello Astro";
const posts = await Astro.glob("./blog/*.md");
---
<html lang="en">
<head>
<title>{title}</title>
</head>
<body>
<h1>{title}</h1>
<ul>
{posts.map((p) => (
<li><a href={p.url}>{p.frontmatter.title}</a></li>
))}
</ul>
</body>
</html>
The --- fences are the component's frontmatter: arbitrary TypeScript that runs once at build time. The template below is JSX-like (curly braces for expressions, but plain HTML attributes everywhere). The output is static HTML with no JavaScript runtime attached.
Islands: when JS does ship
The interactive parts of a page are "islands" — components from a framework (React/Vue/Svelte/Solid/Preact) embedded in static HTML. Each island declares when its JS should load:
---
import Counter from '../components/Counter.jsx';
---
<Counter client:load /> <!-- hydrate on page load -->
<Counter client:idle /> <!-- hydrate when the main thread is idle -->
<Counter client:visible /> <!-- hydrate when the component scrolls into view -->
<Counter client:media="(max-width: 50em)" /> <!-- hydrate only on small screens -->
<Counter client:only="react" /> <!-- skip server render, render client-side only -->
Pages without client:* components ship zero JavaScript. Pages with one ship that island's framework code and only that island's tree — nothing else on the page is hydrated.
Install a framework integration
npx astro add react # or vue, svelte, solid, preact, lit
npx astro add tailwind
npx astro add mdx
npx astro add sitemap
npx astro add image # built-in by default in 5.x
Each integration updates astro.config.mjs and adds the necessary dependencies.
Content collections
Markdown / MDX / JSON / YAML go under src/content/<collection-name>/. Define the schema once with Zod:
// src/content/config.ts
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
published: z.coerce.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
Then in a page:
---
import { getCollection } from "astro:content";
const posts = (await getCollection("blog"))
.filter((p) => !p.data.draft)
.sort((a, b) => b.data.published.valueOf() - a.data.published.valueOf());
---
Frontmatter is type-checked against the schema at build time — a missing field or wrong type fails the build instead of silently rendering "undefined" somewhere.
Build for production
npm run build
# Output lands in dist/ — static HTML, CSS, hashed asset URLs
For a fully static site, that's everything — upload dist/ anywhere that serves static files (Cloudflare Pages, Netlify, GitHub Pages, Caddy file_server, S3 + CloudFront, etc.).
For server-side rendering (e.g. to handle form submissions, API routes, or dynamic per-request data), add an adapter:
npx astro add node # Node.js adapter (Express-style server)
npx astro add cloudflare # Cloudflare Workers
npx astro add deno # Deno Deploy
npx astro add netlify # Netlify Functions
npx astro add vercel # Vercel
Then set output: "server" (full SSR) or output: "hybrid" (default static, opt-in SSR per page) in astro.config.mjs.
When to pick Astro
Astro is the right tool when:
- The site is content-heavy — blog, docs, marketing, portfolio — with islands of interactivity (a search bar, a comment widget, a contact form).
- First paint and Core Web Vitals matter.
- You want to use React/Vue/Svelte for the interactive parts but don't want to ship 60–200 KB of JS to a page that has none.
- Content collections with typed frontmatter sound useful.
It's not the right tool for: rich SPAs where every page is interactive (Next.js / Remix / SvelteKit); pure-Markdown documentation that doesn't need any JS at all (Hugo / Zola / Eleventy build faster); or apps with heavy server-side state (a real backend framework).
Worth knowing
- View Transitions are built in (
<ViewTransitions />in the layout): client-side navigation between Astro pages that animates the diff, while keeping the multi-page-app model underneath. - Image optimization is built-in:
<Image src={...} />generates responsive sources, modern formats (AVIF/WebP), and the rightsrcset. - Markdown is first-class: write a page as
.mdwith frontmatter, get layout, syntax highlighting (Shiki by default), and TOC for free.