Framework recipe: Astro (React islands)
Frontmatter fetches. Island wrapper renders Studio inside an Astro page.
Verified in-repo: smoke-tested against the Studio Documentation Contentstack project (Astro 5 + @astrojs/node + @astrojs/react). curl /blog/welcome-to-studio returns 200 OK, 19 KB composition body, <style data-studio-ssr> with 180 --token-* properties, populated <title>, zero data-cs-defer-builtin placeholders. Astro island (<astro-island client:load>) serializes the full specOptions for client hydration.
Required Vite config for the server-side rendering (SSR) bundle. Without this, Astro's Node adapter throws SyntaxError: The requested module 'entities/decode' does not provide an export named 'default' because the SDK's HTML-parsing transitive deps are CJS-only. Add to astro.config.mjs:
import { defineConfig } from "astro/config"; import node from "@astrojs/node"; import react from "@astrojs/react"; export default defineConfig({ output: "server", adapter: node({ mode: "standalone" }), integrations: [react()], vite: { ssr: { noExternal: [ "entities", "domelementtype", "domhandler", "domutils", "htmlparser2", "parse5", /^@contentstack\/.*/, ], }, }, });
Read Framework recipes: SSR integration first.
The recipe
---
// src/pages/[...slug].astro
import Layout from "../layouts/Layout.astro";
import StudioIsland from "../components/StudioIsland";
import { getSSRStyleTags, getCompositionMetadata } from "@contentstack/studio-react";
import { sdk } from "../lib/studio.server";
import "../registry"; // Rule 2
const url = new URL(Astro.request.url);
const specOptions = await sdk.fetchCompositionData(
{ url: url.pathname, searchQuery: url.searchParams },
{ locale: "en-us" },
);
if (!specOptions?.spec) {
return Astro.rewrite("/404");
}
const styleTags = getSSRStyleTags(specOptions.spec);
const metadata = getCompositionMetadata(specOptions, { baseUrl: url.origin });
---
<Layout>
<slot name="head" slot="head">
<title>{metadata.title}</title>
<meta name="description" content={metadata.description} />
{metadata.ogImage && <meta property="og:image" content={metadata.ogImage} />}
{metadata.canonical && <link rel="canonical" href={metadata.canonical} />}
<Fragment set:html={styleTags} />
</slot>
<StudioIsland client:load specOptions={specOptions} />
</Layout>Not-found handling is not shown here. This recipe is the CI-tested happy path. fetchCompositionData throws when no composition matches, so as written an unclaimed URL returns a 500, not a 404. Wrap it before shipping. See Rule 5 and the canonical resolveComposition helper.
// src/components/StudioIsland.tsx
import { StudioComponent } from "@contentstack/studio-react";
import "../registry"; // island needs its own root import (Rule 2)
export default function StudioIsland({ specOptions }: { specOptions: any }) {
return <StudioComponent specOptions={specOptions} />;
}Per-island registry init
Each React island in Astro is a separate hydration root. Every island that renders a <StudioComponent /> needs its own import "../registry" at the top. Missing it produces "Internal components missing" at hydration.
Smoke pass: verify before treating this recipe as validated by a full run
This recipe is desk-checked against the CI-tested Node recipe. It has not yet been verified against a real Astro app. Before shipping:
- [ ] Scaffold a fresh Astro project with SSR mode enabled and the React integration installed (npm create astro@latest + npx astro add react).
- [ ] Install @contentstack/studio-react (matching the SDK's examples/nextjs-rsc/package.json version).
- [ ] Copy src/pages/[...slug].astro + src/components/StudioIsland.tsx + src/registry.ts from this recipe.
- [ ] Wire a known-good Studio project with at least one saved composition matching a known URL.
- [ ] Run npx astro dev (SSR mode).
- [ ] Run the Verification curl test against a known composition URL. All three assertions must pass.
- [ ] Confirm the per-island registry init works: check that <StudioIsland client:load> renders correctly without "Internal components missing." If it fails, verify the registry import is at the top of the island's .tsx file.
- [ ] For every additional React island in the app that renders <StudioComponent />, verify import "../registry" is present at the top: every island is a separate hydration root.
- [ ] Run npx astro build && npx astro preview to verify production behavior.
Report deltas as GitHub issues so the doc converges on truth.