Build the Studio Page example: full walkthrough
Extracted from The Studio page to keep that page focused on the concepts. This is the step-by-step build of the example (three Sections composed into one Connected Template) that the overview walks you through conceptually.
Prereq: you've read The Studio page Steps 1-3 (how the page works, prerequisite, meet Slots). This recipe picks up at Step 4.
Step 4: build Section 1, the Hero (Simple)
Time to open Studio. In a browser, open your Studio project, then Compositions, then the Sections tab. This is where you'll build the three Sections. The UI action names below (+ New Section, + Add Section Slot, etc.) are the canonical operations. Exact wording may vary slightly across Studio versions. The Sections chapter has the version-specific reference.
The hero is one thing. No iteration. Perfect first Section.
Section: blog_post_hero (Simple — root is a component)
<Hero>
bindings:
headline ← blog_post.hero_group.headline
subhead ← blog_post.hero_group.subhead
cover_image ← blog_post.hero_group.image
cta slot → <PrimaryButton>
label ← blog_post.hero_group.cta.label
href ← blog_post.hero_group.cta.href
Section Slot: notes_area (empty — Template will fill)Author it in Studio's canvas: open Sections, then + New Section. Connect to the hero_group shape. Drag <Hero> in. Bind headline / subhead / cover_image in the right panel. Drop <PrimaryButton> into the cta slot. Bind its label and href. Click below the Hero, add a Section Slot named notes_area. Save.
Or run the build-section skill and it walks you through the same actions.
What this Section enables the moment it exists:
- blog_post_hero becomes a reusable, named piece of the page. Drop it on a landing-page Template or a category-page Template with the same bindings and hero consistency across pages costs zero JSX duplication.
- The cta Component Slot lets you use the same <Hero> component with different CTA widgets on different Sections (blog uses <PrimaryButton>, webinar uses <InlineForm>).
- The notes_area Section Slot lets you use the same blog_post_hero Section with different below-hero content on different Templates.
- Register another <Hero> variant (say <VideoHero>) and you can swap the Section's rendered component in Studio, no code change to routes.
- Rebinding an existing <Hero> prop to a different schema field is a right-panel action, not JSX.
What Studio does NOT enable at this step: authors editing hero content inline on the live page. That's Visual Editor. Studio manages the composition. The entry editor handles content values.
Step 5: build Section 2, the Body (List)
The body is where authors and marketing spend most of their time, and where Studio's biggest wins live, because the body has multiple items of different types, and today those live in a hand-coded dispatcher.
The pieces of a List Section:
- Repeater: Studio's word for the .map(). Bound to one iterating field (here body_sections).
- Condition Block (CB): one per allowed block-type. Each says "if the current item is text_block, render <TextBlock>." Replaces your if/else dispatcher chain.
Studio walks each item in body_sections, checks CBs top-to-bottom, and the first matching CB tells Studio which registered component to render for that item. <TextBlock>, <ImageBlock>, <RelatedResources> don't change. Studio calls them driven by data authors publish rather than JSX you deploy.
Author it: Studio canvas UI (+ New Section, drop a Repeater at the root, bind it to body_sections, add one CB per allowed block-type, drop the rendering component into each CB), or run the build-repeating-section skill.
What this Section uniquely enables:
- Register a new body block-type (say video_block) (add a CB in Studio pointing at <VideoBlock>) no dispatcher edit.
- Swap which component renders an existing block-type: flip CB(text_block) from <TextBlock> to <FancyTextBlock> in Studio.
- Rebind an existing block prop to a different schema field in the right panel.
Content reordering was always available in the entry editor. That's not Studio's win.
Expose a CB's Slot as a Section Slot: Template-time flexibility
By default, each CB in this List binds to a fixed component: CB(related_resources_block) renders <RelatedResources>. That's tight coupling: the choice lives inside this List Section, one decision that applies everywhere the List is used.
But different Templates require different rendering for the related_resources_block slot in the body:
- Blog Template: a compact 3-column card grid.
- Marketing landing Template: a horizontal carousel.
- Author-profile Template: a plain text list.
If you hard-bind CB(related_resources_block) to one component here, all Templates that embed blog_post_body_sections get that same rendering. To give each Template its own rendering, expose that CB's Slot as a Section Slot instead of binding it to a fixed component.
In Studio's canvas, while authoring blog_post_body_sections:
- Click the CB(related_resources_block) node.
- Instead of dropping a component into its Slot, select Expose as Section Slot.
- Name it related_resources_render.
The List Section now looks like:
Section: blog_post_body_sections (List — root is a Repeater)
Repeater(body_sections)
├ CB(text_block) → <TextBlock>
├ CB(image_block) → <ImageBlock>
└ CB(related_resources_block) → Section Slot: related_resources_render (empty — Template fills)Now:
- Blog Template embeds blog_post_body_sections, and plugs a related_grid Section into related_resources_render.
- Marketing landing Template embeds the same blog_post_body_sections, but plugs related_carousel in.
- Author-profile Template plugs related_text_list.
Same body iteration, three different renderings of one block-type across three Templates. Without this technique you'd have three near-duplicate List Sections that differ only in one CB's target. Every new body block-type you add would need three sync'd edits.
Rule of thumb: bind a CB's Slot to a fixed component when the rendering choice is universal across Templates. Expose it as a Section Slot when the choice is Template-specific. You can mix inside one List Section, as above: text_block and image_block stay universal. related_resources_block opens up to per-Template control.
Same pattern as the body, different iterating field.
Section: blog_post_related_articles (List — root is a Repeater)
Repeater(related) ← a Reference-multi
├ CB(article_ref) → <ArticleCard>
└ CB(podcast_ref) → <PodcastCard>The related field is a Reference-multi: it holds references to entries of other content types (article_ref, podcast_ref). The Repeater walks that reference list. Each CB matches on the referenced entry's content-type UID. <ArticleCard> and <PodcastCard> are the atomic leaves, no further iteration below them.
Author it: Studio canvas UI (same as Step 5, but the Repeater at the root binds to related instead of body_sections), or the build-repeating-section skill.
Wins added:
- Adding a new referenceable content type (say webinar_ref) means adding a CB and pointing it at <WebinarCard>, no dispatcher code change.
- Swap <ArticleCard> for <CompactArticleCard> in Studio without touching the route.
- Or expose either CB's Slot as a Section Slot if you want per-Template control (same technique as Step 5).
Step 7: assemble the Template
Three Sections done. Compose them into a Template that binds to the URL.
Template: blog_post_template
URL: /blog/{slug}
CT: blog_post
Sections: 1. blog_post_hero
fills its notes_area Slot with share_bar
2. blog_post_body_sections
fills its related_resources_render Slot with related_grid
3. blog_post_related_articlesIn Studio's canvas, open Templates, then + New Template, then Connected. Bind to blog_post content type. Set the URL pattern. Drop the three Sections in order. For each Section that exposes a Section Slot, drop the target Section into the Slot right here. Save. Deploy. Or run the build-connected-template skill.
A different Template (say marketing_landing_template) uses the same three Sections, but plugs trust_logos into the Hero's notes_area and related_carousel into the body's related_resources_render. Same three Sections, different Template, different final render.
Step 8: what changes in your route code
Two paths, pick one:
Option A: leave your existing route as-is. Your hand-coded <BlogArticle> route keeps rendering the way it always did. Studio's canvas becomes a second rendering path, used by authors to preview compositions, and by marketing pages that consume the Template via a different route later. Zero regression, zero risk.
Option B: swap the route to render from the Template. Replace the <BlogArticle> invocation with Studio's Connected renderer:
"use client";
import { StudioComponent, useCompositionData } from "@contentstack/studio-react";
export default function BlogPost({ params }) {
const { specOptions } = useCompositionData({
url: `/blog/${params.slug}`,
templateContentTypeUid: "blog_post",
});
if (!specOptions?.spec) return null;
return <StudioComponent specOptions={specOptions} />;
}The route now renders whatever the Template + entry data resolve to: order of Sections, block-types in the body, everything data-driven. You do NOT change <Hero>, <TextBlock>, <ImageBlock>, <RelatedResources>, <ArticleCard>, or <PodcastCard>. They're already registered. Studio calls them as-is.
Step 9: going deeper, nested iteration inside a body block
<RelatedResources> from Step 5 has its own internal .map(), one that iterates over references to article_ref / podcast_ref. Same shape as blog_post_related_articles in Step 6.
Two ways to handle it:
- Simplest: leave <RelatedResources> as a self-contained atomic component that does its own iteration in code. Studio treats it as opaque. The CB in blog_post_body_sections renders it and moves on.
- Fully Studio-composable: give <RelatedResources> a Component Slot for its iteration area, then author a nested List Section that fills it. Now the inner block-types are Studio-composable too. You can add a new inner referenceable content type without a dispatcher edit inside <RelatedResources>.
Do the second one only when the authoring team asks for it. Same pattern applied one level deeper.
Step 10: how Studio replaces your .map() at render time
The mechanism is deliberately boring: standard React short-circuit. No framework magic.
For any component that has a Component Slot filled by a List Section (as in Step 9's fully-composable path), the component needs one line:
function RelatedResources({ refs, slot }) {
return slot ?? <div>{refs.map(ref => /* your existing dispatch */)}</div>;
}- Outside Studio: slot is undefined, ?? falls through, your .map() runs. Zero regression.
- Inside Studio: Studio passes slot as the nested List Section's rendered output. .map() never runs.
You add this line yourself in the components you decide to break down further. Studio never edits your source.
Back to how it works
Return to What you gain, precisely on The Studio page for the outcomes table and next-read links.