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 Framework: Content-First Site Builder

Build content-focused sites with Astro: islands architecture, partial hydration, static generation.

AstroSSGIslandsFrontend

By MinhVo

Introduction

The JavaScript ecosystem has a content problem. Modern frameworks like Next.js, Remix, and SvelteKit are excellent for building web applications, but they bring unnecessary complexity and JavaScript overhead to content-focused websites. A blog post does not need client-side routing, state management, or a 200KB JavaScript bundle. It needs fast HTML, excellent SEO, and the ability to add interactive elements where they genuinely enhance the user experience. Astro was designed from the ground up to solve this problem.

Content-first web development

Astro's core innovation is the islands architecture. Instead of treating every page as a single client-side application, Astro renders pages as static HTML and identifies small "islands" of interactivity that need JavaScript. Each island is an independent component that hydrates on its own schedule — immediately on page load, when scrolled into view, when the browser is idle, or when a specific media query matches. This produces websites that load in milliseconds and score 100 on Lighthouse.

The framework supports multiple UI frameworks (React, Vue, Svelte, Solid, Preact, Lit) on the same page. This is not a gimmick — it solves real problems for teams migrating between frameworks, teams with diverse expertise, or projects where different interactive components are best implemented in different tools. A documentation site might use a React search widget, a Vue table of contents, and a Svelte code playground, all within the same Astro page.

Understanding Astro: Core Concepts

The Islands Architecture

The islands architecture, sometimes called "partial hydration," is the foundation of Astro's design. In a traditional SPA framework, the entire page is hydrated as a single application. Every component initializes, every state tree is created, and every event listener is attached — even for components that the user may never interact with. This wastes bandwidth, CPU time, and battery life.

Astro takes a different approach. Components are rendered to static HTML on the server by default. Only components explicitly marked as interactive are hydrated on the client. Each hydrated component is an independent "island" that runs its own JavaScript without affecting the rest of the page.

Client Directives

Client directives control when and how islands are hydrated:

  • client:load — Hydrate immediately on page load. Use for above-the-fold interactive components like navigation menus with mobile toggles or hero sections with animations.
  • client:idle — Hydrate when the browser finishes its initial work (via requestIdleCallback). Use for components that should be interactive soon but not immediately.
  • client:visible — Hydrate when the component enters the viewport (via IntersectionObserver). Use for below-the-fold content like comments, related posts, and interactive widgets.
  • client:media — Hydrate when a CSS media query matches. Use for components that only need interactivity on certain screen sizes.
  • client:only — Skip server rendering entirely and render only on the client. Use for components that depend on browser APIs like localStorage, WebGL, or window.

Content Collections

Content collections provide a structured way to manage content in Astro. Define collections in src/content/config.ts with Zod schemas that validate frontmatter at build time. This catches errors early and provides type-safe access to content data.

Static and Server Rendering

Astro supports three rendering modes:

  • static — All pages are pre-rendered at build time. This is the default and produces the fastest possible sites.
  • server — All pages are rendered on each request (SSR). Required for dynamic features like authentication and personalization.
  • hybrid — Most pages are pre-rendered, with specific routes opted into server rendering.

Islands architecture visualization

Architecture and Design Patterns

The Content Pipeline Pattern

Define content collections with schemas, organize content into logical groups, and use Astro's built-in APIs to query and render content. This creates a robust, type-safe content pipeline that validates data at build time.

The Multi-Framework Island Pattern

Use different frameworks for different interactive components based on their strengths. React for complex state management, Svelte for lightweight interactions, Vue for form-heavy components. Each island hydrates independently.

The Progressive Enhancement Pattern

Build pages as static HTML first. Add interactivity through islands only where it enhances the user experience. The page works perfectly without JavaScript — islands are an enhancement, not a requirement.

The Layout Composition Pattern

Use Astro's slot system to compose layouts. A base layout provides the HTML shell, navigation, and footer. Page-specific layouts add structure. Components fill slots with content. This creates a flexible, maintainable layout hierarchy.

Step-by-Step Implementation

Project Setup

npm create astro@latest my-site
cd my-site
npm install
// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import mdx from '@astrojs/mdx';
import tailwind from '@astrojs/tailwind';
 
export default defineConfig({
  integrations: [react(), mdx(), tailwind()],
  output: 'static',
});

Defining Content Collections

// src/content/config.ts
import { defineCollection, z } from 'astro:content';
 
const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    date: z.coerce.date(),
    tags: z.array(z.string()),
    published: z.boolean().default(true),
    author: z.string().default('MinhVo'),
  }),
});
 
const projects = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    tech: z.array(z.string()),
    url: z.string().url().optional(),
    github: z.string().url().optional(),
    featured: z.boolean().default(false),
  }),
});
 
export const collections = { blog, projects };

Creating Blog Pages

---
// src/pages/blog/[...slug].astro
import { getCollection } 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, headings } = await post.render();
---
 
<BlogLayout title={post.data.title} date={post.data.date} headings={headings}>
  <Content />
</BlogLayout>

Adding Interactive Islands

---
// src/pages/blog/[...slug].astro
import SearchBar from '../../components/SearchBar.tsx';
import Comments from '../../components/Comments.tsx';
import ShareButtons from '../../components/ShareButtons.svelte';
---
 
<BlogLayout title={post.data.title}>
  <Content />
  
  <!-- Hydrate when scrolled into view -->
  <ShareButtons url={Astro.url.href} client:visible />
  
  <!-- Hydrate when browser is idle -->
  <Comments postId={post.slug} client:idle />
</BlogLayout>

Image Optimization

---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---
 
<Image
  src={heroImage}
  alt="Hero image"
  widths={[400, 800, 1200]}
  sizes="(max-width: 768px) 100vw, 800px"
  format="webp"
  quality={80}
/>

Web development workspace

Real-World Use Cases

Developer Documentation Sites

Astro is the framework of choice for documentation sites. Its MDX support enables rich content with embedded components. Content collections provide structured navigation and search. Static generation ensures instant page loads. The Astro docs themselves, along with documentation for Cloudflare, Google, and many open-source projects, are built with Astro.

Personal Blogs and Portfolios

For personal blogs, Astro provides the perfect balance of simplicity and power. Write posts in Markdown, define frontmatter schemas for validation, and deploy to any static host. Add interactive elements like search, comments, and analytics as islands that hydrate on demand.

Marketing and Landing Pages

Marketing pages need to be fast for SEO and conversion. Astro's zero-JS default produces pages that load instantly. Add interactive hero animations, form submissions, and A/B testing as islands. The static shell loads immediately while interactive enhancements stream in.

News and Media Sites

News sites need fast load times (readers are impatient), excellent SEO (discovery through search), and the ability to embed interactive elements (data visualizations, polls, comment sections). Astro delivers all of this with its content-first approach.

E-Commerce Product Pages

Product pages benefit from Astro's hybrid rendering. The product description, images, and specifications are statically generated for instant loads. The add-to-cart button, pricing (which may be personalized), and reviews section are rendered as interactive islands.

Best Practices for Production

  1. Default to static rendering — Only use SSR or hybrid rendering when you have a genuine need for dynamic content. Static pages are faster, cheaper to host, and easier to cache.

  2. Use client:visible as your default directive — Most interactive components don't need to hydrate immediately. Deferring hydration to when the component is visible reduces initial page load time significantly.

  3. Define schemas for all content collections — Zod schemas catch errors at build time, provide type safety in templates, and serve as documentation for your content structure.

  4. Optimize images aggressively — Use Astro's <Image> component with explicit widths, sizes, and format attributes. Serve WebP or AVIF with JPEG fallback. Lazy-load images below the fold.

  5. Implement proper SEO — Use <head> slots to inject Open Graph tags, Twitter cards, canonical URLs, and structured data. Content collections make it easy to pull metadata from frontmatter.

  6. Use MDX for rich content — MDX lets you embed interactive components directly in Markdown content. Use it for callouts, code playgrounds, interactive examples, and custom components.

  7. Leverage view transitions — Add <ViewTransitions /> to your layout for smooth page navigation. This dramatically improves perceived performance for content sites with multiple pages.

  8. Monitor JavaScript budgets — Use Astro's build analysis to track how much JavaScript each island adds. Set budgets and alerts to prevent JavaScript bloat over time.

Common Pitfalls and Solutions

PitfallImpactSolution
Using client:load on every componentShips unnecessary JavaScriptDefault to client:visible or client:idle
Not defining content schemasRuntime errors from malformed frontmatterDefine Zod schemas for all collections
Forgetting image optimizationSlow page loads, poor Core Web VitalsUse <Image> component with responsive sizes
Importing components without client directivesInteractive features silently failAlways add client:* to interactive components
Using SSR when static would sufficeSlower pages, higher hosting costsDefault to static, opt into SSR per-route
Not using view transitionsJarring full-page reloads between pagesAdd <ViewTransitions /> to base layout
Over-engineering the content pipelineComplexity without benefitStart simple, add complexity as needed

Performance Optimization

Astro's performance advantage is structural, but there are additional optimizations. Use content caching to skip rebuilds for unchanged content. Implement prefetching to load linked pages before users click. Use the <Image> component with explicit dimensions to prevent layout shifts.

For large sites, implement pagination in getStaticPaths to avoid building thousands of pages simultaneously:

export async function getStaticPaths({ paginate }) {
  const posts = await getCollection('blog');
  return paginate(posts, { pageSize: 20 });
}

Comparison with Alternatives

FeatureAstroNext.jsGatsby11tyHugo
Default JS0 KB85+ KB100+ KB0 KB0 KB
IslandsFirst-classPartialNoNoNo
Multi-frameworkYesNoNoNoNo
Content collectionsBuilt-inNoGraphQLManualBuilt-in
SSR supportYesYesYesNoNo
View transitionsBuilt-inManualNoNoNo
Build speedFastMediumSlowFastVery fast

Advanced Patterns

Custom Integrations

Astro's integration API lets you extend the build process:

// my-integration.ts
export default function myIntegration() {
  return {
    name: 'my-integration',
    hooks: {
      'astro:config:setup': ({ updateConfig }) => {
        updateConfig({
          vite: {
            plugins: [myVitePlugin()],
          },
        });
      },
      'astro:build:done': ({ pages }) => {
        // Generate sitemap, RSS feed, search index
        generateSitemap(pages);
        generateRSS(pages);
        generateSearchIndex(pages);
      },
    },
  };
}

Dynamic OG Image Generation

Generate Open Graph images at build time using @vercel/og or Sharp:

// src/pages/blog/[slug]/og.png.ts
import { generateOgImage } from '../../lib/og';
 
export async function GET({ props }) {
  const image = await generateOgImage({
    title: props.post.data.title,
    author: props.post.data.author,
  });
 
  return new Response(image, {
    headers: { 'Content-Type': 'image/png' },
  });
}

Future Outlook

Astro is evolving toward full-stack content applications. Server Islands (rendering individual page fragments on the server within static pages) blur the line between static and dynamic. Content Layers unify content from any source under a single API. The View Transitions API is stabilizing and will become the default navigation mode.

The broader ecosystem is converging on Astro's philosophy. React Server Components, Solid Start's islands, and SvelteKit's universal load functions all adopt similar "server-first, hydrate selectively" patterns. Astro pioneered this architecture and continues to push it forward.

Deployment Options

Astro generates static HTML by default, which means it can be deployed to virtually any hosting provider. For static sites, the build output is a folder of HTML, CSS, and JavaScript files that can be served from a CDN with no server runtime required.

Vercel: The @astrojs/vercel adapter provides zero-configuration deployment with automatic preview deployments for pull requests, edge middleware, and serverless functions for SSR routes. Vercel's global CDN ensures fast load times worldwide.

Cloudflare Pages: The @astrojs/cloudflare adapter deploys to Cloudflare's edge network, running SSR routes on Cloudflare Workers. This is the fastest option for global audiences because Cloudflare's edge network has more points of presence than any other provider.

Netlify: The @astrojs/netlify adapter supports both static and SSR deployments with serverless functions. Netlify's form handling and identity features integrate well with Astro's content-focused approach.

Self-hosted: For SSR or hybrid sites, build with the Node adapter and deploy to any Node.js hosting environment. Docker containers, VPS servers, and platform-as-a-service providers all work:

# Build for production
npm run build
 
# Preview locally
npm run preview
 
# Deploy static files to any CDN
# The dist/ folder contains everything needed

Server Islands Deep Dive

Server Islands, introduced in Astro 5, allow individual components on a static page to be rendered on the server at request time. This is useful for personalized content like user avatars, shopping cart counts, or region-specific pricing that cannot be statically generated:

---
// Static page with a server island for the user greeting
import UserGreeting from '../components/UserGreeting.astro';
import StaticHeader from '../components/StaticHeader.astro';
---
 
<html>
  <body>
    <StaticHeader />
    <!-- This component renders on the server at request time -->
    <UserGreeting server:defer>
      <p slot="fallback">Loading...</p>
    </UserGreeting>
    <main>Static content here...</main>
  </body>
</html>

The server:defer directive tells Astro to render this component on the server for each request while keeping the rest of the page statically generated. The fallback slot shows loading content while the server island renders. This pattern gives you the performance of static generation with the personalization of server rendering, without shipping any additional JavaScript to the client.

Migration from Other Frameworks

From Next.js to Astro

Migrating from Next.js to Astro involves converting React components to Astro components and replacing client-side routing with Astro's file-based routing. The key insight is that most Next.js pages are primarily static content with a few interactive elements — perfect for Astro's islands architecture. Convert the static parts to .astro files and keep the interactive components as React islands with appropriate client:* directives.

From Gatsby to Astro

Gatsby's GraphQL data layer maps naturally to Astro's content collections. Export your Gatsby content as Markdown files with frontmatter, define Zod schemas in src/content/config.ts, and use getCollection to query content. Gatsby's plugin ecosystem has Astro equivalents for most functionality (image optimization, SEO, RSS feeds).

From Hugo/Jekyll to Astro

Static site generators like Hugo and Jekyll produce similar output to Astro. The main difference is that Astro uses JavaScript-based templates (.astro files) instead of Go templates or Liquid. The content (Markdown files) transfers directly. The templates need to be rewritten, but the content structure remains the same.

Conclusion

Astro is the best framework for building content-focused websites. Its islands architecture, content collections, and zero-JS default produce sites that are faster, lighter, and more accessible than any alternative.

Key takeaways:

  1. Islands architecture hydrates only interactive components, shipping zero JavaScript by default
  2. Client directives (client:visible, client:idle) control hydration timing for optimal performance
  3. Content collections with Zod schemas validate content at build time
  4. Multiple frameworks (React, Vue, Svelte) can coexist on the same page
  5. Hybrid rendering combines static performance with dynamic flexibility
  6. View transitions provide SPA-like navigation without JavaScript overhead
  7. Image optimization is built in — use <Image> for automatic WebP/AVIF conversion

Start by creating a new Astro project and building a simple blog with content collections. Experience the zero-JS default and perfect Lighthouse scores. Then add islands for interactive features and explore hybrid rendering for dynamic content.