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 right srcset.
  • Markdown is first-class: write a page as .md with frontmatter, get layout, syntax highlighting (Shiki by default), and TOC for free.