References

Hybrid Strategies (SSG + SSR)

Core Patterns

  • Mode Selection: Use output: 'hybrid' to default all pages to SSG with opt-in SSR via export const prerender = false
  • Decision Matrix: Static content (blogs, marketing) stays SSG; user-specific pages (dashboard, cart) use SSR
  • Migration Path: Install adapter, switch output, then mark only dynamic pages with prerender: false
  • Caching SSR: Add Cache-Control headers to SSR endpoints to reduce server load for semi-static data

Mixing static and dynamic rendering, migration paths, and per-page decisions


When to Read This

  • Combining static and dynamic pages in one project
  • Migrating from SSG-only to Hybrid
  • Deciding which pages should be SSG vs SSR
  • Optimizing performance with mixed rendering

Hybrid Mode Setup

// astro.config.mjs
import node from "@astrojs/node";

export default defineConfig({
  output: "hybrid", // Default to SSG, opt-in to SSR
  adapter: node({ mode: "standalone" }), // Adapter REQUIRED
});

Key difference:

  • output: 'server' → All pages SSR by default, opt-in to SSG with prerender: true
  • output: 'hybrid' → All pages SSG by default, opt-in to SSR with prerender: false

Decision Matrix

Page TypeRenderingReason
Homepage, About, PricingSSGStatic content, rarely changes
Blog posts, DocumentationSSGContent-driven, many pages, SEO
Dashboard, ProfileSSRUser-specific data, auth required
Admin panelSSRReal-time data, permissions
Search resultsSSRDynamic queries
Product catalogSSGStatic product data, many SKUs
Cart, CheckoutSSRUser-specific, real-time inventory

Hybrid Patterns

Static Homepage with Dynamic Dashboard

// src/pages/index.astro (SSG by default)
---
const posts = await getPosts(); // Fetched at build time
---
<h1>Welcome</h1>
{posts.map(p => <article>{p.title}</article>)}
// src/pages/dashboard.astro (SSR opt-in)
---
export const prerender = false; // Enable SSR for this page

const user = Astro.locals.user;
if (!user) return Astro.redirect('/login');

const data = await fetchUserData(user.id); // Fetched per-request
---
<h1>Welcome, {user.name}</h1>
// src/pages/blog/[slug].astro (SSG)
---
export async function getStaticPaths() {
  const posts = await getPosts();
  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post },
  }));
}
---
// src/pages/search.astro (SSR)
---
export const prerender = false;

const query = Astro.url.searchParams.get('q');
const results = query ? await searchPosts(query) : [];
---
<form method="GET">
  <input name="q" value={query} />
  <button>Search</button>
</form>
{results.map(r => <div>{r.title}</div>)}

Migration from SSG to Hybrid

Step 1: Install Adapter

npm install @astrojs/node

Step 2: Update Config

// astro.config.mjs
import node from "@astrojs/node";

export default defineConfig({
  output: "hybrid", // Change from 'static'
  adapter: node(),
});

Step 3: Identify Dynamic Pages

---
// Pages that need SSR
export const prerender = false;

// All other pages remain SSG (no change needed)
---

Step 4: Update API Routes

// Before (SSG): Static JSON generation
export const GET: APIRoute = async () => {
  const data = await fetchData();
  return new Response(JSON.stringify(data));
};

// After (Hybrid): Can handle dynamic requests
export const GET: APIRoute = async ({ request, locals }) => {
  const user = locals.user; // Now available
  const data = await fetchUserData(user.id);
  return new Response(JSON.stringify(data));
};

Performance Optimization

Maximize SSG Usage

---
export async function getStaticPaths() {
  const pages = await getAllPages();
  return pages.map(page => ({
    params: { slug: page.slug },
  }));
}
---

Cache SSR Responses

// src/pages/api/trending.ts
export const GET: APIRoute = async () => {
  const data = await getExpensiveData();

  return new Response(JSON.stringify(data), {
    headers: {
      "Cache-Control": "public, max-age=300", // Cache for 5 minutes
    },
  });
};

Common Patterns

Partial Hydration with SSG

---
const staticData = await getStaticData();
---

<div>
  <h1>{staticData.title}</h1>
  <!-- Static HTML -->

  <SearchWidget client:load />
  <!-- Interactive component -->
</div>

API Routes in Hybrid

// Static endpoint (no prerender directive)
// src/pages/api/posts.json.ts
export const GET: APIRoute = async () => {
  const posts = await getPosts();
  return new Response(JSON.stringify(posts));
};

// Dynamic endpoint
// src/pages/api/user/profile.json.ts
export const prerender = false; // SSR required

export const GET: APIRoute = async ({ locals }) => {
  const user = locals.user;
  const profile = await getUserProfile(user.id);
  return new Response(JSON.stringify(profile));
};

Edge Cases

Mixed Data Sources

---
export const prerender = false;

// Static data (could be from build-time)
const categories = await getCategories(); // Could cache this

// Dynamic data (per-request)
const user = Astro.locals.user;
const recommendations = user ? await getRecommendations(user.id) : [];
---

<div>
  <nav>
    {categories.map(c => <a href={`/category/${c.slug}`}>{c.name}</a>)}
  </nav>

  {recommendations.length > 0 && (
    <section>
      <h2>Recommended for you</h2>
      {recommendations.map(r => <article>{r.title}</article>)}
    </section>
  )}
</div>

Environment Variables

---
// SSG pages: Only build-time and PUBLIC_ vars
const buildTime = import.meta.env.BUILD_TIME;
const publicApi = import.meta.env.PUBLIC_API_URL;

// SSR pages: All vars available
export const prerender = false;
const dbUrl = import.meta.env.DATABASE_URL; // ✅ Available in SSR
const secret = import.meta.env.API_SECRET; // ✅ Available in SSR
---

Best Practices

  1. Default to SSG — use SSR only when necessary
  2. Profile SSR response times; optimize or cache
  3. Keep static content in SSG, dynamic data in SSR
  4. Use client directives to add interactivity to SSG pages without SSR
  5. Use HTTP caching for SSR endpoints
  6. SSR uses server resources; SSG is essentially free

References