Install the Studio SDK

View as Markdown
Last updated July 17, 2026

The Studio SDK is the visual editor bridge: studioSdk, <StudioCanvas />, <StudioComponent />, plus the hooks and registration APIs you'll use day-to-day.

This page is the self-contained chapter for the SDK. Install at the top; everything else you need (registration, fetching, mounting, troubleshooting) follows in the order you actually do them.

On this page: - Prerequisites

  1. Install
  2. Initialize studioSdk
  3. Register your components
  4. Fetch a composition with useCompositionData
  5. Mount <StudioCanvas /> and <StudioComponent />
  6. Common patterns
  7. Troubleshoot

Prerequisites

@contentstack/studio-react builds on the two SDKs from the earlier steps in this chapter. Complete both before continuing:


1. Install

npm install @contentstack/studio-react

2. Initialize studioSdk

Add the init call to src/lib/contentstack.ts so it runs once, at app boot, alongside Delivery + Live Preview:

import Contentstack from "@contentstack/delivery-sdk";
import ContentstackLivePreview from "@contentstack/live-preview-utils";
import { studioSdk } from "@contentstack/studio-react";

export const stack = Contentstack.stack({ /* … */ });

ContentstackLivePreview.init({ /* … */ });

studioSdk.init({
  stackSdk:       stack,
  // contentTypeUid: "compositions",   // set once you've created a Studio project
});

export { ContentstackLivePreview, studioSdk };

studioSdk.init options

OptionRequiredWhat it does
stackSdkThe Delivery SDK instance you created earlier. The Studio SDK reads content through it.
contentTypeUidonce you have a projectThe content type Studio writes composition records into. Set this once your Studio project is created (usually "compositions").

Init is side-effect: it registers Studio's internal components and wires up the messaging bridge for the canvas iframe. Import @/lib/contentstack somewhere in your app shell so the init runs.

3. Register Your Components

Studio's palette in the canvas needs to know about your components. Without registering, authors can only drop Studio's defaults.

registerComponent: single

import { registerComponent } from "@contentstack/studio-react";
import { Button } from "@/components/Button";
import buttonIcon from "@/assets/icons/button.svg";

registerComponent({
  type:        "Button",
  displayName: "Button",
  thumbnailUrl:        buttonIcon,
  component:   Button,
  props: {
    label: { type: "string", defaultValue: "Click me" },
    href:  { type: "href",   defaultValue: "#" },
    size:  {
      type:         "choice",
      options:      ["small", "medium", "large"],
      defaultValue: ["medium"],
    },
  },
});

registerComponents: many at once

import { registerComponents } from "@contentstack/studio-react";

registerComponents([
  { type: "Button",  displayName: "Button",   component: Button,  thumbnailUrl: …, props: { … } },
  { type: "Card",    displayName: "Card",     component: Card,    thumbnailUrl: …, props: { … } },
  { type: "Hero",    displayName: "Hero",     component: Hero,    thumbnailUrl: …, props: { … } },
]);

registerLazyComponent: code-split a component

For large component libraries, register a loader instead of the component itself. Studio holds the schema (thumbnailUrl, displayName, props) eagerly so it can render the palette tile; the actual component code loads only when an author drops it or a visitor's page renders it.

import { registerLazyComponent } from "@contentstack/studio-react";
import heroIcon from "@/assets/icons/hero.svg";

registerLazyComponent(
  {
    type:        "Hero",
    displayName: "Hero",
    thumbnailUrl:        heroIcon,
    props: {
      title:    { type: "string",   defaultValue: "Welcome" },
      subtitle: { type: "string",   defaultValue: "" },
      image:    { type: "imageurl", displayName: "Hero image" },
    },
  },
  () => import("@/components/Hero").then((m) => m.Hero),
);

Under the hood Studio wraps the loader in React.lazy + <Suspense fallback={null}>. Your bundler splits the code; the schema is always available.

When to reach for it: - You have a large component library and bundle size matters - A component has heavy dependencies (chart libs, rich-text editors) you don't want in the initial bundle - You want per-route code splitting and Studio's canvas to play nicely

Eager registerComponent is the right default; use registerLazyComponent when bundle size becomes a real concern.

For the full prop-schema shape (text, image, link, color, slot, etc.), see Component schema.

4. Fetch a Composition with useCompositionData

useCompositionData is the hook your templates and pages use to fetch a composition's spec from Contentstack.

Basic shape

import { useCompositionData } from "@contentstack/studio-react";

function BlogPostPage({ slug }) {
  const { specOptions, isLoading, error, refetchSpec } = useCompositionData(
    { url: `/blog/${slug}` },   // query: how to find the composition
  );

  if (isLoading) return <Loading />;
  if (error)     return <Error message={error.message} />;

  return <StudioComponent specOptions={specOptions} />;
}

Query shapes: five accepted forms

How you tell Studio which composition to fetch:

ShapeUse when
{ compositionUid }You already know the composition's UID (fastest). The only path for Freeform compositions.
{ url }You only have the URL pattern. Works, but processes all compositions to find a match (slower).
{ compositionUid, url }UID is used for the fetch; URL is preserved for diagnostics and iframe context.
{ templateContentTypeUid }Fetch by content type; returns the first matching composition (limit 1).
{ url, templateContentTypeUid }Use the content type for the composition lookup; URL is preserved for context.

The priority resolution: compositionUid > templateContentTypeUid > url.

Options: locale, variantAlias, templateEntryUid

const { specOptions, isLoading } = useCompositionData(
  { compositionUid: "blt123" },
  {
    locale:           "fr",                          // override default locale
    variantAlias:     "winter-sale",                 // fetch a specific variant
    templateEntryUid: "blt456",                      // exact entry from the connected CT
    extendQuery: {
      blog_post: { includeReferences: ["author"] },  // per-CT query extensions
    },
  },
);
OptionWhat it does
localeOverride the default stack locale for this query. Auto-fallback via Contentstack's native fallback.
variantAliasFetch a specific variant (e.g. "winter-sale").
templateEntryUidFetch this exact entry from the connected content type, instead of resolving by URL or grabbing the first entry. Priority: templateEntryUid > url > first entry.
extendQueryPer-CT additions: includeReferences, only field projection.
fetchCompositionPer-query override that wraps or replaces the default composition fetch.
fetchTemplateEntryPer-query override for the template entry fetch.

What it returns

FieldTypeMeaning
specOptionsobject | nullThe full rendering payload: pass to <StudioComponent specOptions={specOptions} />. Contains spec, fetchOptions, hasSpec, hasTemplate, seo.
isLoadingbooleanTrue while fetching
errorunknown | nullSet if the fetch failed
refetchSpec() => voidRe-run the fetch (e.g. after a content edit)
refetchData() => voidRe-fetch only the data layer (entries) without re-fetching the spec

Custom fetchers (fetchComposition, fetchTemplateEntry) are advanced; they let you intercept Studio's content fetches to wrap, cache, or replace. The default works for almost everything.

Server-side fetch (SSR / RSC / SSG)

useCompositionData is a hook (client-only). For server-side rendering, use sdk.fetchCompositionData(queryOptions, options) instead:

import { studioSdk } from "@/lib/contentstack";

// Server-side (e.g. in a Server Component or getServerSideProps)
const specOptions = await sdk.fetchCompositionData({
  url:         "/blog/ai-101",
  searchQuery: requestQueryString,   // required — see why below
});

// Pass to client component
<StudioComponent specOptions={specOptions} />

The searchQuery argument is required on the server because there's no window.location.search to read. Studio uses it to detect whether the request is happening inside Studio's iframe and to honor locale / variant / preview overrides Studio passes via URL params.

For the full SSR / RSC / SSG patterns including framework-specific code, see CSR vs SSR.

5. Mount <StudioCanvas /> and <StudioComponent />

These are NOT interchangeable. They go on different routes.

ComponentRoutePurpose
<StudioCanvas />A single dedicated canvas route in your app (e.g. /canvas)Where Studio previews Sections in isolation
<StudioComponent />Your real page routes (/blog/[slug], /contact-us, etc.)Renders templates as actual pages for visitors

A typical app ships both.

Mount <StudioCanvas /> on the canvas route

// app/canvas/page.tsx (Next.js App Router)
import { StudioCanvas } from "@contentstack/studio-react";

export default function CanvasRoute() {
  return <StudioCanvas />;
}

That's it. Studio drives the rest via URL params on the iframe.

Once mounted, head to Studio, Project Settings, Configuration and set Canvas URL to this route's path (covered in Section preview route).

Mount <StudioComponent /> on your template preview routes

// app/blog/[slug]/page.tsx (Next.js App Router)
import { useCompositionData, StudioComponent } from "@contentstack/studio-react";

export default function BlogPostPage({ params }) {
  const { specOptions, isLoading, error } = useCompositionData(
    { url: `/blog/${params.slug}` },
  );

  if (isLoading) return <Loading />;
  if (error)     return <Error message={error.message} />;
  return <StudioComponent specOptions={specOptions} />;
}

You'll do this for each route that renders a Studio-built template: /blog/[slug], /products/[sku], /contact-us, etc.

If the URL pattern doesn't match a composition, you'll see an in-product "Template did not load" error with a clear message. See Troubleshoot.

6. Common Patterns

Loading + error states

const { specOptions, isLoading, error } = useCompositionData({ url });

if (isLoading) return <Skeleton />;
if (error)     return <Error message={error.message} retry={refetchSpec} />;
return <StudioComponent specOptions={specOptions} />;

Refetch on a content update

useEffect(() => {
  ContentstackLivePreview.onEntryChange(refetchSpec);
}, [refetchSpec]);

Wire Live Preview's onEntryChange to the hook's refetchSpec so the page re-renders when authors save edits.

Locale switching

const [locale, setLocale] = useState("en-us");
const { specOptions, isLoading } = useCompositionData(
  { url },
  { locale },
);

return (
  <>
    <button onClick={() => setLocale("fr")}>FR</button>
    {!isLoading && <StudioComponent specOptions={specOptions} />}
  </>
);

Variant preview (Personalization)

const { specOptions } = useCompositionData(
  { compositionUid: "blt123" },
  { variantAlias: "winter-sale" },
);

Per-entry preview

When a single linked template needs to preview a specific entry (rather than the first one):

const { specOptions } = useCompositionData(
  { templateContentTypeUid: "blog_post" },
  { templateEntryUid: "blt456" },
);

7. Troubleshoot

SymptomWhat to check
"No Canvas URL Found" in Studio when opening a sectionProject Settings → Configuration → set the Canvas URL field
"Environment Not Configured"Pick an Environment + Language in Project Configuration
"Template did not load" when opening a linked templateThe composition's URL pattern doesn't match a route on your site. Either fix the pattern or add the route.
Canvas loads blankYour canvas route exists but doesn't mount <StudioCanvas />; add the snippet from step 5
401 fetching contentStack API Key or Delivery Token wrong; copy them again from Stack → Settings → Tokens
Preview doesn't update in real timeLive Preview not initialised, or onEntryChange not wired; see Install Live Preview
Components don't appear in the paletteRegister them via registerComponent / registerComponents before the canvas iframe loads
Lazy components flash blank<Suspense fallback={null}> is the default; provide your own fallback if needed

For the full diagnostic tree, see Troubleshoot.

Optional: Install with the CLI Skill

The studio-skills CLI installs and wires the Studio SDK automatically:

npx @contentstack/studio-skills install

Then ask your LLM: "install Studio in this project". The skill detects your framework, installs all three SDKs, prompts for your stack credentials, and writes the init code to src/lib/contentstack.ts. It also registers a starter set of components and creates the canvas route.

The SDK side is done. The Studio web app side comes next.

Create a Studio project