What is Astro?
Astro is a content-first web framework built around a simple bet: most pages do not need much JavaScript. It renders pages to HTML by default, ships no client JavaScript unless you ask for it, and lets you add interactive components only where they matter.
That design makes Astro exceptionally good at the kinds of sites that make up much of the web: blogs, documentation, marketing pages, portfolios and content-heavy products. It is not anti-JavaScript — it is pro-restraint. You get the interactivity you need and nothing you do not.
.astro components
An Astro component is a file with a frontmatter script, a template and optional scoped styles.
---
// src/components/Card.astro
interface Props {
title: string;
href: string;
}
const { title, href } = Astro.props;
---
<a class="card" href={href}>
<h3>{title}</h3>
<slot />
</a>
<style>
.card {
display: block;
border-radius: 1rem;
padding: 1.5rem;
}
</style>
The frontmatter runs on the server at build time or per request. The template is HTML with JSX-like expressions, and the <slot /> lets a parent pass children. Styles are scoped to the component by default. Because the component runs on the server, you can query databases, read files and call APIs directly in the frontmatter.
File-based routing
Files in src/pages become routes, and dynamic segments use brackets.
src/pages/
├── index.astro # /
├── about.astro # /about
├── blog/
│ ├── index.astro # /blog
│ └── [slug].astro # /blog/:slug
└── rss.xml.js # /rss.xml
For dynamic routes you export getStaticPaths to declare which pages to build, or use SSR to render on demand.
Content collections
Content collections are Astro’s answer to structured content. You define a schema, and every markdown or JSON entry is validated at build time.
// src/content.config.ts
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
schema: z.object({
title: z.string(),
publishedAt: z.coerce.date(),
tags: z.array(z.string()).default([]),
}),
});
export const collections = { blog };
---
// src/pages/blog/index.astro
import { getCollection } from "astro:content";
const posts = (await getCollection("blog"))
.sort((a, b) => b.data.publishedAt.valueOf() - a.data.publishedAt.valueOf());
---
<ul>
{posts.map((post) => (
<li>
<a href={`/blog/${post.id}`}>{post.data.title}</a>
</li>
))}
</ul>
You get type safety, validation and a clean way to query content without a database. For a site with hundreds of posts or docs pages, this is a significant quality-of-life improvement.
The islands architecture
This is Astro’s defining feature. A page is static HTML, and any interactive components are islands that hydrate independently. You control when each island loads with a client directive.
---
import Counter from "../components/Counter.jsx";
import Comments from "../components/Comments.jsx";
---
<Counter client:load />
<Comments client:visible />
The directives are:
client:loadhydrates immediately — for critical, above-the-fold interactivity.client:idlehydrates once the browser is free.client:visiblehydrates when the component scrolls into view.client:onlyskips server rendering and renders only in the browser.client:mediahydrates when a media query matches.
Choosing the least eager directive that still works keeps the page fast. Most content pages need none at all.
Bring your own UI library
Astro can render components from React, Vue, Svelte, Solid and Preact — in the same project.
---
import ReactChart from "../components/Chart.jsx";
import VueForm from "../components/Form.vue";
---
<ReactChart client:load data={data} />
<VueForm client:visible />
Each framework is an optional integration. You can use React for one island and Svelte for another, or skip frameworks entirely. That flexibility means you are never locked in, and you can adopt a library only where it earns its place.
SSR and adapters
Astro is static by default, but it can render on demand. Add an adapter for your platform and opt routes into server rendering.
// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";
export default defineConfig({
output: "server",
adapter: node({ mode: "standalone" }),
});
API endpoints live in src/pages/api and export HTTP methods, and server islands let you render dynamic fragments inside otherwise static pages. This makes Astro suitable for hybrid sites: a static blog with a dynamic dashboard, or a marketing site with personalised sections.
Best practices
- Default to
.astrocomponents; add framework components only for interactivity. - Use the least eager client directive that works.
- Keep content in collections with a schema instead of free-form markdown.
- Scope styles with component
<style>blocks or a chosen CSS tool. - Render static pages at build time and reserve SSR for dynamic routes.
- Use layouts for shared shells and metadata.
- Optimise images with the built-in image components.
Common mistakes
- Adding
client:loadeverywhere and losing the zero-JavaScript benefit. - Reaching for a UI framework when an
.astrocomponent would do. - Forgetting to define a schema and losing type safety in collections.
- Assuming Astro cannot do dynamic apps and overlooking SSR.
- Mixing too many UI frameworks and bloating the project for no reason.
- Using client-side fetching for content that could be rendered at build time.
Where to go next
Astro is the pragmatic choice for content-heavy sites that still need pockets of interactivity. Solidify your HTML and CSS, then compare the model with Next.js, Nuxt and SvelteKit. Then build a small blog with a content collection and one interactive island to see the trade-offs for yourself.