Astro 5.0’s Innovation
Astro 5.0 is a new milestone in content-driven website building frameworks. Key features include flexible data source integration through the Content Layer API and selective SSR through Server Islands.
flowchart TB
subgraph Astro5["Astro 5.0 Architecture"]
subgraph ContentLayer["Content Layer API (New)"]
Local["Local<br/>Markdown"]
CMS["CMS<br/>(Contentful)"]
API["API<br/>(REST)"]
Database["Database<br/>(Drizzle)"]
end
Collections["Unified Type-Safe Collections"]
subgraph Rendering["Rendering"]
SSG["SSG<br/>(Static)"]
ServerIslands["Server Islands<br/>(Partial SSR)"]
SSR["Full SSR<br/>(Dynamic)"]
end
Local --> Collections
CMS --> Collections
API --> Collections
Database --> Collections
Collections --> SSG
Collections --> ServerIslands
Collections --> SSR
end
Content Layer API
New Content Definition
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob, file } from 'astro/loaders';
// Collection from local files
const blog = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
description: z.string(),
publishDate: z.coerce.date(),
tags: z.array(z.string()),
draft: z.boolean().default(false),
}),
});
// Collection from JSON file
const authors = defineCollection({
loader: file('./src/data/authors.json'),
schema: z.object({
name: z.string(),
email: z.string().email(),
avatar: z.string().url(),
bio: z.string(),
}),
});
export const collections = { blog, authors };
Creating Custom Loaders
// src/loaders/cms-loader.ts
import { Loader } from 'astro/loaders';
export function contentfulLoader(options: {
spaceId: string;
accessToken: string;
contentType: string;
}): Loader {
return {
name: 'contentful-loader',
async load({ store, logger }) {
logger.info('Fetching content from Contentful...');
const response = await fetch(
`https://cdn.contentful.com/spaces/${options.spaceId}/entries?content_type=${options.contentType}`,
{
headers: {
Authorization: `Bearer ${options.accessToken}`,
},
}
);
const data = await response.json();
for (const item of data.items) {
store.set({
id: item.sys.id,
data: {
title: item.fields.title,
body: item.fields.body,
slug: item.fields.slug,
publishedAt: item.sys.createdAt,
},
});
}
logger.info(`Loaded ${data.items.length} entries`);
},
};
}
// Usage in src/content.config.ts
import { contentfulLoader } from './loaders/cms-loader';
const articles = defineCollection({
loader: contentfulLoader({
spaceId: import.meta.env.CONTENTFUL_SPACE_ID,
accessToken: import.meta.env.CONTENTFUL_ACCESS_TOKEN,
contentType: 'article',
}),
schema: z.object({
title: z.string(),
body: z.string(),
slug: z.string(),
publishedAt: z.coerce.date(),
}),
});
Server Islands
Server Islands is a feature that embeds dynamic server-rendered components within static pages.
flowchart TB
subgraph StaticHTML["Static HTML (cacheable)"]
Header["Header (Static)"]
subgraph MainContent["Main Content"]
Article["Article Content<br/>(Static)"]
subgraph ServerIsland["Server Island"]
UserInfo["User Info<br/>(Dynamic)"]
CommentCnt["Comment Count<br/>(Dynamic)"]
end
end
Footer["Footer (Static)"]
Header --> MainContent --> Footer
end
Note["Server Islands can be lazy-loaded with fallback display"]
Implementation Example
---
// src/components/UserProfile.astro
export const prerender = false; // Set as Server Island
import { getUser } from '../lib/auth';
const user = await getUser(Astro.cookies);
---
{user ? (
<div class="user-profile">
<img src={user.avatar} alt={user.name} />
<span>{user.name}</span>
<a href="/logout">Logout</a>
</div>
) : (
<a href="/login" class="login-button">Login</a>
)}
---
// src/pages/blog/[slug].astro
import { getCollection, getEntry } from 'astro:content';
import UserProfile from '../../components/UserProfile.astro';
import CommentSection from '../../components/CommentSection.astro';
export const prerender = true; // Static generation
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<html>
<head>
<title>{post.data.title}</title>
</head>
<body>
<header>
<!-- Server Island: Dynamic user info -->
<UserProfile server:defer>
<div slot="fallback">Loading...</div>
</UserProfile>
</header>
<main>
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
<!-- Server Island: Dynamic comments -->
<CommentSection server:defer postId={post.id}>
<div slot="fallback">Loading comments...</div>
</CommentSection>
</main>
</body>
</html>
astro:env Module
A new module that provides type-safe access to environment variables.
// astro.config.mjs
import { defineConfig, envField } from 'astro/config';
export default defineConfig({
env: {
schema: {
// Public variables (also available on client)
PUBLIC_API_URL: envField.string({
context: 'client',
access: 'public',
default: 'https://api.example.com',
}),
// Secret variables (server only)
DATABASE_URL: envField.string({
context: 'server',
access: 'secret',
}),
// Optional variables
ANALYTICS_ID: envField.string({
context: 'client',
access: 'public',
optional: true,
}),
// Number type
CACHE_TTL: envField.number({
context: 'server',
access: 'public',
default: 3600,
}),
// Enum type
NODE_ENV: envField.enum({
values: ['development', 'production', 'test'],
context: 'server',
access: 'public',
default: 'development',
}),
},
},
});
// Usage example
import { PUBLIC_API_URL, ANALYTICS_ID } from 'astro:env/client';
import { DATABASE_URL, CACHE_TTL } from 'astro:env/server';
// Type-safe access
const apiUrl: string = PUBLIC_API_URL;
const analyticsId: string | undefined = ANALYTICS_ID;
const dbUrl: string = DATABASE_URL; // Only available on server
Vite 6 Support
Astro 5.0 adopts Vite 6 by default, improving build performance.
1000 page site performance comparison:
| Metric | Astro 4.x (Vite 5) | Astro 5.0 (Vite 6) | Improvement |
|---|---|---|---|
| Build time | 45s | 32s | 29% faster |
| Memory usage | 512MB | 380MB | 26% reduction |
| Dev server startup | 2.1s | 1.4s | 33% faster |
Other New Features
Improved TypeScript Inference
// Collection types are automatically inferred
import { getCollection } from 'astro:content';
const posts = await getCollection('blog');
// posts: Array<{ id: string; data: BlogSchema; ... }>
// Filtering is also type-safe
const publishedPosts = await getCollection('blog', ({ data }) => {
return data.draft !== true; // data.draft is boolean | undefined
});
Experimental Features
// astro.config.mjs
export default defineConfig({
experimental: {
// SVG component support
svg: true,
// Automatic responsive image generation
responsiveImages: true,
// Content Intellisense
contentIntellisense: true,
},
});
---
// Use as SVG component
import Logo from '../assets/logo.svg';
---
<Logo class="w-32 h-32" fill="currentColor" />
Migration Guide
Migrating from v4 to v5
# Upgrade with Astro CLI
npx @astrojs/upgrade
Major Breaking Changes
| v4 Feature | v5 Change |
|---|---|
src/content/config.ts | Moved to src/content.config.ts |
getCollection() return value | body property moved to render() |
| Legacy content collections | Migration to Content Layer API recommended |
Astro.glob() | getCollection() recommended |
// v4: Old method
const { Content } = await entry.render();
const body = entry.body; // Direct access
// v5: New method
const { Content } = await entry.render();
// body is only accessible via render()