Astro: Static output, component-based authoring

Astro takes an approach that sits between the two main camps of static site generators. For years, you've had to choose between a JavaScript-powered generator that ships a client-side bundle, or a more traditional HTML-focused tool with its own templating system. Astro's pitch is that you can write components in the framework syntax you already know—JSX, Svelte, Vue, or web components—and still get zero-JavaScript static output by default. Client-side behavior is an opt-in per component, which is where the framework's more interesting details live.

Starting up

Scaffolding a project is the standard modern experience:

npm init astro
npm install
npm start

The initial output walks you through what just happened:

You get a local dev server immediately, with the expected HMR and CSS injection behavior:

Components without the JavaScript payload

Astro supports multiple renderers, so your components can be .jsx, .svelte, or .vue files, and the renderer list is extensible. But the native format is the .astro file, which has a few conveniences built in:

  • The format is JSX-like but handles things like the <head> automatically.
  • Styled scoping comes out of the box via a plain <style> tag.
  • A JavaScript "fence" at the top of the file runs during the build, not in the browser.

An .astro file gets straight to the point with minimal boilerplate:

---
import SomeComponent from "../components/SomeComponent";

// This runs in Node, so you look at your command line to see it.
console.log("Hi.");

// Example: <SomeComponent greeting="(Optional) Hello" name="Required Name" />
const { greeting = 'Hello', name } = Astro.props;
const items = ["Dog", "Cat", "Platipus"];
---
<!-- JSX-like, but also more pleasantly HTML like, like this comment  -->
<div class="module">
  <h1>{greeting}, {name}!</h1>
  <ul>
    {items.map((item) => (
      <li>{item}</li>
    ))}
</ul>
</div>

<SomeComponent regular="props" />

<style>
   /* Scoped! */
  .module {
    padding: 1rem;
  }
</style>

The fence—the --- delimiters—holds the component's logic: imports, prop declarations (typed, if you want), and any data setup for the template below. Because this code executes in Node during the build, a console.log() here shows up in your terminal, not the browser console.

File-system routing

Routing in Astro works the way a classic Apache server handles files. If your project has a structure like:

index.html
/about/
  index.html

Then http://website.com/about serves the index.html under the /about folder. Any files you add to your pages directory become routes:

/pages/
  index.astro
  about.astro

That gives you a homepage and an /about/ page without setting up a routing library or component hierarchy.

Markdown as a first-class content option

Markdown is handled two ways. First, you can author entire pages in Markdown. The file's frontmatter specifies which layout to use—typically an .astro file—and the page content flows into the layout's <slot />:

Second, Astro ships a built-in <Markdown /> component that you can import and use anywhere in a component:

---
import { Markdown } from 'astro/components';
---

<main>
  <Markdown>
    # Hello world!
    
    - Do thing
    - Another thing in my *cool list*
  </Markdown>

  <div>Outside Markdown</div>
</main>

Fetching data at build time

For pulling in content from within your project, Astro provides fetchContent, which returns raw Markdown and its generated HTML:

---
import { Markdown } from 'astro/components';
const localData = Astro.fetchContent('../content/data.md');
---

<div class="module">
  <Markdown content={localData[0].astro.source} />
</div>

External data fetching happens just as simply, right alongside the component that needs it. This example pulls posts from the CSS-Tricks API and renders them as cards:

---
import Card from '../components/Card.astro';
import Header from '../components/Header';

const remoteData = await fetch('https://css-tricks.com/wp-json/wp/v2/posts?per_page=12&_embed').then(response => response.json());
---

<!doctype html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>CSS-Trickzz</title>
    <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⭐️</text></svg>">
    <link rel="stylesheet" href="https://css-tricks.com/style/global.css">

    <style lang="scss">
      .grid {
        margin: 4rem;
        display: grid;
        grid-template-columns: repeat(2, 1fr);
        @media (max-width: 650px) {
          grid-template-columns: repeat(1, 1fr);
          margin: 2rem;
        }
        gap: 3rem;
      }
    </style>
</head>

<body>
  <main>
    <Header />

    <div class="grid">
      {remoteData.map((post) => {
        return(
          <Card 
            title={post.title.rendered}
            link={post.link}
            excerpt={post.excerpt.rendered}
            featured_img={post.featured_media_src_url}
          />
        )
      })}
    </div>

  </main>
</body>

</html>

That single code block is enough to build a whole page from the remote data:

Two things follow from this: the fetch runs in Node during the build, so a site like this needs a regular build/deploy cycle to stay current. And unlike some site generators that push you into a separate data file with your own network library, the request lives with the template that consumes it.

Supporting many frameworks

Astro's multi-framework support draws some criticism, both for the npm install footprint and for the idea of mixing frameworks in one project. The counter-argument is that this only affects the build process. Since the user receives plain HTML, the cost of unused renderers never reaches them. If you do hydrate components for client-side interactions, sticking to one framework is the sensible move—and the framework's own examples point toward lightweight options for that purpose.

Styling options

Astro takes a more opinionated stance on styling than many of its peers, covering a wide range of techniques out of the box:

  • Plain CSS via import "./style.css";
  • A <style> block in .astro files, scoped to that component automatically
  • CSS modules for .jsx files
  • Normal .svelte and .vue scoped styles
  • Sass, activated by adding lang="scss" to a style block

More details live in the styling documentation.

Opting in to client-side JavaScript

By default, components render as static HTML. To make a component interactive, you add a modifier to the element, as the README specifies:

  • <MyComponent /> will render an HTML-only version of MyComponent (default)
  • <MyComponent:load /> will render MyComponent on page load
  • <MyComponent:idle /> will use requestIdleCallback() to render MyComponent as soon as main thread is free
  • <MyComponent:visible /> will use an IntersectionObserver to render MyComponent when the element enters the viewport

So you can defer even the component's JavaScript until the browser is idle or until the element scrolls into view. As the announcement post puts it, when a component does need JavaScript, Astro loads only that component and its dependencies, leaving everything else as static HTML. That's the model for good interactive default behavior without the page-wide JavaScript tax.

Maturity and ecosystem

Astro is very new. As of this writing, full documentation hasn't been published; the README is the main reference. The project maintains a public Discord, which serves as a fast feedback channel for the team. The stated ambition goes beyond the framework itself—the aim is for Astro to become a platform where the open-source tool is just the core, a topic covered in detail on a Learn with Jason episode with one of the creators.