Skip to main content
Illustration: a static page with three interactive islands highlighted

Astro has quickly become one of the most popular static site generators, and for good reason. Its island architecture renders static content as HTML while letting interactive features opt into client JavaScript where needed.

This guide walks through the parts of Astro you will touch in your first week: project setup, components, islands, content collections, routing, and deployment. It assumes you are comfortable with HTML, a bit of TypeScript, and the command line.

Setting Up Your First Project

Getting started with Astro is straightforward. Open your terminal and run the create command:

npm create astro@latest

The setup wizard will guide you through choosing a template, installing dependencies, and configuring TypeScript. For most projects, the “Blog” template provides an excellent starting point with content collections already configured.

Once it finishes, start the dev server:

cd my-site
npm run dev

The site is served on http://localhost:4321 with hot module reloading. The project layout is small and worth learning early:

  • src/pages/ holds file-based routes. src/pages/about.astro becomes /about.
  • src/components/ holds reusable components. Astro does not treat this folder specially; it is a convention.
  • src/layouts/ holds page shells, again by convention.
  • public/ holds files copied to the output untouched, such as favicon.svg or robots.txt.
  • astro.config.mjs holds integrations, adapters, and build options.

Understanding Astro Components

Astro components use a .astro file extension and combine HTML-like syntax with a frontmatter script section. Unlike React or Vue components, Astro components render to static HTML at build time unless you explicitly opt into client-side hydration.

The component frontmatter runs on the server at build time, giving you access to file imports, data fetching, and utility functions without shipping that logic to the browser.

---
interface Props {
  title: string;
  href?: string;
}

const { title, href = "/" } = Astro.props;
---

<article class="card">
  <h2><a href={href}>{title}</a></h2>
  <slot />
</article>

<style>
  .card {
    padding: 1.5rem;
    border-radius: 0.75rem;
  }
</style>

A few things in that example are worth noticing:

  • Props are typed with a plain Props interface. Editors and astro check pick it up with no extra configuration.
  • Slots work like they do in web components. A default <slot /> receives children, and named slots (<slot name="footer" />) receive elements marked with slot="footer".
  • Styles are scoped by default. The .card rule above only applies to this component. Use <style is:global> when you genuinely need a global rule.

If you have used JSX, the main adjustment is that there is no re-rendering. The template runs once per request or build, produces HTML, and is done.

Islands and Client Directives

Static HTML is the default, but you will eventually need something interactive: a search box, a chart, a cart. Astro lets you drop in components from React, Preact, Svelte, Vue, or Solid after adding the matching integration:

npx astro add react

A framework component rendered without a directive still outputs only HTML. To ship its JavaScript and hydrate it, add a client:* directive:

---
import Counter from "../components/Counter.tsx";
import Chart from "../components/Chart.tsx";
---

<Counter client:load />
<Chart client:visible />

Choose the directive by how urgent the interaction is:

  • client:load hydrates immediately. Use it for above-the-fold controls the user needs right away.
  • client:idle waits until the browser is idle. Good for lower-priority widgets.
  • client:visible waits until the component scrolls into view. Ideal for charts and embeds further down the page.
  • client:media="(max-width: 768px)" hydrates only when a media query matches, such as a mobile-only menu.
  • client:only="react" skips server rendering entirely, for components that depend on browser APIs.

Each island hydrates independently, so a slow widget at the bottom of the page does not block the rest of it. For many interactions you do not need a framework at all: a <script> tag inside an .astro component is bundled and deduplicated by Astro, which is often enough for toggles, menus, and copy buttons.

Content Collections

One of Astro’s standout features is content collections. They provide type-safe access to your Markdown and MDX content with schema validation. Define your content structure once, and Astro will ensure every piece of content conforms to your schema.

Collections are defined in src/content.config.ts. Since Astro 5, each collection uses a loader that tells Astro where the entries come from. The built-in glob() loader reads files from disk:

import { defineCollection } from "astro:content";
import { glob } from "astro/loaders";
import { z } from "astro/zod";

const blog = defineCollection({
  loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    publishDate: z.coerce.date(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
  }),
});

export const collections = { blog };

If a post is missing a title or has a malformed date, the build fails with a message pointing at the file. That is exactly what you want on a team: content errors surface in development, not in production.

Querying a collection is a single function call, and the results are fully typed:

---
import { getCollection } from "astro:content";

const posts = (await getCollection("blog", ({ data }) => !data.draft))
  .sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf());
---

<ul>
  {posts.map((post) => (
    <li><a href={`/blog/${post.id}/`}>{post.data.title}</a></li>
  ))}
</ul>

Loaders are not limited to local files. The file() loader reads a single JSON or YAML file, and you can write your own loader to pull from a headless CMS or an API, while keeping the same schema validation and query interface.

Routing and Dynamic Pages

Every file in src/pages/ is a route. For pages generated from data, such as one page per blog post, use a bracketed filename and export getStaticPaths():

---
// src/pages/blog/[id].astro
import { getCollection, render } from "astro:content";

export async function getStaticPaths() {
  const posts = await getCollection("blog");
  return posts.map((post) => ({ params: { id: post.id }, props: { post } }));
}

const { post } = Astro.props;
const { Content, headings } = await render(post);
---

<h1>{post.data.title}</h1>
<Content />

render() also returns the post’s headings, which is handy for building a table of contents without parsing anything yourself.

Deployment

Astro supports multiple deployment targets out of the box. Vercel, Netlify, Cloudflare Pages, and Node servers each have an official adapter that npx astro add installs and configures. For static sites, the built-in output requires no additional configuration.

Running npm run build writes a static site to dist/, which any static host or CDN can serve. When you need code that runs per request, such as form handling, authentication, or personalized pages, add an adapter for your host:

npx astro add node

With an adapter installed you can keep most pages prerendered and opt individual routes out with export const prerender = false. Before you deploy, set site in astro.config.mjs so canonical URLs, sitemaps, and RSS feeds point at your real domain.

Where to Go Next

Once the basics feel natural, a few features are worth exploring in roughly this order:

  1. Images. The <Image /> component from astro:assets handles resizing, modern formats, and width and height attributes that prevent layout shift.
  2. View transitions. Adding the <ClientRouter /> component to your layout gives animated client-side navigation with very little code.
  3. Actions. Astro Actions give you type-safe server functions with input validation, a clean fit for contact forms and newsletters.
  4. Server islands. The server:defer directive lets a mostly static page render one personalized fragment, like a user menu, on demand.

Start small: one layout, one collection, a couple of pages. Astro rewards that approach because every page you add stays as fast as the first.

Share this article
astro frontend performance