Skip to main content
Technology & EngineeringAstro252 lines

Astro Basics

Astro fundamentals including project structure, components, islands architecture, and templating syntax

Quick Summary31 lines
You are an expert in Astro fundamentals for building fast, content-focused websites with Astro.

## Key Points

- Default to `.astro` components for static content; only reach for React/Vue/Svelte when you need client-side interactivity.
- Use `client:visible` or `client:idle` over `client:load` to defer hydration and improve initial page load.
- Keep frontmatter logic minimal; extract complex data fetching into utility functions under `src/lib/` or `src/utils/`.
- Use TypeScript for component props via the `Props` interface to catch errors at build time.
- Leverage scoped styles in `.astro` files instead of CSS-in-JS to avoid shipping extra JavaScript.
- **Forgetting client directives**: A React/Vue/Svelte component without a `client:*` directive renders as static HTML with no interactivity. This is intentional but surprises newcomers.
- **Using `client:only` without specifying the framework**: `client:only` requires the framework name (e.g., `client:only="react"`), otherwise Astro does not know which renderer to use.
- **Assuming frontmatter runs on the client**: All code in the `---` fence runs on the server. You cannot access `window`, `document`, or browser APIs there.
- **Importing npm packages that rely on browser globals**: Some packages reference `window` at import time. Use dynamic imports inside `<script>` tags or `client:only` components to work around this.

## Quick Example

```astro
<Layout title="Home">
  <h1 slot="header">Welcome</h1>
  <p>This goes into the default slot.</p>
  <p slot="footer">Copyright 2026</p>
</Layout>
```

```astro
<style is:global>
  body { margin: 0; }
</style>
```
skilldb get astro-skills/Astro BasicsFull skill: 252 lines
Paste into your CLAUDE.md or agent config

Astro Basics — Astro

You are an expert in Astro fundamentals for building fast, content-focused websites with Astro.

Overview

Astro is a web framework designed for content-rich websites that ships zero JavaScript by default. It uses an islands architecture where interactive components hydrate independently, keeping pages fast. Astro components use a .astro file format with a frontmatter script fence and an HTML-like template below it.

Core Concepts

Project Structure

A standard Astro project follows this layout:

src/
  components/    # Astro and UI framework components
  layouts/       # Page layout templates
  pages/         # File-based routing (each file becomes a route)
  content/       # Content collections (Markdown, MDX, JSON)
  styles/        # Global stylesheets
public/          # Static assets served as-is
astro.config.mjs # Astro configuration

Astro Component Anatomy

Every .astro file has two parts separated by a code fence (---):

---
// Component Script (runs at build time on the server)
import Header from '../components/Header.astro';

const title = "My Page";
const items = await fetch('https://api.example.com/items').then(r => r.json());
---

<!-- Component Template (HTML output) -->
<html>
  <head><title>{title}</title></head>
  <body>
    <Header />
    <ul>
      {items.map(item => <li>{item.name}</li>)}
    </ul>
  </body>
</html>

The frontmatter script runs only on the server (at build time in static mode, or on each request in SSR mode). It never ships to the client.

Islands Architecture

Astro's islands architecture lets you embed interactive UI framework components (React, Vue, Svelte, etc.) inside otherwise static HTML pages. Each island hydrates independently.

---
import StaticCard from '../components/Card.astro';
import ReactCounter from '../components/Counter.jsx';
---

<!-- This is static HTML, zero JS -->
<StaticCard title="Hello" />

<!-- This island hydrates on the client -->
<ReactCounter client:load />

Client Directives

Client directives control when and how islands hydrate:

DirectiveBehavior
client:loadHydrate immediately on page load
client:idleHydrate once the browser is idle
client:visibleHydrate when the component scrolls into view
client:media="(max-width: 768px)"Hydrate when a CSS media query matches
client:only="react"Skip server rendering, only render on client

Expressions and Templating

Astro templates support JavaScript expressions inside curly braces:

---
const visible = true;
const classes = ['card', 'featured'];
---

{visible && <p>Conditionally rendered</p>}

<div class:list={['base', { active: visible }, classes]}>
  Dynamic classes
</div>

<ul>
  {['A', 'B', 'C'].map(letter => <li>{letter}</li>)}
</ul>

Slots

Astro components accept children via slots, similar to web component slots:

---
// Layout.astro
const { title } = Astro.props;
---
<html>
  <head><title>{title}</title></head>
  <body>
    <header><slot name="header" /></header>
    <main><slot /></main>
    <footer><slot name="footer" /></footer>
  </body>
</html>

Usage:

<Layout title="Home">
  <h1 slot="header">Welcome</h1>
  <p>This goes into the default slot.</p>
  <p slot="footer">Copyright 2026</p>
</Layout>

Props and TypeScript

---
interface Props {
  title: string;
  description?: string;
  tags: string[];
}

const { title, description = 'Default description', tags } = Astro.props;
---

<article>
  <h2>{title}</h2>
  <p>{description}</p>
  <ul>{tags.map(t => <li>{t}</li>)}</ul>
</article>

Implementation Patterns

Fetching Data at Build Time

---
// Runs at build time (static) or per-request (SSR)
const response = await fetch('https://api.example.com/posts');
const posts = await response.json();
---

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

Scoped Styles

Styles in .astro files are scoped to that component by default:

<style>
  /* Only applies to <h1> elements in this component */
  h1 {
    color: navy;
    font-size: 2rem;
  }
</style>

<h1>Scoped heading</h1>

Use is:global to opt out of scoping:

<style is:global>
  body { margin: 0; }
</style>

Script Tags

<!-- Bundled and deduplicated by Astro -->
<script>
  document.querySelector('#btn').addEventListener('click', () => {
    console.log('clicked');
  });
</script>

<!-- Inline, not processed -->
<script is:inline>
  console.log('runs exactly as written');
</script>

Core Philosophy

Astro exists to prove that most of the web does not need JavaScript. The framework embraces a content-first mentality where pages are static HTML by default, and interactivity is an opt-in exception rather than a baseline assumption. This inversion of the typical SPA model means your starting point is always zero client-side JavaScript, and you consciously add only what users need.

The islands architecture is not just a performance trick but a design philosophy. Each interactive component is an isolated, self-contained unit that hydrates independently. This forces you to think about interactivity as discrete, purposeful decisions rather than a blanket capability. The result is a site where the structure of the code mirrors the structure of the user experience: mostly content, with pockets of interaction.

Astro's component model leans into the web platform itself. The .astro file format uses a thin abstraction over HTML, CSS, and JavaScript rather than inventing a new paradigm. Scoped styles, slots, and JSX-like expressions map directly to web concepts. When you reach for a framework island, you are explicitly leaving Astro's lightweight layer and accepting the cost of a heavier runtime. This transparency about trade-offs is central to writing good Astro code.

Anti-Patterns

  • Wrapping entire pages in framework components. If you nest your whole page inside a React or Vue root just to use familiar tooling, you negate Astro's zero-JS default and turn it into a heavier, slower SPA with extra build complexity.

  • Reaching for client:load on every island. Eagerly hydrating all interactive components defeats the purpose of partial hydration. Evaluate whether client:visible or client:idle would suffice, reserving client:load for above-the-fold elements that require immediate interactivity.

  • Using global state managers across islands. Importing Redux, Zustand, or a heavyweight store to share state between islands introduces tight coupling and large bundles. Use nanostores or browser-native mechanisms (events, localStorage) to keep islands truly independent.

  • Embedding heavy logic in frontmatter. The code fence is for data fetching and light preparation, not business logic. When frontmatter grows beyond a handful of statements, extract it into utility modules under src/lib/ so components stay readable and testable.

  • Treating .astro files like JSX components. Astro components are not React components. They have no lifecycle, no state, and no re-renders. Writing them as if they had client-side behavior (attaching event handlers inline, expecting reactivity) leads to confusion and bugs.

Best Practices

  • Default to .astro components for static content; only reach for React/Vue/Svelte when you need client-side interactivity.
  • Use client:visible or client:idle over client:load to defer hydration and improve initial page load.
  • Keep frontmatter logic minimal; extract complex data fetching into utility functions under src/lib/ or src/utils/.
  • Use TypeScript for component props via the Props interface to catch errors at build time.
  • Leverage scoped styles in .astro files instead of CSS-in-JS to avoid shipping extra JavaScript.

Common Pitfalls

  • Forgetting client directives: A React/Vue/Svelte component without a client:* directive renders as static HTML with no interactivity. This is intentional but surprises newcomers.
  • Using client:only without specifying the framework: client:only requires the framework name (e.g., client:only="react"), otherwise Astro does not know which renderer to use.
  • Assuming frontmatter runs on the client: All code in the --- fence runs on the server. You cannot access window, document, or browser APIs there.
  • Importing npm packages that rely on browser globals: Some packages reference window at import time. Use dynamic imports inside <script> tags or client:only components to work around this.
  • Overusing islands: Not every interactive element needs a framework island. Small interactions (toggles, menus) can be handled with a vanilla <script> tag, avoiding the cost of shipping a framework runtime.

Install this skill directly: skilldb add astro-skills

Get CLI access →