Skip to main content
โšก Calmops

Astro 5.0 Complete Guide: The Static-First Framework Revolution

Introduction

Astro has fundamentally changed how we think about building websites. Unlike traditional SPA frameworks that ship heavy JavaScript to the browser, Astro takes a “static-first” approach, shipping zero JavaScript by default. This philosophy has resonated with developers, making Astro one of the fastest-growing frameworks in the ecosystem.

Astro 5.0, released in 2026, represents the most significant update to the framework. It introduces the Content Layer API for structured content management, Server Islands for dynamic content in static sites, and numerous performance improvements.

This comprehensive guide covers everything you need to know about Astro 5.0. Whether you’re building a blog, documentation site, or marketing page, you’ll learn how to leverage Astro’s unique approach for maximum performance.


Why Choose Astro in 2026

The Astro Philosophy

Astro’s core philosophy is simple: ship less JavaScript. Here’s how it achieves this:

Approach JavaScript Sent Performance
Traditional SPA Full bundle Slower, more CPU usage
Astro Zero by default Instant, efficient
Islands Architecture Only interactive parts Optimal balance

Key Benefits

  • Zero JS by default: HTML-only pages are instant
  • Islands Architecture: Hydrate only what’s needed
  • Content Collections: Type-safe content management
  • Framework Agnostic: Use React, Vue, Svelte, etc.
  • Server Islands: Dynamic content in static sites

Getting Started with Astro 5.0

Installation

Create a new Astro project:

# Using the CLI
npm create astro@latest my-astro-site

# Select options:
# - Include sample files: Yes
# - Install dependencies: Yes
# - TypeScript: Strict

cd my-astro-site
npm run dev

Project Structure

my-astro-site/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ components/
โ”‚   โ”‚   โ”œโ”€โ”€ Header.astro
โ”‚   โ”‚   โ””โ”€โ”€ Counter.tsx  # React component
โ”‚   โ”œโ”€โ”€ content/
โ”‚   โ”‚   โ”œโ”€โ”€ config.ts
โ”‚   โ”‚   โ””โ”€โ”€ blog/
โ”‚   โ”‚       โ””โ”€โ”€ hello-world.md
โ”‚   โ”œโ”€โ”€ layouts/
โ”‚   โ”‚   โ””โ”€โ”€ BaseLayout.astro
โ”‚   โ”œโ”€โ”€ pages/
โ”‚   โ”‚   โ”œโ”€โ”€ index.astro
โ”‚   โ”‚   โ””โ”€โ”€ blog/[...slug].astro
โ”‚   โ””โ”€โ”€ styles/
โ”‚       โ””โ”€โ”€ global.css
โ”œโ”€โ”€ public/
โ”œโ”€โ”€ astro.config.mjs
โ””โ”€โ”€ package.json

Content Collections

Defining Collections

Astro 5.0’s Content Layer API provides type-safe content management:

// src/content/config.ts
import { defineCollection, z } from 'astro:content'

const blog = defineCollection({
  type: 'content', // v2.5+ uses 'content' or 'data'
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.date(),
    updatedDate: z.date().optional(),
    heroImage: z.string().optional(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false)
  })
})

const docs = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    order: z.number(),
    category: z.enum(['getting-started', 'guides', 'api'])
  })
})

export const collections = {
  blog,
  docs
}

Using Collections

---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content'

const posts = await getCollection('blog', ({ data }) => {
  return !data.draft
})

const sortedPosts = posts.sort(
  (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
)
---

<h1>Blog</h1>

<ul>
  {sortedPosts.map(post => (
    <li>
      <a href={`/blog/${post.slug}`}>
        <h2>{post.data.title}</h2>
        <p>{post.data.description}</p>
      </a>
    </li>
  ))}
</ul>

Server Islands

Server Islands are revolutionary. They let you render dynamic content within static pages:

Basic Server Island

---
// src/components/Weather.astro
// This runs on the server at request time
const weather = await fetch(
  'https://api.weather.com/current?city=London'
).then(r => r.json())
---

<div class="weather">
  <p>Current temperature: {weather.temp}ยฐC</p>
  <p>Condition: {weather.condition}</p>
</div>

Using Server Islands in Pages

---
// src/pages/index.astro
import Header from '../components/Header.astro'
import Footer from '../components/Footer.astro'
import Weather from '../components/Weather.astro'
import InteractiveCounter from '../components/Counter.tsx'
---

<html>
  <head>
    <title>My Site</title>
  </head>
  <body>
    <Header />  <!-- Static -->
    
    <main>
      <h1>Welcome</h1>
      
      <!-- Server Island - renders at request time -->
      <Weather server:defer />
      
      <!-- Client Component - hydrates on client -->
      <InteractiveCounter client:visible />
    </main>
    
    <Footer />  <!-- Static -->
  </body>
</html>

Server Island Options

Directive Behavior
server:defer Stream in after page load
server:load Render immediately on server
server:only Server-only, no SSR HTML

Astro Components

Component Syntax

---
// src/components/Card.astro
interface Props {
  title: string
  description?: string
  href: string
}

const { title, description = 'Learn more', href } = Astro.props
---

<article class="card">
  <h2>{title}</h2>
  {description && <p>{description}</p>}
  <a href={href}>{description}</a>
</article>

<style>
  .card {
    padding: 1rem;
    border: 1px solid #ddd;
    border-radius: 8px;
  }
</style>

Passing Props

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

<Card 
  title="Getting Started" 
  href="/docs/getting-started"
  description="Learn how to use Astro"
/>

Dynamic Routing

File-Based Routing

// src/pages/blog/[...slug].astro
import { getCollection } from 'astro:content'

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

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

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

Query Parameters

---
// src/pages/search.astro
const q = Astro.url.searchParams.get('q')
const results = q ? await search(q) : []
---

<form action="/search">
  <input name="q" value={q} />
  <button type="submit">Search</button>
</form>

{results.length > 0 && (
  <ul>
    {results.map(result => (
      <li><a href={result.url}>{result.title}</a></li>
    ))}
  </ul>
)}

Integrations

Adding Frameworks

# Add React
npx astro add react

# Add Vue
npx astro add vue

# Add Svelte
npx astro add svelte

# Add Tailwind
npx astro add tailwind

Using Framework Components

---
// src/pages/index.astro
import ReactCounter from '../components/ReactCounter.tsx'
import VueCounter from '../components/VueCounter.vue'
import SvelteCounter from '../components/SvelteCounter.svelte'
---

<!-- Each hydrates independently -->
<ReactCounter client:visible />
<VueCounter client:visible />
<SvelteCounter client:visible />

Hybrid Rendering

Switching to Server Rendering

// astro.config.mjs
import { defineConfig } from 'astro/config'

export default defineConfig({
  output: 'hybrid', // or 'server' for full SSR
  adapter: vercel()
})

Hybrid Page by Page

---
// src/pages/about.astro
// This page is static (default)
export const prerender = true
---

<h1>About - Static</h1>
---
// src/pages/dashboard.astro
// This page is server-rendered
// No prerender = server-rendered in hybrid mode
---

<h1>Dashboard - Dynamic</h1>

Data Fetching

Fetch in Frontmatter

---
// src/pages/users.astro
const response = await fetch('https://api.example.com/users')
const users = await response.json()
---

<ul>
  {users.map(user => (
    <li>{user.name}</li>
  ))}
</ul>

Using Databases

---
// src/pages/posts.astro
import { db, Post } from '../lib/db'

const posts = await db.select().from(Post).limit(10)
---

<ul>
  {posts.map(post => (
    <li>{post.title}</li>
  ))}
</ul>

Images

Optimized Images

---
// src/pages/index.astro
import { Image } from 'astro:assets'
import myImage from '../assets/hero.png'
---

<Image 
  src={myImage} 
  alt="Hero image"
  width={800}
  height={600}
  format="webp"
/>

External Images

---
import { Image } from 'astro:assets'
---

<Image 
  src="https://example.com/remote-image.jpg"
  alt="Remote image"
  width={800}
  height={600}
/>

Styling

Scoped Styles

<style>
  /* Only applies to this component */
  h1 {
    color: red;
  }
</style>

Global Styles

<style is:global>
  /* Applies everywhere */
  html {
    font-family: system-ui;
  }
</style>

Tailwind Integration

npx astro add tailwind
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
  Button
</button>

Performance Optimization

Lazy Loading Components

---
// Only load when visible
import HeavyComponent from '../components/HeavyComponent'
---

<HeavyComponent client:visible />

Prefetching

---
// astro.config.mjs
export default defineConfig({
  prefetch: {
    prefetchAll: true,
    defaultStrategy: 'viewport'
  }
})

Image Optimization

---
import { Image } from 'astro:assets'
import heroImage from '../assets/hero.jpg'
---

<!-- Automatic WebP conversion, lazy loading, srcset -->
<Image src={heroImage} alt="Hero" />

Deployment

Vercel

npx astro add vercel
// astro.config.mjs
import vercel from '@astrojs/vercel'

export default defineConfig({
  output: 'hybrid',
  adapter: vercel()
})

Netlify

npx astro add netlify

Node.js

npx astro add node

External Resources

Official Documentation

Learning Resources

Tools


Conclusion

Astro 5.0 represents the future of web development. Its static-first approach with Islands Architecture provides the best of both worlds: the performance of static HTML with the interactivity of modern frameworks.

Key takeaways:

  1. Content Collections: Type-safe content management
  2. Server Islands: Dynamic content in static sites
  3. Islands Architecture: Ship less JavaScript
  4. Framework Agnostic: Use your favorite framework
  5. Hybrid Rendering: Mix static and dynamic as needed

Whether you’re building a blog, documentation, marketing site, or web app, Astro 5.0 provides the tools and performance you need to succeed.


Comments