Skip to content

Web Development

Astro Server Islands + Actions — Dynamic Content in Static-First Sites

Astro 5 ships server islands: dynamic HTML chunks rendered on the server and dropped into otherwise static pages. Pair that with Astro Actions (RPC-style server functions), and you can build marketing sites that are 99% static but feel fully dynamic. User-specific quotes, live counters, conditional CTAs. Zero JavaScript hydration. Real-time data. This is the pattern every Velocity site should use.

🏝️ ⚙️

For years, the choice was binary: build a static site (fast, boring, no personalization) or build an app (dynamic, bloated, slow). Astro just split the difference. With server islands, you can render dynamic HTML chunks on the server and inject them into an otherwise static page. The page is still static HTML. The browser still renders it instantly. But parts of it are dynamic — personalized, real-time, pulling from your database. It's the best of both worlds. And when you add Astro Actions (server functions that act like RPC calls from the client), you can trigger real-time updates without a separate API layer. One example: a marketing page that's static for SEO, a server island that shows the logged-in user's personalized quote, and an action that logs their engagement. Ship static HTML in 50ms. Hydrate zero JavaScript. Update in real time.

The trick is knowing when to use each tool. Astro Pages are static. Astro Server Islands are dynamic HTML rendered on the server. Astro Actions are server functions callable from client or server. Most teams either build everything as a full SSR app (overly complex) or build everything static (missing real-time). The fast path: static page shell + one server island for the dynamic part + actions to wire up real-time updates. That's the Velocity pattern. It's what I use across every brand site. And it ships faster than either approach alone.

Server Islands vs Full SSR — What's the Difference?

In a traditional SSR app (Next.js, Remix), every page is dynamic. Every route re-renders on the server. Every request hits a runtime. If you have 10K concurrent users, you need infrastructure to scale. Server cost grows with traffic. Rendering cost stays linear.

In Astro with server islands, the page is pre-built at deploy time. It's static. It lands in a CDN. Requests hit the CDN first — instant. But the server island re-renders on every request. Only the small dynamic chunk hits the server. So you scale the static layer infinitely (CDN is free), and the dynamic layer handles only the pieces that change. A marketing page with one user-specific quote? The page is 99% CDN-cached HTML. The quote is 1% server-rendered. You pay for 1% of the compute.

This is why server islands win for marketing sites. You get the SEO benefits of static HTML (pre-built pages, fast Core Web Vitals), the UX benefits of dynamic content (personalization, real-time data), and the cost benefits of hybrid rendering (scale the cheap layer, pay only for the dynamic layer).

Astro Actions — RPC-Style Server Functions

Astro Actions let you define server functions and call them from the client like you're calling a local function. No REST API. No request/response ceremony. Just `import { myAction } from '@/lib/actions'` and `await myAction({ arg: value })`.

Define an action in `src/lib/actions.ts`:

// src/lib/actions.ts
import { defineAction } from 'astro:actions';

export const server = {
  logEngagement: defineAction({
    input: z.object({ quoteId: z.string() }),
    handler: async ({ quoteId }) => {
      await supabase
        .from('engagement')
        .insert({ quote_id: quoteId, timestamp: new Date() });
      return { success: true };
    },
  }),
};

Call it from a client component:

// src/components/QuoteCard.tsx
import { actions } from 'astro:actions';

export default function QuoteCard({ quote }) {
  const handleClick = async () => {
    await actions.server.logEngagement({ quoteId: quote.id });
  };
  return (
    <button onClick={handleClick}>
      {quote.text}
    </button>
  );
}

Or call it server-side from a server island:

// src/components/PersonalizedQuote.astro
import { actions } from 'astro:actions';

const userId = Astro.cookies.get('userId')?.value;
const quote = await actions.server.getQuoteForUser({ userId });
---

<div>{quote.text}</div>

That's it. Typed, type-safe, end-to-end. The server function runs on the server. The client function call is validated. The response is validated. No REST schema drift. No undefined behavior.

Three Patterns — User Quote, Live Counter, Conditional CTA

Pattern 1: Personalized User Quote

A marketing page is static. But if a logged-in user visits, they should see their own testimonial or a quote from someone in their cohort. Build a server island:

// src/components/PersonalizedQuote.astro
---
import { getQuoteForUser } from '@/lib/data';

const userId = Astro.cookies.get('userId')?.value;
const quote = userId
  ? await getQuoteForUser(userId)
  : null;
---

{quote ? (
  <blockquote>
    <p>{quote.text}</p>
    <footer>— {quote.author}</footer>
  </blockquote>
) : (
  <blockquote>
    <p>"Generic testimonial for anonymous users."</p>
  </blockquote>
)}

The page is static. This island is dynamic. It checks for a user ID, fetches the quote, and renders the HTML. No JavaScript. No hydration. The user sees their personalized quote in the initial HTML. If you have 1000 unique users, the page is built once, the island renders 1000 different outputs, all served as static HTML from the CDN. Cost of personalization is near-zero because the page was pre-generated.

Pattern 2: Live Counter (with Revalidation)

Show a real-time stat: "32,481 projects shipped with Velocity." Update it every 5 minutes:

// src/components/LiveCounter.astro
---
import { getProjectCount } from '@/lib/data';

export const preload = false; // Don't cache this island

const count = await getProjectCount();
---

<div class="stat">
  <div class="number">{count.toLocaleString()}</div>
  <div class="label">Projects shipped</div>
</div>

Set `preload = false` to bypass static generation. On each request, the server fetches the live count and renders it. Pair with cache headers to revalidate every 5 minutes. The HTML is always fresh, but Cloudflare (or your CDN) caches it for 5 minutes, so the cost is negligible.

Pattern 3: Conditional CTA (Different for Logged-In vs Anonymous)

Show different CTAs based on whether the user is logged in:

// src/components/ContextualCta.astro
---
const userId = Astro.cookies.get('userId')?.value;
---

{userId ? (
  <button>Go to Dashboard</button>
) : (
  <button>Start Free Trial</button>
)}

The page is static. This island checks the auth cookie and renders the right CTA. Zero JavaScript. No client-side conditional rendering. No hydration. The user sees the correct button immediately in the initial HTML.

Caching — When to Revalidate and When to Cache

Server islands have a few caching strategies:

  • Preload = true (default): Render at build time, cache forever. Good for static content.
  • Preload = false: Render on every request. Good for live data, user-specific content.
  • Cache headers: Render on request, cache the HTML at the CDN level. Good for data that updates infrequently (every 5 min, hourly, daily).

For a personalized quote, use `preload = false` — the island renders on every request, checks the user ID, and returns their specific quote. For a live counter, use `preload = false` + cache headers (5 min TTL) — the island renders once, the CDN caches the HTML for 5 minutes, then it re-renders. For a static testimonial that never changes, use `preload = true` — it renders at build time and never changes.

The cost difference is huge. A site with 1K QPS of anonymous traffic and one dynamic island that fetches from Supabase:

  • Full SSR (render every request): 1K database hits/second. Database cost skyrockets.
  • Server island + 5 min cache: 12 database hits/second. 1000× cheaper.
  • Static + server island + preload = false + user-specific query (indexed): 100 database hits/second, fast response.

The move: cache as aggressively as you can without losing freshness. A live counter that updates every 5 minutes is better than real-time. Real-time is better than nothing. But real-time + no cache = your database on fire.

Six FAQs

Can I use server islands with a static site host like Netlify?

Yes, with caveats. If all your islands have `preload = true`, you're still static — everything pre-renders at build time. If you need `preload = false` (dynamic on every request), you need a runtime. Netlify Functions or Netlify Edge Functions can handle this. Astro ships adapters for both. Configure your adapter and your islands run on Netlify infrastructure.

What's the difference between a server island and a React island with `client:load`?

React islands ship JavaScript to the client and hydrate. Server islands render HTML on the server and ship zero JavaScript. React islands are interactive. Server islands are static HTML. Use server islands for dynamic content that doesn't need client-side interaction. Use React islands for buttons, forms, dropdowns — things the user clicks.

Can I pass props to a server island from a client component?

No. Server islands render on the server before the client JavaScript loads. If you need to pass data from a client component to a server island, use an Astro Action to update server state, then re-fetch the island data. Or restructure: make the client component call an action, the action updates the database, the server island queries the database. The island re-renders on the next request.

How do I revalidate a server island without rebuilding the entire site?

Set `preload = false` and rely on HTTP cache headers. Or use Supabase's `revalidatePath()` equivalent (depending on your adapter). Most adapters don't support granular revalidation yet — it's on the roadmap. For now, cache + TTL is your friend.

Can server islands call database queries directly?

Yes. They're Astro components running on the server. You can import your Supabase client and query directly. No extra API layer. Keep in mind: every render hits the database (unless you cache). Index your queries and use connection pooling.

What if I need real-time updates without reloading the page?

Use a React island for the real-time piece (subscribe to Supabase Realtime, update on changes), keep the static shell static, and the server islands static. Or use WebSockets + Astro Actions. The pattern: static page + server island for initial render + React island for real-time updates. Three layers, each optimized for its job.

The Bottom Line

Astro server islands are the fastest way to add dynamic content to a static site. Render personalized chunks on the server. Cache aggressively. Pay only for what changes. The page is still fast, the user sees personalized content, and the database doesn't melt. This is how Velocity sites stay under 50kb bundle size while shipping fully dynamic experiences. Marketing page is static. User quote is a server island. CTA is conditional. Counter is live. Zero JavaScript overhead. Every visitor sees custom content in the initial HTML. Build time is measured in milliseconds. Core Web Vitals are perfect. Conversion rate climbs.

Pair server islands with Astro Actions and you've got the full stack: static HTML, dynamic server rendering, RPC-style client-server communication, type-safe end-to-end. No REST API. No schema drift. No undefined behavior. It's the future of marketing sites.

Ready to build a Velocity site that converts? Check out Aidxn Design's web services for a performance-first site, or dig deeper into when to hydrate islands with client directives to squeeze out even more speed.

Let us make some quick suggestions?
Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.