Server Islands
The page shell is rendered once and ISR-cached; the personalized fragment is rendered separately by a force-dynamic route and swapped in client-side. So one dynamic block no longer forces the whole route to force-dynamic — the shell stays on the cache hot path and only the hole is fetched per request.
Tip
Without server islands, a single personalized block (logged-in menu, cart, an A/B test) forces
force-dynamic on the whole page → no ISR caching at all. Here the cached shell holds a placeholder; the inline loader fetches the /api/server-islands-frag fragment per request. Refresh and the fragment's timestamp + token change every time, while the surrounding shell stays served from cache.Output
The fragment below is loaded by a tiny inline script after the shell renders. Refresh and it changes every time; the page around it stays cached.
import { Raw, ServerIsland } from "@bext-stack/framework";
// The page shell is ISR-cached (the demo site renders mode = "isr").
// Only the island below is dynamic — fetched per request.
export default function Page() {
return (
<div>
<h1>My (cached) page shell</h1>
<Raw
html={ServerIsland({
src: "/api/server-islands-frag",
fallback: "<em>loading…</em>",
on: "load", // or "visible" | "idle"
})}
/>
</div>
);
}// src/app/api/server-islands-frag/route.ts
// The dynamic hole. MUST live under /api/ — a page.tsx shadows a
// nested-sibling route.ts, so the fragment gets its own segment.
export const dynamic = "force-dynamic";
export function GET() {
const now = new Date().toISOString();
const n = Math.floor(Math.random() * 100000);
return new Response(
"<strong>Personalized server island</strong> — rendered " + now +
", token " + n + ".",
{
headers: {
"content-type": "text/html; charset=utf-8",
"cache-control": "no-store",
},
},
);
}