Astro 5.0 Released - Evolution with Content Layer API and Server Islands

2025.12.02

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:

MetricAstro 4.x (Vite 5)Astro 5.0 (Vite 6)Improvement
Build time45s32s29% faster
Memory usage512MB380MB26% reduction
Dev server startup2.1s1.4s33% 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 Featurev5 Change
src/content/config.tsMoved to src/content.config.ts
getCollection() return valuebody property moved to render()
Legacy content collectionsMigration 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()
← Back to list