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.astrofiles, scoped to that component automatically - CSS modules for
.jsxfiles - Normal
.svelteand.vuescoped 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 ofMyComponent(default)<MyComponent:load />will renderMyComponenton page load<MyComponent:idle />will use requestIdleCallback() to renderMyComponentas soon as main thread is free<MyComponent:visible />will use an IntersectionObserver to renderMyComponentwhen 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.



