# Visual Builder - mental model and implementation

### About this export

| Field | Value |
| --- | --- |
| **content_type** | lesson |
| **platform** | contentstack-academy |
| **source_url** | https://www.contentstack.com/academy/courses/preview-visual-builder-and-releases/visual-builder-mental-model-and-implementation |
| **course_slug** | preview-visual-builder-and-releases |
| **lesson_slug** | visual-builder-mental-model-and-implementation |
| **markdown_file_url** | /academy/md/courses/preview-visual-builder-and-releases/visual-builder-mental-model-and-implementation.md |
| **generated_at** | 2026-08-03T11:49:51.539Z |

> Part of **[Preview, Visual Builder, and Releases](https://www.contentstack.com/academy/courses/preview-visual-builder-and-releases)** on Contentstack Academy. **Academy MD v3** — structured for retrieval; no quiz or assessment keys.

<!-- ai_metadata: {"lesson_id":"05","type":"text","duration_minutes":1,"topics":["Visual","Builder","mental","model","and","implementation"]} -->

#### Lesson text

# Visual Builder: mental model and implementation

> **TL;DR**
> 
> *   Visual Builder renders your actual frontend inside the Contentstack UI and overlays editing controls on every DOM element tagged with a data-cslp attribute.
> *   The data-cslp value follows the format {content\_type\_uid}.{entry\_uid}.{locale}.{field\_path} -- incorrect paths cause silent failures where elements render but are not editable.
> *   Use addEditableTags() from @contentstack/delivery-sdk to auto-generate tag values instead of hand-coding them.
> *   Visual Builder extends Live Preview; if Live Preview is not working, Visual Builder will not work either.

Visual Builder turns your frontend into an editable surface. Instead of switching between the Contentstack entry form and a preview panel, editors click directly on rendered content - a headline, an image, a call-to-action button - and edit it in place. This is not a separate application or a WYSIWYG editor embedded in the CMS. Visual Builder renders your actual production frontend inside the Contentstack UI and overlays editing controls on top of it, meaning editors see exactly what visitors will see, with the ability to modify any tagged field.

This lesson assumes Live Preview is already working in your application (as described in [Lessons 4.1.2](/course-4-preview-visual-builder-releases/module-4-1-live-preview-and-visual-builder/02-draft-vs-published-routing) and [4.1.3](/course-4-preview-visual-builder-releases/module-4-1-live-preview-and-visual-builder/03-ssr-csr-preview-patterns)). Visual Builder extends Live Preview - it does not replace it.

## The mental model: your frontend as an editing canvas

The key concept behind Visual Builder is field-to-DOM mapping. Every editable element in your rendered HTML is tagged with a data-cslp attribute that tells Visual Builder which content type, entry, locale, and field that element represents. When the editor opens Visual Builder in Contentstack, the platform loads your frontend in an iframe, scans the DOM for data-cslp attributes, and creates clickable overlay regions around each tagged element.

When the editor clicks on a tagged region:

1.  Visual Builder reads the data-cslp attribute to identify the field.
2.  An inline editing panel appears, showing the field's editing interface (text input for single-line text, rich text editor for rich text, file picker for assets).
3.  The editor modifies the content directly.
4.  The Live Preview SDK receives the updated data and re-renders the element, giving the editor immediate visual feedback.
5.  The changes are saved to the entry's draft state in Contentstack.

This workflow collapses the traditional two-step process (edit in form, check in preview) into a single action. The editor's context never breaks - they see the content in its rendered layout throughout the editing process.

## The data-cslp attribute system

The data-cslp attribute is the contract between your frontend and Visual Builder. Without it, Visual Builder cannot identify editable regions. The attribute value follows this format:

{content\_type\_uid}.{entry\_uid}.{locale}.{field\_path}

Each component of this identifier serves a specific purpose:

*   content\_type\_uid: Identifies which content type the field belongs to (for example, page, product, product\_line).
*   entry\_uid: The unique identifier of the specific entry (for example, blt8a3c97f2e1d4f5e6).
*   locale: The locale code for the content (for example, en-us, fr-fr, ja-jp).
*   field\_path: The path to the specific field within the content type schema, using dot notation for nested fields.

### Basic field tagging

// A simple page component with Visual Builder edit tags
function MarketingPage({ entry }: { entry: any }) {
  const uid = entry.uid;
  const locale = "en-us";
  const ct = "page";

  return (
    
  );
}

Every piece of editable content has a corresponding data-cslp attribute. When the editor opens this page in Visual Builder, they can click on the page title, description, or image and edit each one individually.

> **Common pitfall:**
> 
> Using an incorrect field path in data-cslp (e.g., components.0.title instead of components.0.hero.title for a modular block) causes Visual Builder to fail silently for that element -- no overlay appears and the editor cannot click on it, with no error in the UI.

## Using addEditableTags() to automate tagging

Manually constructing data-cslp attribute values for every field is tedious and error-prone. The @contentstack/delivery-sdk provides an addEditableTags() utility via contentstack.Utils that processes an entry object and attaches the correct tag values as properties you can reference in your templates.

import contentstack from "@contentstack/delivery-sdk";

// Fetch the entry from Contentstack
const entry = await stack
  .contentType("page")
  .entry("blt8a3c97f2e1d4f5e6")
  .fetch();

// Add editable tags to the entry object
// The third argument (true) enables locale-prefixed edit tag paths
contentstack.Utils.addEditableTags(entry, "page", true);

// Now entry fields have a $ prefix property with the tag value
// entry.$?.title  =>  { "data-cslp": "page.blt8a3c97f2e1d4f5e6.en-us.title" }
// entry.$?.description  =>  { "data-cslp": "page.blt8a3c97f2e1d4f5e6.en-us.description" }

After calling addEditableTags(), each field on the entry object gains a corresponding property under the $ key. You can then use these in your JSX:

function MarketingPage({ entry }: { entry: any }) {
  return (
    
  );
}

The addEditableTags() utility handles the UID, content type, and locale insertion automatically. If you rename a field in your content type or change the entry UID, the tags update automatically on the next fetch - you do not need to manually update string literals scattered across your components.

With the core tagging system in place, the next step is handling complex field types.

## Handling complex field types

Simple text and image fields are straightforward to tag. Complex field types - modular blocks, reference fields, group fields, and JSON RTE - require specific tagging strategies.

### Group fields

Group fields are nested objects within an entry. The field path uses dot notation to traverse into the group:

// Content type schema:
// seo (Group)
//   ├── meta\_title (Single Line Text)
//   ├── meta\_description (Multi Line Text)
//   └── og\_image (File)

function SEOHead({ entry }: { entry: any }) {
  return (
    <>
      
    
  );
}

With addEditableTags(), nested group fields are accessible seamlessly through corresponding nested $ property:

// After contentstack.Utils.addEditableTags(entry, "page", true):
// entry.seo.$?.meta\_title => "page.blt\_matrix\_link\_001.en-us.seo.meta\_title"

### Modular blocks

Modular blocks are arrays of typed blocks, each with its own schema. The field path includes the block index and the block type:

// Content type schema:
// page\_components (Modular Blocks)
//   ├── hero\_block
//   │   ├── heading (Single Line Text)
//   │   └── background\_image (File)
//   ├── feature\_grid\_block
//   │   ├── section\_title (Single Line Text)
//   │   └── features (Group - multiple)
//   └── testimonial\_block
//       ├── quote (Multi Line Text)
//       └── author\_name (Single Line Text)

function PageComponents({ entry }: { entry: any }) {
  return (
    <div>
      {entry.page\_components?.map((block: any, index: number) =&gt; {
        if (block.hero\_block) {
          return (
            <section key="{index}">
              <h1 data-cslp="{\`page.${entry.uid}.en-us.page\_components.${index}.hero\_block.heading\`}">
                {block.hero\_block.heading}
              </h1>
              <img data-cslp="{\`page.${entry.uid}.en-us.page\_components.${index}.hero\_block.background\_image\`}" src="{block.hero\_block.background\_image?.url}" alt="">
            </section>
          );
        }

        if (block.testimonial\_block) {
          return (
            <blockquote key="{index}">
              <p data-cslp="{\`page.${entry.uid}.en-us.page\_components.${index}.testimonial\_block.quote\`}">
                {block.testimonial\_block.quote}
              </p>
              <cite data-cslp="{\`page.${entry.uid}.en-us.page\_components.${index}.testimonial\_block.author\_name\`}">
                {block.testimonial\_block.author\_name}
              </cite>
            </blockquote>
          );
        }

        return null;
      })}
    </div>
  );
}

The critical detail is the index in the field path: page\_components.${index}.hero\_block.heading. This tells Visual Builder which specific block instance the editor is interacting with. Without the index, Visual Builder cannot map the click to the correct block in the entry's modular blocks array.

When using addEditableTags(), the function automatically handles the index-based paths for modular blocks. Each block item in the array receives its own $ property with correctly indexed paths.

### Reference fields

Reference fields point to entries in another content type. The referenced entry has its own UID and content type, so the data-cslp attribute must use the referenced entry's identifiers, not the parent entry's.

// A product references a "product\_line" entry
function ProductDetail({ entry }: { entry: any }) {
  const productLine = entry.product\_line\[0\]; // Reference fields return arrays

  return (
    

      {/\* The product line title is from the referenced "product\_line" content type \*/}
      

      {/\* The reference field itself on the product \*/}
      
  );
}

There are two distinct edit tags here. The tag targeting the reference field on the product lets the editor change which product line is referenced. The tag targeting the title field on the product line entry itself lets the editor rename the product line. Both are valid, and the correct choice depends on the editorial intent.

### JSON Rich Text Editor (JSON RTE)

JSON RTE content is stored as a structured JSON tree rather than an HTML string. The data-cslp attribute tags the container element, and Visual Builder provides an inline rich text editing experience:

// JSON RTE field renders as structured content
function RichTextBlock({ entry, index }: { entry: any; index: number }) {
  return (
    <div data-cslp="{\`page.${entry.uid}.en-us.components.${index}.rich\_text.content\`}">
      {renderJsonRte(entry.components\[index\].rich\_text.content)}
    </div>
  );
}

You tag the wrapper container element, not individual paragraphs or headings within the RTE content. Visual Builder recognizes that the tagged field is a JSON RTE and opens the appropriate rich text editor when the editor clicks on it.

Now that field tagging covers simple and complex types, here is the step-by-step setup sequence.

## Implementation steps

Setting up Visual Builder follows a specific sequence. Reordering these steps often leads to a partially functional setup.

### Step 1: Ensure Live Preview is working

Visual Builder depends on Live Preview infrastructure. Before configuring Visual Builder, verify that:

*   Your preview deployment is accessible and fetches draft content using the Preview Token.
*   The Live Preview SDK is initialized with the correct region-specific hosts (REST Preview host for live\_preview.host, Application host for clientUrlParams.host). See Lesson 3.1.2 for the full endpoint tables.
*   onEntryChange callbacks trigger correctly when editors modify content.
*   The preview URL is correctly configured under Settings > Live Preview in Contentstack.
*   The editButton is configured with exclude: \["outsideLivePreviewPortal"\] so the edit button only appears inside the Contentstack preview iframe context.

If Live Preview is not working, Visual Builder will not work. Verify this before proceeding.

### Step 2: Add data-cslp attributes to all rendered content

Tag every piece of editable content in your components with the correct data-cslp attribute. Use addEditableTags() to automate this where possible. Pay particular attention to array index mappings inside modular blocks, group dot notation paths, and target reference constraints.

### Step 3: Configure Visual Builder in stack settings

In the Contentstack dashboard, navigate to Settings > Live Preview and enable Visual Builder. The configuration requires setting the base Preview URL and establishing explicit content type mapping configurations using pattern routes.

### Step 4: Test in the Contentstack entry editor

Open an entry in Contentstack and switch to the Visual Builder view. You should see your frontend rendered cleanly inside the UI overlay, showing hoverable highlight regions around every element with a valid data-cslp attribute. If highlight regions do not appear, check the browser console for CSP errors or incorrect attribute string formatting parameters.

With the implementation steps covered, let's see how everything comes together in a real-world example.

## Kickstart Veda page example

Consider Veda building a marketing homepage with modular blocks. In kickstart-veda, the content type page uses a modular blocks field called components with block types such as hero, list, rich\_text, media, and two\_column.

// components/Page.tsx
import contentstack from "@contentstack/delivery-sdk";

interface PageProps {
  entry: any;
}

export function Page({ entry }: PageProps) {
  // Add editable tags to the entire entry tree
  contentstack.Utils.addEditableTags(entry, "page", true);

  return (
    
      {entry.components?.map((component: any, index: number) => {
        if (component.hero) {
          return ;
        }
        if (component.list) {
          return ;
        }
        if (component.rich\_text) {
          return ;
        }
        if (component.two\_column) {
          return ;
        }
        return null;
      })}
    
  );
}

function HeroBlock({ block, index, entryUid }: { block: any; index: number; entryUid: string }) {
  const prefix = \`page.${entryUid}.en-us.components.${index}.hero\`;

  return (
    
      {block.ctas?.map((ctaWrapper: any, ctaIndex: number) => (
        
      ))}
      
  );
}

function ListBlock({ block, index, entryUid }: { block: any; index: number; entryUid: string }) {
  const prefix = \`page.${entryUid}.en-us.components.${index}.list\`;

  return (
    
  );
}

function RichTextBlock({ block, index, entryUid }: { block: any; index: number; entryUid: string }) {
  const prefix = \`page.${entryUid}.en-us.components.${index}.rich\_text\`;

  return (
    
  );
}

function TwoColumnBlock({ block, index, entryUid }: { block: any; index: number; entryUid: string }) {
  const prefix = \`page.${entryUid}.en-us.components.${index}.two\_column\`;

  return (
    
  );
}

With this implementation, an editor can click on any element and edit it directly. A marketing editor can open the page in Visual Builder and click on any element - the hero title, a referenced product card field, rich text content, or two-column layout - and edit it directly. The modular block structure means editors can also reorder components through the Contentstack entry form, and Visual Builder reflects the new order instantly.

The important nuance is that the Veda list block stores its content in a reference field, so the page-level edit tag for the reference picker is page...components.{index}.list.reference, while the nested product fields use the referenced entry's own content type and UID (for example, product.{uid}.en-us.title). 

The two\_column configuration reflects the current kickstart-veda renderer behavior which expects nested media and rich\_text objects. If your own schema models two\_column differently, always derive the edit-tag paths from the actual field UIDs in your content type rather than copying this example verbatim.

## Performance considerations

Visual Builder adds a postMessage bridge and DOM observation layer to your application. This has measurable but typically negligible performance impact:

*   MutationObserver: Visual Builder uses MutationObserver to detect DOM changes and update overlay positions. On pages with frequent dynamic animations, this can add minor CPU overhead.
*   postMessage frequency: Each editor keystroke generates a postMessage event. The SDK debounces these internally, but rapid typing can still produce a short burst of messages safely.
*   Overlay rendering: Overlays render as positioned elements in the iframe DOM. Initial calculation for over 100 elements takes slightly longer, which affects the editor's initial load time, not production speed.

These performance characteristics only affect the preview experience. They have zero impact on production because the Live Preview SDK and Visual Builder are completely inactive on the production deployment targets.

## Common mistakes

### Mistake 1: missing data-cslp attributes on modular block children

Tagging the modular block container but not the individual fields within each block results in a single large clickable region that opens the full modular blocks form instead of field-level editing. For the best editor experience, tag each field within each block individually.

### Mistake 2: deploying Visual Builder code to production without conditional loading

While the Live Preview SDK only activates in the iframe context, including the SDK bundle in the production build adds unnecessary JavaScript weight. Conditionally import the SDK based on your preview mode environment variables to keep production bundle lean.

import { getContentstackEndpoints } from "@timbenniks/contentstack-endpoints";

if (process.env.NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW === "true") {
  const endpoints = getContentstackEndpoints(
    process.env.NEXT\_PUBLIC\_CONTENTSTACK\_REGION || "na",
    true
  );

  import("@contentstack/live-preview-utils").then((module) => {
    module.default.init({
      ssr: false,
      enable: true,
      mode: "builder",
      stackSdk: stack.config,
      stackDetails: {
        apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
        environment: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_ENVIRONMENT!,
      },
      clientUrlParams: {
        host: endpoints.application,
      },
      editButton: {
        enable: true,
        exclude: \["outsideLivePreviewPortal"\],
      },
    });
  });
}

### Mistake 3: incorrect field paths for nested content

Using shallow names instead of explicit dot notation inside groups, or forgetting the block type wrapper name inside modular blocks, causes Visual Builder to fail silently for that element. Always verify your paths against the content type schema maps.

#### Key takeaways

- Connect **Visual Builder - mental model and implementation** back to your stack configuration before moving to the next module.
- Capture one concrete artifact (screenshot, Postman call, or code snippet) that proves the step works in your environment.
- Re-read the delivery versus management boundary for anything you changed in the entry model.

## Supplement for indexing

### Content summary

Visual Builder - mental model and implementation. Visual Builder: mental model and implementation TL;DR Visual Builder renders your actual frontend inside the Contentstack UI and overlays editing controls on every DOM element tagged with a data-cslp attribute. The data-cslp value follows the format {content\ type\ uid}.{entry\ uid}.{locale}.{field\ path} -- incorrect paths cause silent failures where elements render but are not editable. Use addEditableTags() from @contentstack/delivery-sdk to auto-generate tag values instead of hand-coding them. Visual Builder extends Live Preview; if Live Preview is not working, Visual Builder will not work either. Visual Builder turns your frontend into an editable surface. Instead of switching between t

### Retrieval tags

- Visual
- Builder
- mental
- model
- and
- implementation
- preview-visual-builder-and-releases
- lesson 05
- Visual Builder - mental model and implementation
- preview-visual-builder-and-releases lesson

### Indexing notes

Index this lesson as a primary chunk tagged with lesson_id "05" and topics: [Visual, Builder, mental, model, and, implementation].
Parent course slug: preview-visual-builder-and-releases. Use asset_references URLs as thumbnail hints in search results when present.
Never surface LMS quiz content or assessment answers from this file.

### Asset references

_No image or video thumbnail URLs were extracted._

### External links

| Label | URL |
| --- | --- |
| Contentstack Academy home | `https://www.contentstack.com/academy/` |
| Training instance setup | `https://www.contentstack.com/academy/training-instance` |
| Academy playground (GitHub) | `https://github.com/contentstack/contentstack-academy-playground` |
| Contentstack documentation | `https://www.contentstack.com/docs/` |
