MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Astro 4.0: Content-Focused Web Framework

Astro 4.0: server islands, content layers, and improved performance for content sites.

AstroSSGContentFrontend

By MinhVo

Introduction

Astro 4.0 represents a watershed moment for content-driven web development. Released on December 5, 2023, this major version introduces server islands, incremental content caching, internationalization routing, a revolutionary dev toolbar, and a completely redesigned documentation site. Astro has firmly established itself as the go-to framework for blogs, marketing pages, documentation sites, and e-commerce storefronts — any project where content performance and SEO are paramount. With over 75,000 GitHub stars and adoption by companies like Google, Microsoft, and Nordstrom, Astro's "zero JavaScript by default" philosophy has resonated deeply with the web development community.

Modern web framework architecture diagram

The core thesis behind Astro is elegantly simple: most web frameworks ship far too much JavaScript to the browser. A blog post rendered by a React SPA sends hundreds of kilobytes of JavaScript for content that could be served as static HTML. Astro flips this model entirely. Every component renders to HTML at build time (or request time for SSR), and JavaScript is only delivered for interactive "islands" — small, isolated components that genuinely require client-side interactivity. The result is pages that load in milliseconds, score 100 on Lighthouse, and deliver exceptional Core Web Vitals.

Astro 4.0 builds on this foundation with features that push the framework into territory traditionally reserved for full-stack web application frameworks. Server islands enable mixing static and dynamic content on a single page without sacrificing cacheability. Content collections now support live data sources alongside local files. The new dev toolbar transforms the local development experience with built-in accessibility auditing and island inspection. And incremental content caching slashes build times by skipping unchanged content.

Understanding Astro 4.0: Core Concepts

The Islands Architecture Deep Dive

The islands architecture is not merely a feature of Astro — it is the fundamental architectural pattern that defines how Astro applications are structured and rendered. Unlike traditional single-page applications (SPAs) where the entire page is hydrated as one monolithic JavaScript bundle, Astro decomposes each page into a static HTML shell with isolated interactive components (islands) that hydrate independently.

When Astro renders a page, it processes every .astro component, every Markdown file, and every framework component (React, Svelte, Vue, etc.) into static HTML. This happens at build time for static sites, or at request time for server-rendered pages. The critical insight is that framework components are rendered to HTML but not hydrated unless explicitly instructed. A React component in an Astro page produces its HTML output during the build, but no React JavaScript is sent to the browser unless you add a client:* directive.

---
// This React component renders to HTML at build time.
// No React JavaScript ships to the browser.
import Counter from '../components/Counter';
---
 
<!-- Static HTML output — zero JavaScript -->
<Counter count={0} />
 
<!-- This island hydrates on the client — ships React + component JS -->
<Counter count={0} client:load />

The hydration directives control when JavaScript loads:

  • client:load — Hydrate immediately on page load. Use for above-the-fold interactive elements.
  • client:idle — Hydrate when the browser becomes idle (via requestIdleCallback). Use for non-critical interactive elements.
  • client:visible — Hydrate when the element scrolls into the viewport (via IntersectionObserver). Use for below-the-fold content.
  • client:media — Hydrate when a CSS media query matches. Use for responsive components (e.g., mobile hamburger menu).
  • client:only — Skip server rendering entirely, render only on the client. Use sparingly for components that depend on browser APIs.

Islands architecture rendering flow

Server Islands: Static Shell, Dynamic Content

Server islands, introduced in Astro 4.x and refined through subsequent releases, solve one of the most challenging problems in web performance: how to serve personalized or dynamic content within a cacheable static page. Traditionally, you had to choose — either the entire page is static (fast but no personalization) or the entire page is server-rendered (personalized but slower and harder to cache).

Server islands break this tradeoff. When you mark a component with the server:defer directive, Astro splits it into its own endpoint at build time. The rest of the page renders as static HTML and can be aggressively cached by CDNs. The server island is fetched separately on the client and its content replaces the fallback placeholder.

---
import ProductLayout from '../layouts/ProductLayout.astro';
import PriceWidget from '../components/PriceWidget';
import UserAvatar from '../components/UserAvatar.astro';
---
 
<ProductLayout title="Premium Widget">
  <!-- Static content: rendered and cached at build time -->
  <h1>Premium Widget</h1>
  <p>Our best-selling widget with amazing features.</p>
  <img src="/images/widget.jpg" alt="Premium Widget" />
 
  <!-- Server Island: personalized pricing, rendered per-request -->
  <PriceWidget productId="widget-123" server:defer>
    <div slot="fallback">Loading price...</div>
  </PriceWidget>
 
  <!-- Server Island: user-specific avatar -->
  <UserAvatar server:defer>
    <div slot="fallback">
      <div class="avatar-placeholder"></div>
    </div>
  </UserAvatar>
</ProductLayout>

The implementation works primarily at build time. When Astro encounters a server:defer directive, it omits the component's content from the main page and injects a small inline script. This script fetches the component's rendered HTML from a special endpoint. The component itself has full access to cookies, headers, and request data — it behaves exactly like a normal server-rendered component.

Props passed to server islands must be serializable: number, string, Array, Map, Set, RegExp, Date, BigInt, URL, Uint8Array, and plain objects are supported. Functions and objects with circular references cannot be serialized and will throw errors at build time.

Fallback content is critical for good user experience. Without it, the server island's container renders as empty space until the fetch completes. Best practices include:

  • Generic placeholders: Show a skeleton UI or loading spinner that matches the island's dimensions.
  • Meaningful defaults: Display cached or generic data (e.g., a default avatar, estimated pricing).
  • Progressive disclosure: Show the most important content first, then load personalized content.
---
import Avatar from '../components/Avatar.astro';
import GenericAvatar from '../components/GenericAvatar.astro';
---
 
<Avatar server:defer>
  <GenericAvatar slot="fallback" />
</Avatar>

Content Collections and Content Layers

Content collections provide a type-safe, schema-validated way to manage structured content in Astro. Collections are defined by the location and shape of their entries, and Astro provides performant, scalable APIs to load, query, and render content from anywhere — local Markdown files, headless CMSs, databases, or live API endpoints.

// src/content/config.ts
import { defineCollection, z } from 'astro:content';
import { glob, file } from 'astro/loaders';
 
const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/data/blog' }),
  schema: z.object({
    title: z.string(),
    date: z.coerce.date(),
    description: z.string(),
    tags: z.array(z.string()),
    published: z.boolean().default(true),
    featured: z.boolean().default(false),
    author: z.string().default('MinhVo'),
  }),
});
 
const authors = defineCollection({
  loader: file('./src/data/authors.json'),
  schema: z.object({
    name: z.string(),
    bio: z.string(),
    avatar: z.string().url(),
    social: z.object({
      twitter: z.string().optional(),
      github: z.string().optional(),
    }).optional(),
  }),
});
 
export const collections = { blog, authors };

Astro 4.0 introduces two types of collections:

  1. Build-time collections — Content is loaded and validated at build time. This is the default and fastest option for content that doesn't change between deploys. Use the glob() loader for directories of files or the file() loader for single-file collections.

  2. Live collections — Content is fetched at request time from frequently-updating sources. Use custom loaders that fetch from databases, APIs, or CMSs. Live collections support Zod schemas for validation but require an adapter for on-demand rendering.

// Live collection loader for database content
const products = defineCollection({
  loader: {
    name: 'postgres-loader',
    load: async ({ store }) => {
      const db = await getDbConnection();
      const products = await db.query('SELECT * FROM products');
      for (const product of products.rows) {
        store.set(product.id, {
          data: product,
          id: product.id,
        });
      }
    },
  },
  schema: z.object({
    name: z.string(),
    price: z.number(),
    description: z.string(),
    inStock: z.boolean(),
  }),
});

Querying collections is straightforward with type-safe APIs:

---
import { getCollection, getEntry } from 'astro:content';
 
// Get all published blog posts, sorted by date
const allPosts = await getCollection('blog', ({ data }) => data.published);
const sortedPosts = allPosts.sort(
  (a, b) => b.data.date.valueOf() - a.data.date.valueOf()
);
 
// Get a single entry by slug
const post = await getEntry('blog', 'my-first-post');
 
// Access referenced data
const author = await getEntry(post.data.author);
---
 
<ul>
  {sortedPosts.map(post => (
    <li>
      <a href={`/blog/${post.slug}`}>{post.data.title}</a>
      <time>{post.data.date.toLocaleDateString()}</time>
    </li>
  ))}
</ul>

The Astro Dev Toolbar

Astro 4.0 introduces the Dev Toolbar — a powerful browser-based development tool that appears only during local development. It is never included in production builds. The toolbar provides three built-in applications:

Inspect: Highlights interactive islands on the page, showing which elements have client-side JavaScript and which are static HTML. Click an island to view its props and open the source file in your editor.

Audit: Runs automated accessibility checks against the current page, catching missing alt text, improper ARIA roles, and other common issues without leaving the browser.

Sentry (Spotlight.js): A partnership with Sentry that surfaces runtime errors, network failures, and performance issues directly in the dev environment.

Third-party developers can build custom toolbar apps using the Dev Toolbar API, enabling features like surfacing test failures, linting errors, or CMS preview data directly in the browser.

Web development tools and debugging

Internationalization (i18n) Routing

Astro 4.0 adds first-class internationalization support with built-in i18n routing. Configure locales, default language, and routing strategy in astro.config.mjs:

// astro.config.mjs
import { defineConfig } from 'astro/config';
 
export default defineConfig({
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'es', 'fr', 'de', 'ja'],
    routing: {
      prefixDefaultLocale: false, // /about vs /en/about
      redirectToDefaultLocale: true,
      fallbackType: 'redirect',
    },
  },
});

Astro automatically generates the correct routing for translated pages, handles hreflang tags for SEO, and provides helper functions for locale-aware URL generation:

---
import { getRelativeLocaleUrl } from 'astro:i18n';
 
const aboutUrl = getRelativeLocaleUrl('es', 'about');
// Returns: /es/about
---
<a href={aboutUrl}>Sobre nosotros</a>

Incremental Content Caching

Astro 4.0 introduces experimental incremental content caching, which skips re-processing content that hasn't changed since the last build. For large sites with thousands of pages, this can reduce build times from minutes to seconds. The cache is stored in .astro/cache and is invalidated when content files or their dependencies change.

// astro.config.mjs
export default defineConfig({
  experimental: {
    contentCollectionCache: true,
  },
});

View Transitions API

Astro 4.0 expands its View Transitions support with new APIs for controlling page transitions:

---
import { ViewTransitions } from 'astro:transitions';
---
 
<html>
  <head>
    <ViewTransitions />
  </head>
  <body>
    <h1 transition:animate="fade">My Page</h1>
  </body>
</html>

Supported animations include fade, slide, and custom Keyframe animations. The transition:persist directive keeps elements alive across navigations — useful for audio players, video embeds, or map widgets.

Architecture and Design Patterns

The Static-First Pattern

Generate all content pages as static HTML at build time. Use server islands only for personalized or real-time sections. This maximizes CDN cacheability and produces the fastest possible load times.

Page Request Flow:
1. CDN serves cached static HTML (0ms origin latency)
2. Browser renders static content immediately
3. Inline scripts fetch server islands in parallel
4. Server islands stream personalized content
5. Islands replace fallback content when ready

The Multi-Framework Pattern

Astro's unique multi-framework support allows different interactive components to use different frameworks on the same page. This is invaluable during migrations or when teams have different framework expertise:

---
import SearchBar from '../components/SearchBar.tsx';      // React
import CartDrawer from '../components/CartDrawer.svelte';  // Svelte
import ImageGallery from '../components/Gallery.vue';      // Vue
import MotionBox from '../components/MotionBox.tsx';        // React + Framer Motion
---
 
<main>
  <SearchBar client:visible />
  <CartDrawer client:idle />
  <ImageGallery client:media="(min-width: 768px)" />
  <MotionBox client:load />
</main>

Each framework's JavaScript is loaded independently and only when the hydration condition is met. React's bundle is only fetched if the SearchBar island becomes visible. Svelte's bundle is only fetched when the browser is idle.

The Content Pipeline Pattern

Build a robust, type-safe content pipeline that validates content at build time:

// src/content/config.ts
import { defineCollection, z, reference } from 'astro:content';
import { glob } from 'astro/loaders';
 
const blog = defineCollection({
  loader: glob({ pattern: '**/*.{md,mdx}', base: './src/data/blog' }),
  schema: z.object({
    title: z.string().min(1).max(100),
    date: z.coerce.date(),
    description: z.string().min(50).max(160),
    tags: z.array(z.string()).min(1).max(5),
    author: reference('authors'),
    heroImage: z.string().optional(),
    ogImage: z.string().url().optional(),
    canonical: z.string().url().optional(),
    draft: z.boolean().default(false),
  }),
});

This schema validates frontmatter at build time — catching missing titles, invalid dates, overly long descriptions, and missing tags before they reach production.

The Progressive Enhancement Pattern

Build pages that work without JavaScript, then enhance with interactive islands:

---
import LikeButton from '../components/LikeButton';
---
 
<!-- Static fallback: a form that works without JS -->
<form method="POST" action="/api/like">
  <input type="hidden" name="postId" value={post.id} />
  <button type="submit">Like ({post.likes})</button>
</form>
 
<!-- Progressive enhancement: replaces form with interactive button -->
<LikeButton postId={post.id} initialCount={post.likes} client:visible />

Step-by-Step Implementation

Setting Up Astro 4.0

# Create a new Astro 4.0 project
npm create astro@latest my-site -- --template blog
 
# Add framework integrations
npx astro add react
npx astro add tailwind
 
# Start development
npm run dev
// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import svelte from '@astrojs/svelte';
import tailwind from '@astrojs/tailwind';
import mdx from '@astrojs/mdx';
 
export default defineConfig({
  integrations: [react(), svelte(), tailwind(), mdx()],
  output: 'hybrid',
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'es', 'fr'],
    routing: { prefixDefaultLocale: false },
  },
  experimental: {
    contentCollectionCache: true,
  },
});

Content Collections with Schema

// src/content/config.ts
import { defineCollection, z, reference } from 'astro:content';
import { glob, file } from 'astro/loaders';
 
const blog = defineCollection({
  loader: glob({ pattern: '**/*.{md,mdx}', base: './src/data/blog' }),
  schema: z.object({
    title: z.string(),
    date: z.coerce.date(),
    description: z.string(),
    tags: z.array(z.string()),
    published: z.boolean().default(true),
    author: reference('authors'),
  }),
});
 
const authors = defineCollection({
  loader: file('./src/data/authors.json'),
  schema: z.object({
    name: z.string(),
    bio: z.string(),
    avatar: z.string(),
  }),
});
 
export const collections = { blog, authors };
---
// src/pages/blog/[...slug].astro
import { getCollection, getEntry } from 'astro:content';
import BlogLayout from '../../layouts/BlogLayout.astro';
 
export async function getStaticPaths() {
  const posts = await getCollection('blog', ({ data }) => data.published);
  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post },
  }));
}
 
const { post } = Astro.props;
const { Content } = await post.render();
const author = await getEntry(post.data.author);
---
 
<BlogLayout title={post.data.title} date={post.data.date}>
  <header>
    <h1>{post.data.title}</h1>
    <p>By {author.data.name} on {post.data.date.toLocaleDateString()}</p>
  </header>
  <Content />
</BlogLayout>

Server Islands in Practice

---
// src/pages/products/[slug].astro
import ProductLayout from '../../layouts/ProductLayout.astro';
import PriceDisplay from '../../components/PriceDisplay';
import ReviewsWidget from '../../components/ReviewsWidget';
import { getCollection } from 'astro:content';
 
export async function getStaticPaths() {
  const products = await getCollection('products');
  return products.map(product => ({
    params: { slug: product.slug },
    props: { product },
  }));
}
 
const { product } = Astro.props;
---
 
<ProductLayout title={product.data.name}>
  <!-- Static content: cached at the edge -->
  <h1>{product.data.name}</h1>
  <p>{product.data.description}</p>
  <img src={product.data.image} alt={product.data.name} />
 
  <!-- Dynamic: real-time pricing from inventory system -->
  <PriceDisplay sku={product.data.sku} server:defer>
    <span class="price-skeleton" slot="fallback">Loading price...</span>
  </PriceDisplay>
 
  <!-- Dynamic: user reviews from database -->
  <ReviewsWidget productId={product.id} server:defer>
    <div class="reviews-loading" slot="fallback">
      <p>Loading reviews...</p>
    </div>
  </ReviewsWidget>
</ProductLayout>

Multi-Framework Islands

---
import SearchBar from '../components/SearchBar.tsx';        // React
import CartDrawer from '../components/CartDrawer.svelte';    // Svelte
import ImageGallery from '../components/Gallery.vue';        // Vue
import ThemeToggle from '../components/ThemeToggle.tsx';     // React
import NotificationBell from '../components/Notification.tsx'; // React
---
 
<header>
  <h1>My Store</h1>
  <SearchBar client:visible />
  <ThemeToggle client:idle />
  <NotificationBell client:idle />
</header>
 
<main>
  <ImageGallery images={product.images} client:media="(min-width: 768px)" />
</main>
 
<aside>
  <CartDrawer client:load />
</aside>

Web performance and optimization metrics

Performance Benchmarks and Comparisons

JavaScript Bundle Size

ScenarioAstro 4.0Next.js (SPA)Gatsby11ty
Blog post (no interactivity)0 KB JS85-200 KB150-300 KB0 KB
Blog post with comments widget15-30 KB100-250 KB170-350 KB15-30 KB
Product page with cart25-50 KB150-400 KB200-450 KBN/A
Dashboard (highly interactive)80-150 KB200-500 KB250-500 KBN/A

Core Web Vitals (Typical Blog)

MetricAstro 4.0Next.jsGatsby
LCP0.8-1.2s1.5-3.0s2.0-4.0s
FID/INP< 50ms50-200ms100-300ms
CLS00-0.10-0.15
Lighthouse10070-9560-85

Build Performance

Astro 4.0's incremental content caching can reduce build times dramatically for large sites:

Site SizeAstro 4.0 (clean)Astro 4.0 (cached)Next.jsGatsby
100 pages8s2s25s45s
1,000 pages45s8s120s300s
10,000 pages6 min30s15 min45 min

Real-World Use Cases

Blog and Documentation Sites

Astro is purpose-built for content sites. Content collections with Zod schemas validate frontmatter at build time. Built-in Markdown and MDX support enables rich content with embedded components. Automatic RSS feed generation, sitemap creation, and optimized image handling make blogs production-ready with minimal configuration.

// src/pages/rss.xml.ts
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
 
export async function GET(context) {
  const posts = await getCollection('blog');
  return rss({
    title: 'My Blog',
    description: 'Thoughts on web development',
    site: context.site,
    items: posts.map(post => ({
      title: post.data.title,
      pubDate: post.data.date,
      description: post.data.description,
      link: `/blog/${post.slug}/`,
    })),
  });
}

E-Commerce Storefronts

Build e-commerce sites with static product pages (instant loading, CDN-cached) and dynamic server islands for pricing, inventory, and cart functionality. The static shell loads in milliseconds while personalized content streams in progressively.

Marketing and Landing Pages

Marketing pages demand perfect Core Web Vitals for SEO rankings and conversion rates. Astro's zero-JS default produces pages that score 100 on Lighthouse consistently. A/B testing can be implemented through server islands — the static shell remains cached while the variant content is served per-request.

Enterprise Documentation

Large documentation sites with thousands of pages benefit from Astro's incremental content caching and static generation. The content pipeline validates documentation consistency, generates navigation automatically, and supports versioned documentation through content collections.

Best Practices for Production

  1. Ship zero JavaScript by default — Only add client:* directives when genuinely needed. Audit your islands regularly — each one adds JavaScript to the page.

  2. Use client:visible for below-the-fold islands — Defer hydration for components that aren't visible on initial load. This reduces the critical rendering path.

  3. Validate content with Zod schemas — Define schemas for all collections. This catches malformed frontmatter, missing fields, and type errors at build time.

  4. Optimize images with <Image> and <Picture> — Astro's image components handle format conversion (WebP, AVIF), responsive sizing, and lazy loading automatically.

  5. Leverage server islands for mixed content — Static shell + server islands is the optimal architecture for pages with both static and dynamic content.

  6. Pre-render what you can — Even in hybrid or server mode, use export const prerender = true for pages that can be statically generated.

  7. Use content layers for external data — Load content from CMSs, APIs, and databases through content layers for a unified, type-safe API.

  8. Monitor bundle size — Use astro build --analyze to inspect island bundles and ensure no unnecessary JavaScript is shipped.

  9. Implement proper caching headers — Configure CDN caching for static assets and server island responses. Use Cache-Control headers to maximize edge caching.

  10. Enable incremental content caching — For large sites, enable the experimental content collection cache to skip reprocessing unchanged content.

Common Pitfalls and Solutions

PitfallImpactSolution
Adding client:load everywhereShips full framework JS, defeats Astro's purposeUse static rendering by default; hydrate only interactive components
Missing content schemasRuntime errors from malformed frontmatterDefine Zod schemas for all collections
No fallback content on server islandsEmpty space during loadingAlways provide skeleton UI or placeholder content
Overusing islandsIncreased JavaScript bundle sizeEvaluate if each island is truly needed; consider server-rendered alternatives
Ignoring image optimizationSlow LCP, poor Core Web VitalsUse Astro's <Image> component with format and size options
Not pre-rendering in hybrid modeUnnecessary server loadAdd export const prerender = true to static pages
Missing meta tagsPoor SEO and social sharingUse Astro's <head> with proper meta tags and Open Graph data
Importing large libraries client-sideBloated island bundlesUse dynamic imports and code splitting within islands

Debugging Astro Builds

# Verbose build output
astro build --verbose
 
# Analyze bundle sizes
astro build --analyze
 
# Type check content collections
astro check

Use the Astro Dev Toolbar during development to inspect islands, audit accessibility, and identify hydration issues. The toolbar shows which components are interactive, their hydration directives, and their props.

Migration from Other Frameworks

Migrating to Astro is incremental and low-risk due to its multi-framework support:

From Next.js

  1. Install the React integration: npx astro add react
  2. Copy your React components directly into the Astro project
  3. Convert Next.js pages to Astro pages with getStaticPaths()
  4. Replace getStaticProps / getServerSideProps with content collections or Astro.glob()
  5. Add client:* directives to components that need hydration
  6. Remove Next.js-specific routing and replace with Astro's file-based routing

From Gatsby

  1. Migrate GraphQL queries to content collection queries
  2. Replace Gatsby plugins with Astro integrations
  3. Convert MDX content to Astro content collections
  4. Replace Gatsby's <Link> with Astro's <a> tags (Astro handles prefetching automatically)

From WordPress

  1. Export content to Markdown files or connect WordPress as a headless CMS via content layers
  2. Use Astro's WordPress migration guide for automated content extraction
  3. Replace WordPress themes with Astro layouts and components

Comparison with Alternatives

FeatureAstro 4.0Next.js 14RemixSvelteKit11ty
JS by DefaultZeroFull SPAFull SPAFull SPAZero
Islands✓ (native)Partial✗✗✗
Multi-Framework✓✗✗✗✗
Server Islands✓✗✗✗✗
Content Collections✓ (Zod)✗✗✗Plugins
i18n Routing✓ (built-in)✗ (library)✗✗Plugins
Dev Toolbar✓✗✗✗✗
Build Speed★★★★★★★★★★★★★★★★★★★★★
Best ForContent sitesWeb appsFull-stackFull-stackStatic

Future Outlook

Astro is evolving toward full-stack content applications. Server islands are the first step in a broader strategy to enable dynamic, personalized content within static shells. Future developments include:

  • Astro DB: A built-in database layer powered by LibSQL for storing and querying data directly in Astro applications.
  • Actions: Type-safe server functions callable from client-side code, similar to Next.js Server Actions but with Astro's zero-JS philosophy.
  • Sessions: Built-in session management for server islands and on-demand rendered pages.
  • Container API (experimental): Render Astro components outside of HTTP requests — in tests, CLI tools, or email templates.

The convergence of content frameworks and web application frameworks is the defining trend. Astro is uniquely positioned at this intersection — offering the performance of static sites with the interactivity of modern web applications, all while shipping minimal JavaScript to the browser.

Conclusion

Astro 4.0 is the definitive framework for building content-focused websites. Its islands architecture, server islands, content collections, and zero-JS default produce sites that are faster, lighter, and more accessible than any alternative. The framework's multi-framework support, built-in i18n routing, and revolutionary dev toolbar make it a joy to develop with.

Key takeaways:

  1. Astro ships zero JavaScript by default — JavaScript is only added for interactive islands via client:* directives.
  2. Server islands enable mixing static and dynamic content on the same page without sacrificing cacheability.
  3. Content collections with Zod schemas provide type-safe, validated content management from any source.
  4. The Dev Toolbar provides built-in accessibility auditing, island inspection, and third-party extensibility.
  5. Internationalization routing is first-class, with locale-aware URL generation and hreflang support.
  6. Incremental content caching dramatically reduces build times for large sites.
  7. Multi-framework support allows React, Svelte, Vue, and Preact components to coexist on the same page.

Start by creating a new Astro project with npm create astro@latest. Build a blog with content collections to experience the zero-JS default and perfect Lighthouse scores. Then add islands for interactive features and server islands for dynamic content. Astro 4.0 delivers the best developer experience and end-user performance in the content-focused web framework space.