# Preview, Visual Builder, and Releases

### About this export

| Field | Value |
| --- | --- |
| **content_type** | course |
| **platform** | contentstack-academy |
| **source_url** | https://www.contentstack.com/academy/courses/preview-visual-builder-and-releases |
| **language** | en |
| **product_area** | Contentstack Academy |
| **learning_path** | cms-developer-certification |
| **course_id** | preview-visual-builder-and-releases |
| **slug** | preview-visual-builder-and-releases |
| **version** | 2026-07-31 |
| **last_updated** | 2026-08-03 |
| **status** | published |
| **keywords** | ["Contentstack Academy"] |
| **summary_one_line** | Preview, Visual Builder, and Releases Build the editorial feedback loop that headless architectures do not give you for free: reliable preview, in-context editing, and coordinated future-state publishing. Who This Course… |
| **total_duration_minutes** | 62 |
| **lessons_count** | 10 |
| **video_lessons_count** | 0 |
| **text_lessons_count** | 10 |
| **linked_learning_path** | cms-developer-certification |
| **linked_assessment_ref** | LMS_UNCONFIGURED_COURSE_ASSESSMENT |
| **markdown_file_url** | /academy/md/courses/preview-visual-builder-and-releases.md |
| **generated_at** | 2026-08-03T11:49:50.779Z |
| **intended_audience** | [] |
| **prerequisites** | [] |
| **related_courses** | [] |

> **Academy MD v3** — companion `.md` for Ask AI. Quizzes and graded assessments are **LMS-only**; this file never contains answer keys.

## Course Overview

| Metadata | Value |
| --- | --- |
| Catalog duration | 1h 2m 25s |
| Released (if known) | 2026-07-31 |
| Product area | Contentstack Academy |

### Description

# Preview, Visual Builder, and Releases

Build the editorial feedback loop that headless architectures do not give you for free: reliable preview, in-context editing, and coordinated future-state publishing.

## Who This Course Is For

This course is for developers responsible for preview environments, editor experience, release coordination, or frontend integrations that must reflect draft content safely.

## You Will Be Able To

*   explain the architecture required for trustworthy Live Preview
*   configure Visual Builder and preview paths around real frontend behavior
*   manage scheduling, versioning, and releases without guessing what will go live

## Recommended Preparation

Finish Courses 1-3 first so you already understand delivery vs preview boundaries, environment strategy, and frontend rendering responsibilities.

## Estimated Effort

1 - 2 hours

## Build Thread

You will extend the Veda storefront from published delivery into draft preview, editor-visible page editing, and release-aware future-state validation.

## Suggested Next Step

Start with [Preview requirements and concepts](/course-4-preview-visual-builder-releases/module-4-1-live-preview-and-visual-builder/01-preview-requirements-and-concepts).

### Learning objectives

1. Follow each lesson in order.
2. Practice in a training stack using placeholders **YOUR_STACK_API_KEY** and **YOUR_DELIVERY_TOKEN** in local `.env` files only.
3. Validate API responses against the official documentation.

### Topics covered

Contentstack Academy

## Course structure

```text
preview-visual-builder-and-releases/
├── 01-live-preview-and-visual-builder-overview · text · 3 min
├── 02-preview-requirements-and-concepts · text · 1 min
├── 03-draft-vs-published-preview-host-routing-and-caching · text · 1 min
├── 04-ssr-and-csr-preview-patterns · text · 1 min
├── 05-visual-builder-mental-model-and-implementation · text · 1 min
├── 06-visual-builder-graphql-localization-and-edge-cases · text · 1 min
├── 07-scheduling-releases-and-versioning-overview · text · 1 min
├── 08-releases-scheduling-and-coordinated-publishing · text · 1 min
├── 09-entry-versioning-comparison-and-rollback · text · 1 min
├── 10-previewing-future-states-with-timeline · text · 1 min
```

## Lessons

### Lesson 01 — Live Preview and Visual Builder : Overview

<!-- ai_metadata: {"lesson_id":"01","type":"text","duration_minutes":3,"topics":["Live","Preview","and","Visual","Builder","Overview"]} -->

#### Lesson text

# Live Preview and Visual Builder

This module teaches the implementation details behind a trustworthy editorial preview experience.

## Why This Module Matters

Headless architectures separate content from presentation, which means developers must rebuild the editorial feedback loop intentionally.

## You Will Be Able To

*   describe the architecture required for reliable Live Preview
*   implement preview paths for SSR and CSR patterns
*   configure Visual Builder so editors can work in context instead of by guesswork

## Recommended Preparation

Complete Course 3 first so delivery APIs, runtime boundaries, and frontend rendering patterns already feel concrete.

## Estimated Effort

90-105 minutes

## Practice Focus

Extend the Veda storefront from published delivery into draft preview and in-context editing flows that editors can trust.

## Suggested Next Step

Start with lesson 1 in this module and treat preview correctness as a product requirement, not a nice-to-have.

#### Key takeaways

- Connect **Live Preview and Visual Builder : Overview** 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.

### Lesson 02 — Preview requirements and concepts

<!-- ai_metadata: {"lesson_id":"02","type":"text","duration_minutes":1,"topics":["Preview","requirements","and","concepts"]} -->

#### Lesson text

# Preview requirements and concepts

> **TL;DR**
> 
> *   Preview is an architecture concern, not a UI feature -- it requires separate delivery planes, tokens, hosts, and caching strategies for draft vs. published content.
> *   Every preview request needs deterministic context propagation: environment, locale, branch, live-preview hash, and optional timestamp.
> *   SSR preview must be request-scoped to avoid cross-request state leakage, and caching must be explicitly disabled for preview traffic.
> *   Treat preview correctness as a release criterion, not an afterthought.

> **Prerequisites**
> 
> This module assumes familiarity with React (hooks, useEffect, useState), Next.js App Router (server components, draftMode, router.refresh), and the difference between SSR and CSR rendering. The code examples use TypeScript throughout.

Preview is not a UI accessory. It is a correctness system.

When editors ask “is this page ready?”, they are asking a systems question: _Can I trust what I see before publish?_ If preview is unreliable, teams either publish too cautiously or publish too optimistically.

## The core mental model: two delivery planes

A useful way to reason about preview is to split content delivery into two planes:

*   Published plane: optimized for live user traffic and stable runtime delivery
*   Preview plane: optimized for draft validation and editorial feedback loops

If your app treats preview as “published plus a flag,” you usually get inconsistent results. Preview has different host routing, request context, and caching expectations.

For Contentstack, published traffic typically goes through delivery endpoints, while preview flows use preview-oriented endpoints and context (including live preview metadata).

## Requirement 1: runtime supports preview context propagation

Preview is context-sensitive. To retrieve the right draft state, request context must be propagated end to end:

*   environment
*   locale and fallback behavior
*   branch (if branch-aware workflows are used)
*   live preview hash/session data
*   optionally preview timestamp for timeline-style future-state validation

The important concept is not any single header or query parameter. The concept is deterministic context propagation. If context is partially dropped by middleware, router layers, SSR adapters, or API wrappers, preview becomes non-deterministic.

> **Common pitfall:**
> 
> If preview state is stored globally in a long-lived server process, one editor's draft context can leak into another editor's request, producing non-deterministic preview results.

## Requirement 2: your frontend can participate in preview transport

For Live Preview and Visual Builder workflows, the frontend experience must support in-context integration (typically iframe-based interaction patterns and edit-tag mapping).

In practice this means:

*   the rendering application can be loaded in an editorial preview context
*   client and/or server layers can read preview context from request/query
*   rendered output can map content fields to UI regions for in-context editing workflows

Contentstack docs also note boundaries: some channels (for example native apps or third-party page builders) are not primary Live Preview targets.

## Requirement 3: request-scoped behavior for SSR

SSR introduces one of the most common preview defects: cross-request leakage. If preview state is stored globally in a long-lived process object, one editor’s context can affect another request. Reliable SSR preview requires request-scoped initialization and context handling.

Design rules:

*   initialize preview-specific state per request
*   avoid singleton mutable preview clients in SSR paths
*   make preview mode explicit in logs and tracing

This is less about framework syntax and more about process isolation discipline.

## Requirement 4: preview and production caching strategies diverge

Preview traffic values freshness over cache efficiency. Production traffic usually values cache efficiency over per-request freshness. If you apply production CDN/app cache policy to preview endpoints, editors see stale data and lose trust immediately.

A robust strategy separates cache behavior by mode:

*   production mode: optimize for hit rate and latency
*   preview mode: no-cache or tightly controlled short-lived cache, with explicit invalidation semantics

Treat this as an architectural invariant, not a runtime tweak.

## Requirement 5: observable preview pipeline

Teams often discover preview defects only through manual complaints (“I changed text but preview didn’t update”). Reliable systems expose preview telemetry:

*   mode (published vs preview)
*   locale
*   branch/alias
*   preview hash presence
*   timestamp context presence
*   selected host (published vs preview)

With these signals, troubleshooting becomes deterministic. Without them, preview debugging devolves into guesswork.

## Light implementation example

The following example shows the architectural shape of preview-aware retrieval. Focus on explicit mode and context, not framework details:

export function buildContentstackRequest(mode: "published" | "preview", ctx: {
  branch?: string;
  locale?: string;
  livePreviewHash?: string;
  previewTimestamp?: string;
}) {
  const host =
    mode === "preview"
      ? "https://rest-preview.contentstack.com"
      : "https://cdn.contentstack.io";

  const headers: Record = {
    api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
    branch: ctx.branch || "main",
  };

  if (mode === "preview") {
    headers.preview\_token = process.env.NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW\_TOKEN!;
    if (ctx.livePreviewHash) headers.live\_preview = ctx.livePreviewHash;
    if (ctx.previewTimestamp) headers.preview\_timestamp = ctx.previewTimestamp;
  } else {
    headers.access\_token = process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN!;
  }

  return { host, headers, locale: ctx.locale || "en-us" };
}

This pattern creates explicit separation between delivery and preview planes and prevents “accidental preview” behavior from hidden defaults.

## Conceptual integration with Timeline and Visual Builder

Preview requirements become stricter when Timeline or Visual Builder enters the stack.

### Timeline implication

Timeline adds time as a query dimension. That means preview is no longer only “draft vs published,” but potentially “state at time T.”

Architectural effect:

*   timestamp context must travel through request layers
*   preview correctness now depends on both content state and temporal state

### Visual Builder implication

Visual editing requires field-to-DOM traceability. If your component tree cannot map rendered UI back to content fields, editors can see content but cannot reliably edit in place.

Architectural effect:

*   edit tags become part of delivery contract quality
*   component abstractions must preserve content lineage, not erase it

## Common failure modes

### Failure mode 1: preview works locally but fails in shared environments

Root cause: environment/branch context differences are not explicitly encoded.

Prevention: treat environment + branch + locale as required preview inputs in integration contracts.

### Failure mode 2: stale preview after content save

Root cause: cache policy copied from production path.

Prevention: explicit no-cache behavior (or strict short cache with controlled invalidation) for preview mode.

### Failure mode 3: inconsistent SSR preview behavior

Root cause: shared mutable preview client state.

Prevention: request-scoped preview client/context initialization.

### Failure mode 4: editors cannot edit what they can see

Root cause: missing or inconsistent edit-tag strategy.

Prevention: define edit-tag coverage as a functional requirement, not optional enhancement.

## Implementation checklist for production readiness

Before calling preview “done,” verify:

1.  Delivery and preview hosts are separated by explicit mode logic.
2.  Preview context values are captured, propagated, and logged.
3.  SSR path is request-scoped and free of shared preview state.
4.  Preview cache behavior is intentionally different from production behavior.
5.  Visual editing and timeline behavior are validated for critical scenarios.

If any of these are missing, you have partial preview, not reliable preview.

## Summary

Reliable preview in Contentstack implementations is an architecture outcome. It requires explicit separation between published and preview delivery planes, deterministic context propagation, request-scoped SSR behavior, preview-aware caching, and observability.

When these requirements are met, preview becomes a high-trust collaboration surface between editors and developers. When they are skipped, preview becomes a source of ambiguity and release risk.

#### Key takeaways

- Connect **Preview requirements and concepts** 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.

### Lesson 03 — Draft vs published - preview host routing and caching

<!-- ai_metadata: {"lesson_id":"03","type":"text","duration_minutes":1,"topics":["Draft","published","preview","host","routing","and"]} -->

#### Lesson text

# Draft vs published: preview host routing and caching

> **TL;DR**
> 
> *   Use a Preview Token (against the preview host) for draft content and a Delivery Token (against the CDN host) for published content -- never mix them.
> *   The safest architecture is two separate deployments (production and preview) with identical codebases but different environment variables.
> *   Preview caching must be explicitly disabled at every layer (CDN, browser, framework fetch cache) or editors will see stale drafts.
> *   The Live Preview SDK auto-detects iframe context and redirects SDK calls to the preview API, including the real-time live\_preview hash.

Every content entry in Contentstack exists in one of two states: saved as a draft, or published to an environment. The entire preview infrastructure exists to answer one question reliably: can an editor see exactly what the page will look like before they hit publish? Getting that right requires deliberate separation of how draft content and published content are fetched, routed, and cached.

As covered in [Lesson 4.1.1](/course-4-preview-visual-builder-releases/module-4-1-live-preview-and-visual-builder/01-preview-requirements-and-concepts), preview and production operate on different delivery planes. This lesson focuses on the concrete mechanics: how tokens control access to draft versus published content, how hosting architecture routes requests to the correct plane, and why caching strategy must differ between the two.

## Preview Token vs Delivery Token

Contentstack uses two distinct token types to separate draft and published access at the API level.

A Delivery Token is scoped to a specific environment (for example, production or staging). When you make a request to the Content Delivery API at cdn.contentstack.io using a Delivery Token, the response only includes entries that have been published to that environment. Unpublished drafts, in-progress edits, and entries awaiting approval are invisible. This is exactly what you want for your live site.

A Preview Token grants access to the latest saved version of every entry, regardless of publish state. Requests go to your region's REST Preview host (for example, rest-preview.contentstack.com for AWS NA, eu-rest-preview.contentstack.com for AWS EU). Every region has its own preview host -- see the [full endpoint table in Lesson 3.1.2](/docs/developers/contentstack-regions/api-endpoints) for all seven regions. If an editor has saved changes but not yet published, the Preview Token returns those unsaved changes. If an entry has never been published, the Preview Token still returns it.

You generate both tokens under Settings > Tokens in the Contentstack dashboard. The critical difference is behavioral:

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

const endpoints = getContentstackEndpoints(process.env.NEXT\_PUBLIC\_CONTENTSTACK\_REGION || "na");

// Production request  -  only returns published content
const productionHeaders = {
  api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY,
  access\_token: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN,
  environment: "production",
};
const productionUrl = \`${endpoints.contentDelivery}/v3/content\_types/page/entries\`;

// Preview request  -  returns latest draft, including unpublished changes
const previewHeaders = {
  api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY,
  preview\_token: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW\_TOKEN,
  environment: "production",
};
const previewUrl = \`${endpoints.preview}/v3/content\_types/page/entries\`;

The @timbenniks/contentstack-endpoints package resolves the correct base URLs for any region string. It is a zero-dependency helper that stays in sync with [Contentstack's official regions data](https://artifacts.contentstack.com/regions.json). You could just as easily look up the hosts from the endpoint documentation and set them manually -- the package simply removes the risk of hardcoding the wrong host for your region.

flowchart LR
    subgraph Production
        A\[Frontend\] -->|Delivery Token| B\[CDA host for your region\]
        B --> C\[Published content only\]
    end
    subgraph Preview
        D\[Preview Frontend\] -->|Preview Token| E\[REST Preview host for your region\]
        E --> F\[Latest draft + unpublished\]
    end

Notice that both requests specify the same environment. The Preview Token does not bypass environments - it still respects the environment scope. What it bypasses is the publish gate. The entry does not need to be published to that environment for the Preview Token to return it.

> **Common pitfall:**
> 
> If you accidentally use a Delivery Token in your preview environment, editors will only see already-published content -- and the bug often goes unnoticed during setup because testing happens with already-published entries.

This distinction has a direct architectural implication: your preview deployment must use a different token and a different API host than your production deployment.

## Two hosting approaches

There are two primary architectures for separating preview from production traffic.

### Approach 1: separate preview host

The most common and most reliable approach uses two distinct deployments:

*   www.example.com - the production deployment, using the Delivery Token against cdn.contentstack.io
*   preview.example.com - the preview deployment, using the Preview Token against your region's preview host (for example, rest-preview.contentstack.com in AWS NA)

Both deployments run the same frontend application codebase. The difference is entirely in environment configuration. The preview deployment reads NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW\_TOKEN instead of NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN, and targets the preview API host.

flowchart TD
    subgraph Same Codebase
        A\[Git Repository\]
    end
    A --> B\[Production Deploy
www.example.com\]
    A --> C\[Preview Deploy
preview.example.com\]
    B -->|NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN| D\[cdn.contentstack.io\]
    C -->|NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW\_TOKEN| E\[rest-preview.contentstack.com\]

\# Production environment variables (.env.production)
NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY=your\_api\_key
NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN=your\_delivery\_token
NEXT\_PUBLIC\_CONTENTSTACK\_ENVIRONMENT=production
NEXT\_PUBLIC\_CONTENTSTACK\_REGION=eu
NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW=false

# Preview environment variables (.env.preview)
NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY=your\_api\_key
NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN=your\_delivery\_token
NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW\_TOKEN=your\_preview\_token
NEXT\_PUBLIC\_CONTENTSTACK\_ENVIRONMENT=production
NEXT\_PUBLIC\_CONTENTSTACK\_REGION=eu
NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW=true

Both deployments share the same NEXT\_PUBLIC\_CONTENTSTACK\_REGION value. The region determines all API hosts -- you never hardcode host URLs directly. The preview deployment adds NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW\_TOKEN and sets NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW=true.

This approach is favored because it provides complete isolation. The production deployment never touches the preview API, eliminating any chance of draft content leaking to public visitors. The preview deployment can have different caching rules, different authentication, and different infrastructure scaling without affecting production.

In Contentstack, you configure this under Settings > Live Preview. The Live Preview configuration requires a Preview URL - this is the base URL of your preview deployment (for example, https://preview.example.com). When an editor opens Live Preview in the entry editor, Contentstack loads this URL in an iframe and passes context parameters including the entry UID, content type, and locale.

### Approach 2: single host with mode switching

Some teams prefer running a single deployment that switches behavior dynamically based on request context:

// Next.js middleware example: detect preview mode from query parameters
import { NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest) {
  const isPreview = request.nextUrl.searchParams.has("live\_preview");

  if (isPreview) {
    const response = NextResponse.next();
    response.headers.set("Cache-Control", "no-store, no-cache, must-revalidate");
    response.headers.set("X-Content-Mode", "preview");
    return response;
  }

  return NextResponse.next();
}

This approach reduces infrastructure cost - one deployment instead of two - but introduces risk. A bug in the mode-switching logic could expose draft content to production visitors, or apply production caching to preview requests. For teams with strict content governance requirements, the separate host approach is safer.

## URL routing: preview must mirror production

A subtle but critical requirement: the URL structure of your preview deployment must match your production deployment exactly. If the production URL for a blog post is www.example.com/blog/q3-earnings-report, the preview URL must be preview.example.com/blog/q3-earnings-report.

**Worth noting:** Editors navigate preview by content, not by URL. When an editor opens Live Preview for an entry, Contentstack constructs the preview URL by combining the preview host base URL with the entry's URL path. If your preview deployment uses a different routing scheme than production, editors will land on 404 pages or see the wrong content.

Consider a corporate website where a marketing page lives at /solutions/enterprise. The preview configuration in Contentstack must resolve to preview.example.com/solutions/enterprise. If your preview deployment is a separate build that uses different route conventions, the mapping breaks.

// Contentstack Live Preview URL resolution
// Base URL configured in Settings > Live Preview: https://preview.example.com
// Entry URL path from content type URL field: /solutions/enterprise
// Resolved preview URL: https://preview.example.com/solutions/enterprise

// Your production and preview deployments MUST both resolve this path
// to the same page component rendering the same entry

The practical guideline is straightforward: deploy the same application code to both production and preview. The only difference should be environment variables controlling which token and API host to use.

## Caching: the fundamental divergence

Caching is where preview and production requirements directly conflict, and where most preview implementations break down.

### Production caching strategy

For production, aggressive caching is desirable and expected:

*   CDN caching: Cache rendered pages at the edge for minutes or hours. Contentstack publishes trigger webhook-based invalidation.
*   Browser caching: Set Cache-Control: public, max-age=3600 or longer for static pages.
*   API response caching: Cache Delivery API responses in-memory or in Redis to reduce API calls.
*   ISR/SSG caching: Use framework Incremental Static Regeneration or full static generation to avoid runtime API calls entirely.

This is standard practice. The Delivery API responses are immutable between publishes, so caching them aggressively is safe and efficient.

### Preview caching strategy

For preview, caching is the enemy. An editor saves a draft, opens Live Preview, and expects to see the change immediately. If any layer - CDN, browser, application, or API response cache - serves a stale version, the editor loses trust in the preview system.

function setCacheHeaders(res: Response, mode: "production" | "preview") {
  if (mode === "preview") {
    // Prevent ALL caching for preview responses
    res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
    res.setHeader("Pragma", "no-cache");
    res.setHeader("Expires", "0");
    res.setHeader("Surrogate-Control", "no-store");
    // CDN-specific headers (Vercel, Cloudflare, Fastly)
    res.setHeader("CDN-Cache-Control", "no-store");
  } else {
    // Aggressive caching for production
    res.setHeader("Cache-Control", "public, s-maxage=3600, stale-while-revalidate=86400");
  }
}

The Surrogate-Control and CDN-Cache-Control headers are important because some CDN platforms (Fastly, Cloudflare) respect these headers separately from Cache-Control. If you only set Cache-Control: no-store, the CDN might still cache the response based on its own rules.

### Caching pitfalls in practice

**Pitfall 1: CDN caching preview responses.** If your preview deployment sits behind Cloudflare, Vercel's Edge Network, or another CDN, ensure the CDN configuration skips caching for the preview hostname entirely. On Vercel, this means setting cache headers in your application code - Vercel respects Cache-Control headers from your application. On Cloudflare, create a page rule or cache rule for preview.example.com/\* with cache level set to "Bypass."

**Pitfall 2: Browser caching from a previous production visit.** If a user visits the production site and their browser caches a page, then visits the preview site with the same URL path, the browser might serve the cached production version. The no-store directive prevents this, but only if it is set on the preview response. The initial preview request must already include the correct cache headers.

**Pitfall 3: Application-level caching in SSR.** Frameworks like Next.js have their own data caching layer. In Next.js App Router, fetch() calls are cached by default. For preview, you'll want to explicitly opt out:

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

const endpoints = getContentstackEndpoints(process.env.NEXT\_PUBLIC\_CONTENTSTACK\_REGION || "na");

async function getPageContent(slug: string, isPreview: boolean) {
  const host = isPreview ? endpoints.preview : endpoints.contentDelivery;

  const token = isPreview
    ? process.env.NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW\_TOKEN
    : process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN;

  const tokenHeader = isPreview ? "preview\_token" : "access\_token";

  const response = await fetch(
    \`${host}/v3/content\_types/page/entries?query={"url":"/${slug}"}\`,
    {
      headers: {
        api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
        \[tokenHeader\]: token!,
        environment: "production",
      },
      cache: isPreview ? "no-store" : "force-cache",
      next: isPreview ? { revalidate: 0 } : { revalidate: 3600 },
    }
  );

  return response.json();
}

## How the Live Preview SDK detects preview mode

The Contentstack Live Preview SDK (@contentstack/live-preview-utils) automates much of the preview detection and token switching. When initialized, the SDK checks whether the application is loaded inside the Contentstack entry editor iframe. If it detects the iframe context - by reading query parameters and listening for postMessage events from the Contentstack app - it activates preview mode automatically.

import Contentstack from "@contentstack/delivery-sdk";
import ContentstackLivePreview from "@contentstack/live-preview-utils";
import { getContentstackEndpoints } from "@timbenniks/contentstack-endpoints";

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

const stack = Contentstack.stack({
  apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
  deliveryToken: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN!,
  environment: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_ENVIRONMENT!,
  region: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_REGION,
  live\_preview: {
    enable: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW === "true",
    preview\_token: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW\_TOKEN,
    host: endpoints.preview,
  },
});

ContentstackLivePreview.init({
  ssr: false,
  enable: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW === "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"\],
  },
});

The clientUrlParams.host must match the Contentstack web app for your region (for example, eu-app.contentstack.com for AWS EU). The live\_preview.host must match the REST Preview host for your region. Both are resolved automatically from the region string by getContentstackEndpoints() -- pass true as the second argument to strip the https:// prefix, which is what the SDK expects for host values.

The editButton configuration adds a floating edit button that opens the Visual Builder when an editor clicks it. The exclude: \["outsideLivePreviewPortal"\] setting ensures it only appears inside the Contentstack preview iframe, not when visitors browse the preview URL directly.

When Live Preview is active, the SDK intercepts calls made through the Contentstack Delivery SDK and redirects them to the preview API host with the Preview Token. This means your application code can use the same SDK calls for both production and preview - the Live Preview SDK handles the routing transparently.

The SDK also manages the live\_preview hash, a session-specific identifier that Contentstack uses to serve the exact in-memory draft state the editor is currently working on. Without this hash, the preview API returns the last saved draft. With the hash, it returns the real-time editing state, including changes the editor has typed but not yet saved.

## Putting it together: corporate website example

Consider a corporate website with the following setup:

*   Production: www.acmecorp.com, deployed on Vercel, using Delivery Token
*   Preview: preview.acmecorp.com, deployed on a second Vercel project, using Preview Token
*   Content types: page, blog\_post, press\_release

The Contentstack configuration under Settings > Live Preview sets the preview URL to https://preview.acmecorp.com. Both deployments share the same Git repository and build pipeline. The only difference is the environment variables injected at build time.

When a marketing editor creates a new press release and wants to preview it before publishing:

1.  The editor opens the press release entry in Contentstack and clicks the Live Preview icon.
2.  Contentstack loads https://preview.acmecorp.com/press-releases/q4-results in the preview panel iframe.
3.  The preview deployment receives the request, detects preview mode (the Live Preview SDK reads the iframe context), and fetches the draft press release using the Preview Token from rest-preview.contentstack.com.
4.  The page renders with the unpublished content. No caching occurs - the response headers include Cache-Control: no-store.
5.  The editor modifies the headline. The Live Preview SDK receives a postMessage event with the updated content and re-renders the page in real time.

Meanwhile, public visitors to www.acmecorp.com/press-releases see only published press releases. The production deployment uses the Delivery Token, which cannot access the draft. The CDN caches the production response aggressively, serving it in milliseconds.

## Common mistakes

### Mistake 1: using the Delivery Token in the preview deployment

If the preview environment uses a Delivery Token instead of a Preview Token, editors only see already-published content. New entries and draft changes are invisible. This often goes unnoticed during initial setup because the developer tests with already-published entries. The bug surfaces when an editor creates a brand-new entry and preview shows a 404.

### Mistake 2: applying production cache rules to the preview host

Teams that use a single CDN configuration for both production and preview domains inadvertently cache draft content. The editor sees a stale version, refreshes, sees the same stale version, and concludes that preview is broken. The fix is explicit: configure no-cache rules for the preview domain at the CDN level and in the application response headers.

### Mistake 3: mismatched URL paths between preview and production

If the preview deployment uses a different routing structure (for example, /preview/blog/slug instead of /blog/slug), Contentstack cannot construct the correct preview URL. Editors see 404 pages or the wrong content. Both deployments must resolve the same URL paths to the same content.

#### Key takeaways

- Connect **Draft vs published - preview host routing and caching** 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.

### Lesson 04 — SSR and CSR preview patterns

<!-- ai_metadata: {"lesson_id":"04","type":"text","duration_minutes":1,"topics":["SSR","and","CSR","preview","patterns"]} -->

#### Lesson text

# SSR and CSR preview patterns

> **TL;DR**
> 
> *   CSR preview updates are instant and data-driven (the SDK serves updated content from the postMessage payload); SSR preview updates require a server round-trip.
> *   Set ssr: true or ssr: false in ContentstackLivePreview.init() to match your rendering model -- the wrong setting produces silent failures.
> *   In Next.js App Router, use router.refresh() instead of window.location.reload() to preserve scroll position and client state during SSR preview updates.
> *   For hybrid pages, keep SSR-fetched fields on the server path and CSR-fetched fields on the client path -- never mix data sources for the same field.

Live Preview behaves differently depending on whether your application renders content on the server or in the browser. The rendering strategy you chose for performance and SEO reasons also determines how preview updates reach the editor's screen - through full page reloads triggered by server-side re-fetching, or through in-place DOM updates driven by client-side SDK interception. Understanding both patterns is necessary because most production applications use a hybrid of SSR and CSR, and preview must work correctly across both.

This lesson builds on the token and routing concepts from [Lesson 4.1.2](/course-4-preview-visual-builder-releases/module-4-1-live-preview-and-visual-builder/02-draft-vs-published-routing) and applies them to the two rendering models.

## How Live Preview communicates with your application

Before examining SSR and CSR patterns individually, it helps to understand the communication mechanism that Live Preview uses regardless of rendering strategy.

When an editor opens Live Preview in the Contentstack entry editor, the application loads inside an iframe within the Contentstack UI. The Contentstack app and your application communicate through the browser's postMessage API. When the editor modifies a field - typing a new headline, selecting a different image, reordering modular blocks - the Contentstack app sends a message to the iframe containing the updated entry data.

The @contentstack/live-preview-utils package listens for these messages. What it does with them depends on the rendering model:

*   CSR pattern: The SDK receives the updated data and provides it directly to your components through a callback, enabling instant re-renders without any network request.
*   SSR pattern: The SDK detects the change and triggers a page reload (or a targeted server-sent event), causing the server to re-fetch the draft content and return updated HTML.

This is the fundamental difference. CSR preview updates are data-driven and instantaneous. SSR preview updates require a round-trip to the server and are slightly slower but structurally simpler.

sequenceDiagram
    participant Editor as Editor (Contentstack)
    participant SDK as Live Preview SDK
    participant App as Your App

    Note over Editor,App: CSR Pattern
    Editor->>SDK: Field change (postMessage)
    SDK->>App: Updated data via callback
    App->>App: Re-render component

    Note over Editor,App: SSR Pattern
    Editor->>SDK: Field change (postMessage)
    SDK->>App: Trigger page refresh
    App->>App: Server re-fetches draft content
    App->>App: Full page re-render

## CSR preview: client-side rendering pattern

In a client-side rendered application - a React SPA, a Vue SPA, or a CSR-mode page in a framework like Next.js - the browser fetches content directly from the Contentstack API and renders it in the DOM.

### How it works

1.  The application initializes @contentstack/delivery-sdk with either a Delivery Token or Preview Token, depending on the mode (as covered in [Lesson 4.1.2](/course-4-preview-visual-builder-releases/module-4-1-live-preview-and-visual-builder/02-draft-vs-published-routing)).
2.  Components call the SDK to fetch entry data and render it.
3.  The Live Preview SDK initializes and detects the iframe context.
4.  When the editor modifies content, the Live Preview SDK intercepts the next SDK call or directly provides the updated entry data to a registered callback.
5.  The component re-renders with the new data. No page reload occurs.

### Implementation with React SPA

// hooks/useContentstackEntry.ts
import { useEffect, useState, useCallback } from "react";
import ContentstackLivePreview from "@contentstack/live-preview-utils";
import Contentstack from "@contentstack/delivery-sdk";
import { getContentstackEndpoints } from "@timbenniks/contentstack-endpoints";

const endpoints = getContentstackEndpoints(
  import.meta.env.NEXT\_PUBLIC\_CONTENTSTACK\_REGION || "na",
  true
);

const stack = Contentstack.stack({
  apiKey: import.meta.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY,
  deliveryToken: import.meta.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN,
  environment: import.meta.env.NEXT\_PUBLIC\_CONTENTSTACK\_ENVIRONMENT,
  region: import.meta.env.NEXT\_PUBLIC\_CONTENTSTACK\_REGION,
  live\_preview: {
    preview\_token: import.meta.env.NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW\_TOKEN,
    enable: true,
    host: endpoints.preview,
  },
});

ContentstackLivePreview.init({
  ssr: false,
  enable: import.meta.env.NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW === "true",
  stackDetails: {
    apiKey: import.meta.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY,
    environment: import.meta.env.NEXT\_PUBLIC\_CONTENTSTACK\_ENVIRONMENT,
  },
  stackSdk: stack.config,
  mode: "builder",
  clientUrlParams: {
    host: endpoints.application,
  },
  editButton: {
    enable: true,
    exclude: \["outsideLivePreviewPortal"\],
  },
});

export function useContentstackEntry(contentType: string, entryUid: string) {
  const \[entry, setEntry\] = useState(null);

  const fetchEntry = useCallback(async () => {
    const result = await stack
      .contentType(contentType)
      .entry(entryUid)
      .fetch();
    setEntry(result);
  }, \[contentType, entryUid\]);

  useEffect(() => {
    fetchEntry();

    // Register for live preview updates
    ContentstackLivePreview.onEntryChange(fetchEntry);

    return () => {
      // Cleanup when component unmounts
    };
  }, \[fetchEntry\]);

  return entry;
}

// components/PageDetail.tsx
import { useContentstackEntry } from "../hooks/useContentstackEntry";

export function PageDetail({ entryUid }: { entryUid: string }) {
  const entry = useContentstackEntry("page", entryUid);

  if (!entry) return <div>Loading...</div>;
	return 
	(  
		<article>  
		<h1 data-cslp={\`page.${entryUid}.en-us.title\`}>  {entry.title}</h1>  
		<p data-cslp={\`page.${entryUid}.en-us.description\`}>  {entry.description}</p>  
		<img  data-cslp={\`page.${entryUid}.en-us.image\`}  src={entry.image?.url}  alt={entry.image?.title || entry.title}  />  
		</article>  
	); 
}

The key mechanism is ContentstackLivePreview.onEntryChange(callback), which registers a callback that fires whenever the editor modifies any field. When a change occurs, the Live Preview SDK calls the callback. Because the SDK has already intercepted the Contentstack Delivery SDK instance (passed as stackSdk during initialization), the re-fetch returns the real-time draft data without hitting the network - the SDK serves it from the postMessage payload. The component re-renders instantly.

The data-cslp attributes on each element are edit tags that connect the rendered content to specific CMS fields. These are covered in detail in Lesson 4.1.4, but they are relevant here because they enable field-level highlighting during Live Preview.

## SSR preview: server-side rendering pattern

In a server-side rendered application - Next.js with App Router or Pages Router, Nuxt, Remix, or any server-rendered framework - content is fetched on the server, rendered to HTML, and sent to the browser. The browser receives a fully rendered page.

### How it works

1.  The server receives a page request and fetches content from the Contentstack API using the Preview Token (in preview mode) or Delivery Token (in production).
2.  The server renders the page to HTML and sends it to the browser.
3.  The browser hydrates the page, and the Live Preview SDK initializes on the client side.
4.  When the editor modifies content, the Live Preview SDK detects the change via postMessage.
5.  The SDK triggers a page refresh, causing the server to re-fetch the updated draft content and re-render the page.

The SSR pattern is structurally simpler - you do not need to manage client-side data re-fetching or component re-rendering. The trade-off is that each preview update requires a full server round-trip, making updates slower (typically 200-500ms instead of near-instant).

### Implementation with Next.js App Router

Next.js App Router provides a built-in Draft Mode API that pairs well with Contentstack Live Preview.

// app/api/preview/route.ts  -  Enable draft mode for preview
import { draftMode } from "next/headers";
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const secret = request.nextUrl.searchParams.get("secret");

  // Validate the preview secret to prevent unauthorized access
  if (secret !== process.env.NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW\_SECRET) {
    return NextResponse.json({ message: "Invalid secret" }, { status: 401 });
  }

  const dm = await draftMode();
  dm.enable();

  const slug = request.nextUrl.searchParams.get("slug") || "/";
  return NextResponse.redirect(new URL(slug, request.url));
}

// lib/contentstack.ts  -  Preview-aware content fetching
import Contentstack from "@contentstack/delivery-sdk";
import { getContentstackEndpoints } from "@timbenniks/contentstack-endpoints";

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

export function getStack(isPreview: boolean) {
  return Contentstack.stack({
    apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
    deliveryToken: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN!,
    environment: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_ENVIRONMENT!,
    region: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_REGION,
    live\_preview: isPreview
      ? {
          preview\_token: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW\_TOKEN!,
          enable: true,
          host: endpoints.preview,
        }
      : undefined,
  });
}

export async function getPage(slug: string, isPreview: boolean) {
  const stack = getStack(isPreview);
  const query = stack.contentType("page").entry().query();
  const result = await query.equalTo("url", \`/pages/${slug}\`).find();
  return result.entries?.\[0\] || null;
}

// app/pages/\[slug\]/page.tsx  -  Server component with preview support
import { draftMode } from "next/headers";
import { getPage } from "@/lib/contentstack";
import { PageDetail } from "./PageDetail";

export default async function Page({ params }: { params: { slug: string } }) {
  const dm = await draftMode();
  const isPreview = dm.isEnabled;
  const page = await getPage(params.slug, isPreview);

  if (!page) return <div>Not found</div>;

  return ;
}

// app/pages/\[slug\]/PageDetail.tsx  -  Client component for Live Preview
"use client";

import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import ContentstackLivePreview from "@contentstack/live-preview-utils";
import { getContentstackEndpoints } from "@timbenniks/contentstack-endpoints";

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

interface PageDetailProps {
  entry: any;
  isPreview: boolean;
}

export function PageDetail({ entry, isPreview }: PageDetailProps) {
  const \[currentEntry, setCurrentEntry\] = useState(entry);
  const router = useRouter();

  useEffect(() => {
    if (!isPreview) return;

    ContentstackLivePreview.init({
      ssr: true,
      enable: true,
      mode: "builder",
      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"\],
      },
    });

    ContentstackLivePreview.onEntryChange(() => {
      router.refresh();
    });
  }, \[isPreview, router\]);

  return (
    
  );
}

Notice the critical difference: in SSR mode, ContentstackLivePreview.init() is called with ssr: true, and the onEntryChange callback triggers window.location.reload() or router refreshes instead of re-fetching data on the client. The server handles the data fetching on each request.

## The onEntryChange callback in depth

The onEntryChange method from @contentstack/live-preview-utils is the primary hook for responding to Live Preview updates. Call it with your callback function. Its behavior differs depending on the ssr flag:

*   ssr: false: The callback fires every time the editor modifies a field. Your callback re-fetches through the SDK and gets updated content instantly without a network call because the SDK serves the data from the local message payload.
*   ssr: true: The callback fires when the editor modifies a field. Since the server needs to re-fetch, the typical implementation triggers a page reload or a router refresh (in Next.js App Router, you should use router.refresh() instead of window.location.reload() for a smoother experience).

// More nuanced SSR refresh using Next.js App Router
import { useRouter } from "next/navigation";

const router = useRouter();

ContentstackLivePreview.onEntryChange(() => {
  // router.refresh() re-runs server components without a full page reload
  router.refresh();
});

Using router.refresh() preserves client-side state (scroll position, form inputs, expanded accordions) while still re-fetching server data. This provides a significantly better editor experience than a full page reload.

## Hybrid pattern: SSR pages with CSR components

Real-world applications rarely use pure SSR or pure CSR. Veda, for example, might server-render the product page for SEO (title, description, images, structured data) but client-render an interactive related products component that fetches data in the browser.

For Live Preview, this means combining both patterns:

// app/pages/\[slug\]/page.tsx  -  Hybrid: SSR page with CSR component
import { draftMode } from "next/headers";
import { getPage } from "@/lib/contentstack";
import { PageDetail } from "./PageDetail";
import { RelatedProducts } from "./RelatedProducts";

export default async function Page({ params }: { params: { slug: string } }) {
  const dm = await draftMode();
  const isPreview = dm.isEnabled;
  const page = await getPage(params.slug, isPreview);

  return (
    <>
      {/\* SSR: rendered on the server with draft content \*/}
      

      {/\* CSR: rendered in the browser, fetches its own data \*/}
      
    
  );
}

The SSR portion uses the server-side preview pattern: the server fetches draft content using the Preview Token, and Live Preview triggers a structural route refresh on changes. The CSR portion uses the client-side preview pattern: it initializes its own SDK connection and uses the change event listener to re-render in place.

**Design Rule:** Avoid mixing the two patterns for the exact same field. If a field is fetched on the server, its preview updates should come through the server path. If a field is fetched on the client, its preview updates should come through the client path. Mixing patterns creates synchronization bugs where the server-rendered HTML and client-rendered updates disagree.

## Edit tags: connecting rendered content to CMS fields

The data-cslp attribute (Content Stack Live Preview) is how the Live Preview SDK and Visual Builder identify which rendered DOM element corresponds to which CMS field. The attribute value follows a specific format:

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

For a product entry with UID blt\_matrix\_link\_001 in the en-us locale:

The @contentstack/live-preview-utils package provides an addEditableTags() helper that automatically adds data-cslp attributes to your entry data, which you can then spread onto your components. Edit tags work identically in both SSR and CSR patterns. Whether the server rendered the HTML or the browser rendered it, the data-cslp attribute tells the Live Preview SDK which field each element represents, enabling the field-level highlighting that editors see when hovering over content in the preview panel.

## The postMessage bridge

Under the hood, all Live Preview communication flows through the browser's postMessage API. Understanding this mechanism helps when debugging preview issues.

The Contentstack entry editor (parent window) and your application (iframe) exchange messages with a defined protocol:

1.  **Handshake:** When the iframe loads, the Live Preview SDK sends an initialization message to the parent, confirming that it is ready to receive updates.
2.  **Entry change:** When the editor modifies a field, the parent sends a message containing the updated entry data, the content type UID, and the field that changed.
3.  **Hash update:** The parent sends an updated live\_preview hash that the SDK uses for subsequent API calls in SSR mode.
4.  **Navigation:** If the editor switches to a different entry, the parent sends a navigation message, and the SDK can trigger a URL change in the iframe context.

// Simplified view of what the Live Preview SDK handles internally
// You do not write this code  -  the SDK manages it
window.addEventListener("message", (event) => {
  if (event.origin !== "https://app.contentstack.com") return;

  const { type, data } = event.data;

  switch (type) {
    case "init":
      // Handshake complete  -  preview is active
      break;
    case "client-data-send":
      // Entry data updated  -  trigger onEntryChange callback
      updateEntryData(data);
      break;
    case "live-preview-hash":
      // New hash for SSR re-fetch
      updateLivePreviewHash(data.hash);
      break;
  }
});

The utility utilities handle all of this under the hood, but knowing it exists explains why Live Preview requires an iframe context, why cross-origin restrictions can block it, and why the SDK needs to know the Contentstack app host (clientUrlParams.host).

## Common mistakes

> **Common pitfall:**
> 
> Initializing the Live Preview SDK with ssr: false in a server-rendered app causes the SDK to intercept client-side SDK calls that never actually fetch the data -- the server does. The result is a confusing flicker where server HTML shows old content, the client briefly shows new content, then hydration conflicts cause unpredictable behavior.

### Mistake 1: initializing Live Preview SDK with ssr: false in an SSR application

If your application is server-rendered but you initialize the SDK with ssr: false, the SDK attempts to intercept Delivery SDK calls on the client side. But the data fetching happens on the server, so the interception does nothing. The editor modifies content, the callback fires, the client re-fetches, but the server-rendered HTML does not update. The result is a confusing visual mismatch.

### Mistake 2: forgetting to disable fetch caching in SSR preview mode

In Next.js App Router, fetch() calls in Server Components are cached by default. If you do not set cache: "no-store" or next: { revalidate: 0 } for preview requests, the server returns cached published content even though the Preview Token should return draft content. The editor saves a change, triggers a refresh, and sees the same old content because the framework served it from its fetch cache.

### Mistake 3: using window.location.reload() instead of router.refresh() in Next.js

A full page reload in Next.js discards all client-side state, resets scroll position, and requires a complete HTML re-parse. Using router.refresh() from next/navigation re-runs server components and updates the DOM without a full page reload, preserving the editor's context and providing a smoother preview experience.

#### Key takeaways

- Connect **SSR and CSR preview patterns** 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.

### Lesson 05 — Visual Builder - mental model and implementation

<!-- 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.

### Lesson 06 — Visual Builder - GraphQL, localization, and edge cases

<!-- ai_metadata: {"lesson_id":"06","type":"text","duration_minutes":1,"topics":["Visual","Builder","GraphQL","localization","and","edge"]} -->

#### Lesson text

# Visual Builder: GraphQL, localization, and edge cases

> **TL;DR**
> 
> *   GraphQL preview uses a separate endpoint (e.g., graphql-preview.contentstack.com) and requires the live\_preview hash header for real-time editing.
> *   data-cslp values always reference the content type schema field UID, not GraphQL aliases -- even if you alias fields in your query.
> *   For multi-locale sites, the locale in data-cslp must be dynamic and match the content being rendered; hardcoding en-us breaks Visual Builder for every other locale.
> *   Your preview host must include frame-ancestors 'self' https://app.contentstack.com in its CSP headers or Visual Builder cannot load it in the iframe.

Once the core Visual Builder implementation is working with the REST Content Delivery API, three categories of complexity remain: integrating with the GraphQL Content Delivery API, supporting multi-locale editing workflows, and handling the infrastructure edge cases that cause Visual Builder to fail silently in production environments. Each of these scenarios introduces specific configuration requirements that differ from the standard REST-based setup covered in [Lesson 4.1.4](/course-4-preview-visual-builder-releases/module-4-1-live-preview-and-visual-builder/04-visual-builder-implementation).

## Visual Builder with GraphQL

Contentstack offers both REST and GraphQL Content Delivery APIs. The Live Preview SDK supports both, but GraphQL requires additional configuration because the query structure and response shape differ from REST.

### GraphQL preview endpoint

The standard GraphQL fields endpoint follows the pattern https://{region-prefix}graphql.contentstack.com/stacks/{api\_key}. For preview, use the preview-specific GraphQL endpoint and pass the Preview Token instead of the Delivery Token. Every region has its own GraphQL preview host -- see the [full endpoint table in Lesson 3.1.2](/docs/developers/contentstack-regions/api-endpoints) for all seven regions, or use @timbenniks/contentstack-endpoints to resolve them from a region string.

// lib/graphql-client.ts  -  Preview-aware GraphQL client
import { getContentstackEndpoints } from "@timbenniks/contentstack-endpoints";

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

const GRAPHQL\_ENDPOINT = isPreview
  ? \`${endpoints.graphqlPreview}/stacks/${process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY}\`
  : \`${endpoints.graphqlDelivery}/stacks/${process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY}\`;

const AUTH\_TOKEN = isPreview
  ? process.env.NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW\_TOKEN
  : process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN;

export async function graphqlFetch(query: string, variables?: Record) {
  const response = await fetch(GRAPHQL\_ENDPOINT, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      access\_token: AUTH\_TOKEN!,
      environment: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_ENVIRONMENT!,
    },
    body: JSON.stringify({ query, variables }),
    cache: isPreview ? "no-store" : "force-cache",
  });

  const result = await response.json();
  return result.data;
}

### The live\_preview hash with GraphQL

When using the Live Preview SDK with GraphQL, the live\_preview hash must be passed as a query parameter or header on each GraphQL request during a live editing session. The SDK provides this hash when it detects an active Live Preview session.

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

const endpoints = getContentstackEndpoints(process.env.NEXT\_PUBLIC\_CONTENTSTACK\_REGION || "na");

export async function graphqlFetchWithPreview(
  query: string,
  variables?: Record,
  livePreviewHash?: string
) {
  const headers: Record = {
    "Content-Type": "application/json",
    access\_token: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW\_TOKEN!,
    environment: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_ENVIRONMENT!,
  };

  if (livePreviewHash) {
    headers\["live\_preview"\] = livePreviewHash;
  }

  const response = await fetch(
    \`${endpoints.graphqlPreview}/stacks/${process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY}\`,
    {
      method: "POST",
      headers,
      body: JSON.stringify({ query, variables }),
      cache: "no-store",
    }
  );

  const result = await response.json();
  return result.data;
}

The hash ensures that the GraphQL endpoint returns the exact in-memory state the editor is currently working on, not just the last saved draft. Without it, there can be a delay between the editor's keystrokes and the content appearing in the preview.

### Field paths in GraphQL vs REST responses

GraphQL responses follow the query structure you define, not the flat field structure of REST responses. This affects how data-cslp attribute values map to content.

Consider a documentation page content type with a title, body, and a group field seo containing meta\_title and meta\_description. The REST API returns:

{
  "uid": "blt9a8b7c6d5e4f3g2h",
  "title": "Our Story",
  "body": "<p>Welcome to the docs...</p>",
  "seo": {
    "meta\_title": "Our Story | Docs",
    "meta\_description": "Learn how to get started..."
  }
}

A GraphQL query for the same entry:

query GetDocPage($url: String!) {
  all\_page(where: { url: $url }) {
    items {
      uid
      title
      body
      seo {
        meta\_title
        meta\_description
      }
    }
  }
}

The GraphQL response nests the data, but the field structure within the entry is identical to REST. The data-cslp attribute values remain the same regardless of whether you used REST or GraphQL to fetch the data:

// data-cslp values are the SAME for both REST and GraphQL
<h1 data-cslp="page.blt9a8b7c6d5e4f3g2h.en-us.title">
  {entry.title}
</h1><span data-cslp="page.blt9a8b7c6d5e4f3g2h.en-us.seo.meta\_title">
  {entry.seo.meta\_title}
</span>

> **Common pitfall:**
> 
> If you alias fields in your GraphQL query (heroTitle: title) and then use the alias in data-cslp, Visual Builder cannot map the click to the correct field. The data-cslp must always use the original schema field UID (title), not the alias.

The data-cslp values always reference the content type schema path, not the GraphQL query path.

### Initializing Live Preview SDK with GraphQL

When using GraphQL without the Contentstack JavaScript Delivery SDK, you do not pass a stackSdk instance to ContentstackLivePreview.init(). Instead, you manage the data flow manually through the onEntryChange callback:

import ContentstackLivePreview from "@contentstack/live-preview-utils";
import { getContentstackEndpoints } from "@timbenniks/contentstack-endpoints";

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

ContentstackLivePreview.init({
  ssr: true,
  enable: true,
  mode: "builder",
  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"\],
  },
});

ContentstackLivePreview.onEntryChange(() => {
  router.refresh();
});

Without stackSdk, the Live Preview SDK cannot intercept and serve data from the message payload automatically. In CSR mode, this means you need to manually re-fetch the GraphQL query on each change. In SSR mode, triggering router.refresh() causes the server to re-run the GraphQL query with the updated live\_preview hash.

With the GraphQL integration covered, the next area of complexity is multi-locale editing.

## Localization with Visual Builder

Contentstack supports content localization across multiple locales. Visual Builder integrates with the locale system, allowing editors to switch between locales in the Contentstack UI and see the preview update to show the localized content.

### How locale switching works in Visual Builder

When an editor switches the locale in the Contentstack entry editor (using the locale dropdown), the platform sends a postMessage to the preview iframe indicating the new locale. The Live Preview SDK detects this change and can trigger an automatic content refresh.

The behavior depends on how your framework handles locale routing:

**Path-based locale routing (****/en-us/docs/getting-started****,** **/es/docs/primeros-pasos****):**

// The Live Preview SDK can trigger navigation to the locale-specific URL
ContentstackLivePreview.onEntryChange(() => {
  // The SDK provides the current locale context
  const locale = ContentstackLivePreview.getLocale();

  // Navigate to the locale-specific path
  const currentPath = window.location.pathname;
  const pathWithoutLocale = currentPath.replace(/^\\/(en-us|es|ja)/, "");
  window.location.href = \`/${locale}${pathWithoutLocale}\`;
});

**Query parameter or cookie-based locale routing:**

ContentstackLivePreview.onEntryChange(() => {
  const locale = ContentstackLivePreview.getLocale();

  // Update the locale query parameter and refresh
  const url = new URL(window.location.href);
  url.searchParams.set("locale", locale);
  window.location.href = url.toString();
});

### Tagging localized fields correctly

The locale component in the data-cslp attribute must match the actual locale of the content being rendered. If your page renders French content, the data-cslp must specify fr-fr, not en-us:

// Incorrect: hardcoding en-us when rendering French content
<h1 data-cslp="page.blt\_matrix\_link\_001.en-us.title">
  Premiers pas
</h1>

// Correct: using the actual locale
<h1 data-cslp="{\`page.blt\_matrix\_link\_001.${currentLocale}.title\`}">
  {entry.title}
</h1>

If the locale in data-cslp does not match the locale context in the Contentstack entry editor, Visual Builder cannot map the click to the correct localized field. The editor clicks on the French title, but Visual Builder tries to open the English version of that field.

Make the locale dynamic in your components:

function DocsPage({ entry, locale }: { entry: any; locale: string }) {
  return (
    
  );
}

### Locale fallback behavior in preview

Contentstack supports locale fallback: if an entry is not localized for ja-jp, it can fall back to en-us. In preview mode, this means the preview might show English content when the editor has selected Japanese. This is correct behavior - it matches what production would show - but it can confuse editors who expect to see localized content.

To make fallback behavior transparent, consider adding a visual indicator in your preview deployment when content is falling back:

function LocalizedField({ entry, field, locale, fallbackLocale }: {
  entry: any;
  field: string;
  locale: string;
  fallbackLocale: string;
}) {
  const isLocalized = entry.\_locale === locale;

  return (
    <div classname="{isLocalized" ?="" ""="" :="" "locale-fallback"}="">
      <span data-cslp="{\`page.${entry.uid}.${locale}.${field}\`}">
        {entry\[field\]}
      </span>
      {!isLocalized &amp;&amp; (
        <small classname="fallback-indicator">
          Showing {fallbackLocale} fallback
        </small>
      )}
    </div>
  );
}

With GraphQL and localization in place, the remaining challenges are infrastructure edge cases that cause Visual Builder to fail silently.

## Edge cases and troubleshooting

### Iframe restrictions: CSP and X-Frame-Options

Visual Builder loads your preview application in an iframe hosted on the Contentstack domain (for example, app.contentstack.com). If your preview deployment sends headers that block iframe embedding, Visual Builder cannot render your application.

Two headers commonly cause this conflict:

*   X-Frame-Options: Setting this to DENY or SAMEORIGIN prevents any cross-origin iframe embedding. For your preview host, you'll want to either remove this header or allow explicit origin mapping targets.
*   Content-Security-Policy with frame-ancestors: The modern replacement for X-Frame-Options. Your preview deployment must include the Contentstack app domain in the directive:

Content-Security-Policy: frame-ancestors 'self' https://app.contentstack.com https://\*.contentstack.com;

In Next.js, you configure this in next.config.js:

// next.config.js  -  Allow Contentstack to embed the preview site
const securityHeaders = \[
  {
    key: "Content-Security-Policy",
    value: "frame-ancestors 'self' https://app.contentstack.com https://\*.contentstack.com",
  },
\];

module.exports = {
  async headers() {
    return \[
      {
        source: "/:path\*",
        headers: securityHeaders,
      },
    \];
  },
};

You'll want to avoid applying this permissive policy to your production deployment. Only the preview host should allow iframe embedding by the Contentstack domain.

### Cross-origin issues

The Contentstack app runs on app.contentstack.com. Your preview runs on preview.example.com. The postMessage API works across origins, but other browser security features can interfere:

*   Cookies: If your preview application uses cookies for authentication or session management, they must be set with SameSite=None; Secure to work inside a cross-origin iframe context.
*   Storage access: In browsers with enhanced tracking protection, cross-origin iframes may lose access to localStorage and sessionStorage, meaning cached tokens or local tracking values do not persist.

### Authentication-gated preview pages

Some preview deployments require authentication to prevent unauthorized access to draft content. When the preview is loaded inside the Contentstack iframe, the authentication flow must work within that iframe context:

*   Basic auth: HTTP basic auth prompts inside an iframe can be problematic. Some browsers block the authentication prompt entirely in cross-origin iframes. Consider using token-based auth instead.
*   OAuth/SSO redirects: Redirecting to an OAuth provider from within an iframe often fails because OAuth providers set X-Frame-Options: DENY on their login pages. Use a token or secret passed via query parameter instead:

// Preview authentication via shared secret in the URL
// The Contentstack preview URL includes the secret as a query parameter
// Settings > Live Preview > Preview URL: https://preview.example.com?preview\_secret=your-secret

// Middleware to validate the preview secret
export function middleware(request: NextRequest) {
  const previewSecret = request.nextUrl.searchParams.get("preview\_secret");

  if (previewSecret !== process.env.PREVIEW\_SECRET) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  return NextResponse.next();
}

### Previewing content with unpublished references

When an entry references another entry (for example, a blog post referencing an author), both entries must be available through the Preview API for preview to work correctly. If the blog post is a draft and the referenced author entry has never been saved, the preview will show the blog post but the author field will be empty or cause an error.

The Preview Token returns the latest saved state of every entry, so as long as the referenced entry has been saved at least once (even without publishing), it will appear in preview. The edge case occurs when new reference UIDs are missing, items are deleted, or reference targets live in an inaccessible branch context. Build defensive conditional rendering layouts for reference fields in preview mode:

function BlogPost({ entry, isPreview }: { entry: any; isPreview: boolean }) {
  const author = entry.author?.\[0\];

  return (
    
      {author ? (
        
      ) : isPreview ? (
        
      ) : null}
    
  );
}

### Static site generators and Visual Builder

If your production site uses static site generation (SSG) without a runtime server, Visual Builder cannot work with the static output. Static HTML files do not re-fetch content on each request, so Live Preview updates have no mechanism to reach the rendered page.

The solution is to maintain a separate preview deployment that uses SSR or CSR, even if production uses SSG:

*   Production: Static HTML generated at build time, deployed to a CDN. No server runtime required.
*   Preview: SSR deployment (for example, server mode configuration) that fetches draft content on each request and supports Live Preview.

Both deployments share the same codebase. The production build uses static export configurations, while the preview target runs in server mode. This is the same dual-deployment pattern described in Lesson 4.1.2, with the additional consideration that the build mode differs between the two.

### Third-party scripts in iframe context

Some third-party scripts - analytics trackers, chatbots, A/B testing tools, and consent management platforms - behave unexpectedly or break entirely when loaded inside an iframe. Common issues include:

*   **Analytics:** The script detects the iframe context and records the Contentstack domain as the referrer, polluting production metrics. Exclude the preview host from analytics tracking loops.
*   **Chatbots:** Chat widgets attempt to resize or escape the iframe container, causing visual layout issues. Conditionally disable chat widgets in preview mode.
*   **Consent banners:** Cookie consent banners may not function correctly in cross-origin iframes due to browser storage access restrictions.

Conditionally disable these scripts in preview mode:

// Only load third-party scripts outside of preview mode
export function ThirdPartyScripts({ isPreview }: { isPreview: boolean }) {
  if (isPreview) return null;

  return (
    <>

#### Key takeaways

- Connect **Visual Builder - GraphQL, localization, and edge cases** 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.

### Lesson 07 — Scheduling, Releases, and Versioning : Overview

<!-- ai_metadata: {"lesson_id":"07","type":"text","duration_minutes":1,"topics":["Scheduling","Releases","and","Versioning","Overview"]} -->

#### Lesson text

# Scheduling, Releases, and Versioning

This module covers the future-state publishing tools that let teams ship coordinated content changes without relying on manual timing and hope.

## Why This Module Matters

As soon as multiple entries, locales, or stakeholders must go live together, simple publish actions stop being enough.

## You Will Be Able To

*   choose between direct scheduling, releases, and version-based rollback paths
*   reason about future-state preview and release coordination
*   avoid common failures around publish order, missing dependencies, and rollback sequencing

## Recommended Preparation

Complete Module 4.1 first so preview behavior and editorial validation are already familiar.

## Estimated Effort

75-90 minutes

## Practice Focus

Use the Veda storefront to coordinate campaign launches, preview future states, and understand what will happen before content reaches production.

## Suggested Next Step

Start with lesson 1 in this module and evaluate each publishing tool by risk, coordination need, and rollback behavior.

#### Key takeaways

- Connect **Scheduling, Releases, and Versioning : Overview** 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.

### Lesson 08 — Releases - scheduling and coordinated publishing

<!-- ai_metadata: {"lesson_id":"08","type":"text","duration_minutes":1,"topics":["Releases","scheduling","and","coordinated","publishing"]} -->

#### Lesson text

# Releases: scheduling and coordinated publishing

> **TL;DR**
> 
> *   A Release is a named collection of entries and assets that publish or unpublish together atomically, eliminating partial-deployment risk for multi-entry campaigns.
> *   Each item in a Release carries a publish or unpublish action, so a single Release can swap old content for new content in one operation.
> *   Scheduled Releases lock their item list to prevent last-minute unreviewed changes -- unschedule first to make modifications.
> *   Entries must be in a publishable workflow stage before the Release fires, or they will be skipped or block the deployment.

Publishing content one entry at a time works until it does not. The moment a campaign, product launch, or site redesign spans multiple entries across multiple content types, individual publishing becomes a coordination hazard. A hero banner goes live before the landing page it links to. A navigation item appears in the menu pointing to a page that does not exist yet. A promotional price shows up on the product detail page while the old price still renders on the category listing. These are not hypothetical failures. They happen every time teams try to synchronize content by publishing items one after another and hoping the timing holds.

Contentstack Releases exist to eliminate this class of problem. A Release is a named collection of entries and assets that publish or unpublish together, atomically, to a specified environment. Instead of coordinating fifteen separate publish actions across a team, you assemble the items into a Release, optionally schedule it for a future date, and deploy it as a single operation.

## What a Release contains

A Release is a container that holds references to specific entries and assets. Each item in a Release carries metadata about the intended action:

*   **Publish:** the item will be published to the target environment when the Release deploys.
*   **Unpublish:** the item will be removed from the target environment when the Release deploys.

This dual capability is critical for real-world content operations. A seasonal campaign does not just add content; it often replaces content. You might publish a new holiday hero banner while simultaneously unpublishing the previous autumn promotion. A Release handles both actions in one deployment.

Each Release deploy action targets selected environment(s) and locale(s). In most teams, you still promote deliberately (for example, staging first, then production) rather than deploying everywhere at once. This aligns with the promotion strategy covered in the [environments and publishing lesson](/course-3-apis-and-developer-tooling/module-3-3-environments-and-deployment/01-environments-publishing-promotion): content should move through environments intentionally.

## Creating a Release

Releases are managed under Publish Queue > Releases in the Contentstack UI. To create a new Release:

1.  Navigate to Publish Queue in the left sidebar.
2.  Select Releases.
3.  Click + New Release.
4.  Provide a descriptive name (e.g., "Holiday Collection 2025" or "Digital Dawn Launch").
5.  Optionally add a description explaining the scope and purpose.

The name matters more than you might think. In organizations running multiple concurrent campaigns, vague names like "Updates" or "New content" become indistinguishable in the Release list within days. Use names that encode the campaign, date, or business context.

## Adding entries and assets to a Release

There are three ways to add items to an existing Release:

### From the entry editor

When editing any entry, click the Release icon or use the publish dropdown to select Add to Release instead of publishing directly. This lets editors flag content for coordinated publishing as part of their normal workflow without needing to navigate away from the entry.

### From bulk actions

In the entry list view for any content type, select multiple entries using the checkboxes and choose Add to Release from the bulk action menu. This is the efficient path when you know exactly which entries need to be part of a campaign. For example, selecting all products in the Digital Dawn collection for a coordinated launch.

### Via the Release detail screen

Open an existing Release and use the Add Items interface to search for and add entries or assets. This approach works well when a Release manager is assembling a deployment package from a list of requirements provided by the editorial or marketing team.

> **Common pitfall:**
> 
> If an entry in a scheduled Release is still in a non-publishable workflow stage (e.g., "Review") when the Release fires, it may be silently skipped or block the entire deployment -- and teams often discover this only after the campaign goes live incomplete.

## Scheduling a Release

The real power of Releases emerges when you schedule them for future deployment. A scheduled Release publishes (or unpublishes) all its items automatically at the specified date and time without manual intervention.

To schedule a Release:

1.  Open the Release from Publish Queue > Releases.
2.  Click Schedule Release.
3.  Select the target environment (e.g., production).
4.  Set the date and time for deployment.
5.  Choose the locale if your stack uses localization.
6.  Confirm the schedule.

Once scheduled, the Release enters a locked state. You cannot add or remove items from a scheduled Release without first unscheduling it. This prevents last-minute unreviewed changes from slipping into a coordinated deployment.

Scheduling is timezone-aware. Set the deployment time according to your business needs. For Veda running a Holiday Collection launch, you might schedule the Release for midnight when your primary customer base is active, even if your editorial team works in a different timezone.

## The Release API

Releases can be created and managed programmatically through the Content Management API. This enables CI/CD integration, automated Release assembly from external planning tools, and scripted campaign management.

Creating a Release via API

// create-release.ts  -  programmatically create a Holiday Collection Release
const response = await fetch("https://api.contentstack.io/v3/releases", {
  method: "POST",
  headers: {
    api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
    authorization: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_MANAGEMENT\_TOKEN!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    release: {
      name: "Holiday Collection 2025",
      description:
        "Homepage hero, new Digital Dawn products, and navigation updates for Holiday launch.",
    },
  }),
});

const { release } = await response.json();
console.log(\`Release created: ${release.uid}\`);

Adding items to a Release via API

// add-items-to-release.ts  -  add entries to the Holiday Collection Release
const releaseUid = "blt\_holiday\_collection\_001";

const items = \[
  {
    uid: "blt\_digital\_dawn\_hero\_001",
    content\_type\_uid: "page",
    version: 3,
    locale: "en-us",
    action: "publish",
  },
  {
    uid: "blt\_matrix\_link\_001",
    content\_type\_uid: "product",
    version: 7,
    locale: "en-us",
    action: "publish",
  },
  {
    uid: "blt\_old\_campaign\_page\_019",
    content\_type\_uid: "page",
    version: 2,
    locale: "en-us",
    action: "unpublish",
  },
\];

const response = await fetch(
  \`https://api.contentstack.io/v3/releases/${releaseUid}/items\`,
  {
    method: "POST",
    headers: {
      api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
      authorization: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_MANAGEMENT\_TOKEN!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ items }),
  }
);

Notice that each item specifies an action field. This is how a single Release can both publish new campaign content and unpublish old content in one atomic operation.

Deploying a Release via API

// deploy-release.ts  -  deploy the Release to production
const releaseUid = "blt\_digital\_dawn\_001";

const response = await fetch(
  \`https://api.contentstack.io/v3/releases/${releaseUid}/deploy\`,
  {
    method: "POST",
    headers: {
      api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
      authorization: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_MANAGEMENT\_TOKEN!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      release: {
        environments: \["production"\],
      },
    }),
  }
);

## Worked example: Holiday Collection campaign

Consider Veda preparing for the Holiday Collection launch. The campaign touches content across multiple content types:

Content type

Entry

Action

Page

Homepage with Holiday hero

Publish

Product

8 Digital Dawn products

Publish

Product Line

Digital Dawn collection update

Publish

Header

"Holiday" menu item added

Publish

Page

Previous campaign page

Unpublish

That is 12 entries across 4 content types, plus an unpublish action. Without Releases, an editor would need to manually publish each entry and remember to unpublish the old banner. The margin for error is significant.

With a Release named "Holiday Collection 2025":

1.  Over the preceding weeks, editors prepare all entries and add them to the Release as they reach final approval.
2.  The Release manager reviews the complete item list to verify nothing is missing.
3.  The Release is scheduled for November 28 at 00:00 EST.
4.  At midnight, all 12 entries deploy atomically. The new hero, updated prices, promotional banner, and navigation change appear together. The old autumn banner disappears.

The customer experience is seamless: one moment the site shows the previous campaign, the next moment it shows the Holiday Collection. No intermediate state where half the campaign is live and half is not.

## Releases vs. individual scheduled publishing

Contentstack also supports scheduling individual entry publishes. An editor can set any single entry to publish at a future date and time. So when should you use Releases instead of individual schedules?

Scenario

Individual schedule

Release

Single blog post going live at 9 AM

Appropriate

Overkill

One product price update

Appropriate

Overkill

Campaign with 5+ entries that must go live together

Risky

Required

Content swap (publish new, unpublish old)

Error-prone

Clean

Cross-content-type coordinated launch

Fragile

Designed for this

Individual schedules operate independently. If one fails, the others still fire. That independence is a feature for isolated content changes and a liability for coordinated campaigns. Releases treat the group as a unit.

## Release constraints and limitations

Understanding Release limitations prevents surprises at deployment time:

*   **Workflow stage requirement:** entries must be in a publishable workflow stage before they can be deployed as part of a Release. If an entry is stuck in "Review" when the Release fires, the entire Release deployment may fail or that item will be skipped (depending on configuration). Coordinate with your workflow design (covered in [Module 5.1](/course-5-workflow-branches-collaboration/module-5-1-workflow-and-content-lifecycle/01-content-lifecycle)) to ensure entries reach a publishable stage before the scheduled Release time.
*   **Environment and locale selection:** a Release deploy action can target one or more environments and one or more locales. Many teams still choose one environment at a time to maintain staged promotion control.
*   **Locked when scheduled:** once a Release is scheduled, its item list is frozen. To add a last-minute entry, you'll need to unschedule, modify, and reschedule.
*   **Item limits:** Releases have a maximum number of items per Release. For very large operations (hundreds of entries), you may need to split across multiple Releases or use bulk publish operations.
*   **No partial deploy:** you cannot deploy a subset of items from a Release. It is all or nothing. If you realize one entry in a fifteen-item Release is not ready, you'll need to either remove it from the Release or delay the entire deployment.

## Common mistakes

### Mistake 1: adding entries that have not completed workflow review

An editor adds an entry to a Release while it is still in draft or review workflow stage. When the scheduled Release fires, the entry cannot be published because it has not been approved. Depending on the Release configuration, this may block the entire deployment or silently skip the entry, leaving the campaign incomplete. Always verify workflow stages before scheduling a Release.

### Mistake 2: forgetting to include referenced assets

A Release contains entries that reference product images, but the images themselves are not in the Release and have not been published to the target environment. After deployment, the entries are live but render with broken image references. When assembling a Release, check that all referenced assets are either already published to the target environment or included in the Release.

### Mistake 3: scheduling without stakeholder review of the complete Release

The Release contains the right entries, but no one reviewed the full list as a whole before scheduling. After deployment, the team discovers a missing entry or an entry that should not have been included. Treat Release review as a gate: before scheduling, the Release manager should walk through every item with the campaign owner.

## Summary

Releases transform multi-entry publishing from a coordination problem into a managed operation. By grouping entries and assets into a named collection, scheduling them for a specific time, and deploying them atomically, Releases eliminate the risk of partial or inconsistent content states. The Release API enables programmatic assembly and deployment, supporting CI/CD integration and automated campaign management. The key constraints to internalize are that entries must be in publishable workflow stages and that scheduled Releases are locked until unscheduled.

#### Key takeaways

- Connect **Releases - scheduling and coordinated publishing** 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.

### Lesson 09 — Entry versioning, comparison, and rollback

<!-- ai_metadata: {"lesson_id":"09","type":"text","duration_minutes":1,"topics":["Entry","versioning","comparison","and","rollback"]} -->

#### Lesson text

# Entry versioning, comparison, and rollback

> **TL;DR**
> 
> *   Every save creates a new, immutable, complete snapshot of the entry -- not a diff. Any version can be loaded independently.
> *   Restoring a previous version creates a new version (it never overwrites history), preserving a full audit trail for compliance.
> *   Restoring does not republish: the restored content updates the draft, and you'll want to explicitly publish it to push changes live.
> *   The Version API provides programmatic access to the full history, enabling automated compliance reporting and change audits.

Every time you save an entry in Contentstack, the system creates a new version. Not a diff. Not a delta. A complete, immutable snapshot of the entry at that moment. This happens automatically, silently, and without any action from the editor beyond clicking Save. Version 1 is the initial creation. Version 2 is the first edit. Version 47 is the forty-seventh save. The version number only increments; it never resets, and previous versions are never overwritten.

This versioning behavior is not a convenience feature bolted on for power users. It is the foundation of content auditability, error recovery, and regulatory compliance. In industries like pharmaceuticals, finance, and healthcare, the ability to prove exactly what content was published at a specific point in time and who changed it is a legal requirement, not a nice-to-have.

## How versioning works in practice

When an editor opens an entry and makes changes, nothing happens to the version history until they click Save. The act of saving creates a new version with the complete state of all fields. If an editor opens an entry, changes the title, and saves, version N+1 contains the new title plus the unchanged values of every other field. If they then change a description field and save again, version N+2 contains the new description, the previously changed title, and all other field values.

This means any version is a self-contained snapshot. You do not need to reconstruct it from a chain of diffs. You can load version 12 and see exactly what every field contained at that point.

### Version metadata

Each version carries metadata beyond the field values:

*   **Version number:** the sequential integer identifier.
*   **Created by:** the user who performed the save.
*   **Created at:** the timestamp of the save.
*   **Locale:** the locale in which the save occurred (localized entries have independent version histories per locale).

This metadata provides the audit trail. For a pharmaceutical company tracking changes to drug information pages, the combination of who, when, and what changed constitutes a compliance record.

## Viewing version history

To access the version history of any entry:

1.  Open the entry in the entry editor.
2.  Click the Versions tab (or the version indicator, depending on UI layout).
3.  The version list appears, showing each version with its number, author, and timestamp.

The version list is ordered from newest to oldest. Each version is clickable, allowing you to inspect the complete state of the entry at that point in time.

## Comparing versions

Contentstack provides a diff view that shows field-by-field changes between any two versions. This is invaluable when you need to understand what changed and when.

To compare versions:

1.  Open the entry's version history.
2.  Select two versions to compare.
3.  The diff view highlights additions, deletions, and modifications for each field.

The comparison is field-level, not character-level. If a Rich Text Editor field changed, the diff shows the old and new content for that field. If a reference field was updated, the diff shows which references were added or removed. Fields that did not change between the two versions are either hidden or shown as unchanged, depending on the view settings.

### What the diff view reveals

Consider a product entry for a pharmaceutical company. Between version 5 and version 8, the diff might expose targeted field variances across compliance values:

Field

Version 5

Version 8

dosage\_instructions

"Take once daily with food"

"Take once daily with or without food"

side\_effects

(unchanged)

(unchanged)

regulatory\_status

"Pending review"

"Approved - FDA 2025-03-15"

last\_reviewed\_by

"Dr. Smith"

"Dr. Patel"

This view immediately answers the question: "What changed between the version that was under review and the version that was approved?" For compliance purposes, this diff is a reviewable artifact.

## Restoring a previous version

When a content error needs to be corrected quickly, or a recent change proves problematic, restoring a previous version is the fastest recovery path.

To restore a version:

1.  Open the entry's version history.
2.  Navigate to the version you want to restore.
3.  Click Restore on that version.

Critically, restoring a version does not delete any history. It does not rewind the version counter. Instead, it creates a new version with the content from the selected historical version. If the entry is currently at version 10 and you restore version 7, the entry moves to version 11, which contains the exact content from version 7. Versions 8, 9, and 10 remain in the history, fully accessible.

This non-destructive behavior is essential for audit compliance. Restoration is itself a tracked event. You can always see that version 11 was a restoration of version 7, and you can still inspect versions 8 through 10 to understand what happened between the original and the restoration.

> **Common pitfall:**
> 
> Restoring a previous version does not automatically publish it. If you restore and forget to publish, the live site continues serving the old (incorrect) content even though the draft looks correct in the entry editor.

### Restoring does not republish

Restoring a version updates the draft state of the entry. It does not automatically publish the restored content. After restoration, you'll want to explicitly publish the entry to the desired environment for the change to be reflected on the live site. This separation is intentional: it gives teams a chance to verify the restored content before it goes live, rather than blindly pushing a historical snapshot to production.

For the pharmaceutical example, if a drug information page was updated with incorrect dosage information (version 10), the process would follow this flow loop:

1.  Identify the last known correct version (version 9).
2.  Restore version 9, creating version 11.
3.  Review version 11 to confirm correctness.
4.  Publish version 11 to the production environment.

Each step is auditable. The compliance team can trace exactly when the error was introduced (version 10), when it was corrected (version 11), and when the correction reached the live site (the publish event).

## The Version API

Version history is accessible programmatically through the Content Management API. This enables automated auditing, compliance reporting, and integration with external change management systems.

Fetching all versions of an entry

// get-versions.ts  -  retrieve version history for a drug information entry
const contentTypeUid = "drug\_information";
const entryUid = "blt\_matrix\_link\_bracelet";

const response = await fetch(
  \`https://api.contentstack.io/v3/content\_types/${contentTypeUid}/entries/${entryUid}/versions\`,
  {
    headers: {
      api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
      authorization: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_MANAGEMENT\_TOKEN!,
    },
  }
);

const { versions } = await response.json();
versions.forEach((version: any) => {
  console.log(
    \`v${version.\_version} | ${version.updated\_at} | ${version.updated\_by}\`
  );
});

Fetching a specific version

// get-specific-version.ts  -  retrieve version 7 for compliance review
const contentTypeUid = "drug\_information";
const entryUid = "blt\_matrix\_link\_bracelet";
const versionNumber = 7;

const response = await fetch(
  \`https://api.contentstack.io/v3/content\_types/${contentTypeUid}/entries/${entryUid}/versions/${versionNumber}\`,
  {
    headers: {
      api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
      authorization: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_MANAGEMENT\_TOKEN!,
    },
  }
);

const { entry } = await response.json();
console.log(\`Version ${versionNumber} title: ${entry.title}\`);
console.log(\`Dosage: ${entry.dosage\_instructions}\`);
console.log(\`Regulatory status: ${entry.regulatory\_status}\`);

Building an automated audit report

// audit-report.ts  -  generate a compliance report for all changes in a date range
const contentTypeUid = "drug\_information";
const entryUid = "blt\_matrix\_link\_bracelet";
const auditStart = "2025-01-01T00:00:00.000Z";
const auditEnd = "2025-06-30T23:59:59.000Z";

const response = await fetch(
  \`https://api.contentstack.io/v3/content\_types/${contentTypeUid}/entries/${entryUid}/versions\`,
  {
    headers: {
      api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
      authorization: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_MANAGEMENT\_TOKEN!,
    },
  }
);

const { versions } = await response.json();
const auditWindow = versions.filter((v: any) => {
  const date = new Date(v.updated\_at);
  return date >= new Date(auditStart) && date <= new Date(auditEnd);
});

console.log(\`Audit period: ${auditStart} to ${auditEnd}\`);
console.log(\`Total changes: ${auditWindow.length}\`);
auditWindow.forEach((v: any) => {
  console.log(\`  v${v.\_version} | ${v.updated\_at} | Changed by: ${v.updated\_by}\`);
});

This kind of programmatic access turns Contentstack's version history into a data source for compliance dashboards, change management tickets, and regulatory filing evidence.

## How publishing relates to versions

The relationship between versions and publishing is straightforward but frequently misunderstood:

*   Publishing always publishes the current version. When you click Publish on an entry, the version that is currently active (the latest saved version) is what gets published.
*   Publishing does not create a new version. The act of publishing is a distribution event, not a content change.
*   Restoring a version changes the current version but does not publish. After restoring, the entry's draft state reflects the restored content, but the live (published) content remains whatever was last published until you explicitly publish again.

This means the published content and the current draft content can be different versions. An editor might be working on version 12 (draft) while version 10 is still the published version on production. This is normal and expected. The publishing action bridges the gap when the draft is ready.

## Asset versioning

Assets in Contentstack also maintain version history. When you upload a new file to an existing asset, the previous file is preserved as a historical version. This applies to images, PDFs, videos, and any other file type managed as an asset.

Asset versioning behaves similarly to entry versioning:

*   Each upload creates a new version.
*   Previous versions are retained and accessible.
*   You can restore a previous asset version, which creates a new version with the old file.
*   Asset versions carry metadata: upload timestamp, user, file size, dimensions (for images).

For the pharmaceutical company, this means that if a product data sheet PDF is updated with incorrect information, the compliance team can restore the previous PDF version without losing the audit trail of the erroneous upload.

**Operational Step:** One important distinction: asset versions are file-level, not field-level. An asset is primarily its file. When you compare asset versions, you are comparing files (and their metadata), not a structured set of fields like you would with an entry.

## Named versions

Contentstack supports setting a name on a version to mark it with a human-readable label. This is useful for identifying significant milestones in an entry's lifecycle without relying solely on version numbers.

For example, after a drug information page passes regulatory review, you might name that version "FDA Approved - March 2025". Later, when reviewing the version history, this label immediately identifies the compliance milestone without requiring the reviewer to open each version and inspect its contents.

Named versions are particularly valuable when version numbers grow large. On an entry with 50+ versions, a named version acts as a bookmark, letting teams quickly locate the approved baseline, the pre-launch state, or the last known good version.

## Practical use cases

### Rolling back a content error

An editor publishes an entry with a typo in a critical field (a wrong phone number on a contact page). The correction path is clean: open the version history, restore the pre-error state, verify, and publish to production. Total recovery time completes in minutes without manual layout reconstruction.

### Comparing before and after a major content update

A marketing team overhauls the homepage content. Two weeks later, conversion rates drop. The product manager asks: "What exactly changed?" The diff view between the pre-overhaul version and the current version shows every field-level change, enabling data-driven analysis of what content changes correlated with the metric shift.

### Auditing content changes for compliance

A regulatory body requests evidence of all changes to product safety information over the past year. The Version API provides programmatic access to every version, every timestamp, and every author, enabling automated report generation without manual log review.

## Common mistakes

### Mistake 1: assuming restore republishes content

An editor restores a previous version expecting the live site to update immediately. It does not. Restoration updates the draft; publishing updates the live site. These are separate actions. Always publish after restoring if you need the change to reach a live environment.

### Mistake 2: conflating version numbers with publish events

Version 15 is not necessarily the version that is currently live. The published version might be version 12 if no one has published since version 12. Check the publish details in the entry editor or the publish queue to determine which version is live on each environment.

### Mistake 3: ignoring locale-specific version histories

In a localized stack, each locale has its own version history for an entry. Restoring version 5 of the English locale does not affect the French locale's version history. When rolling back content, verify you are operating in the correct locale context, especially in stacks with many locales where the locale selector can be easy to overlook.

## Summary

Contentstack's automatic versioning creates a complete, immutable audit trail of every content change. Each save produces a new version. Any two versions can be compared field by field. Any historical version can be restored non-destructively, creating a new version rather than erasing history. The Version API enables programmatic access for compliance reporting and automated auditing. Publishing and versioning are related but distinct operations: versions track content changes, while publishing distributes a specific version to an environment. Understanding this separation is critical for both day-to-day content operations and regulatory compliance scenarios.

#### Key takeaways

- Connect **Entry versioning, comparison, and rollback** 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.

### Lesson 10 — Previewing future states with timeline

<!-- ai_metadata: {"lesson_id":"10","type":"text","duration_minutes":1,"topics":["Previewing","future","states","with","timeline"]} -->

#### Lesson text

# Previewing future states with timeline

> **TL;DR**
> 
> *   Standard Live Preview shows a single entry's draft; "time travel" preview composites all scheduled changes to show the full site state at a target date.
> *   Three implementation approaches exist: preview all drafts (simplest), date-parameterized preview (more targeted), or Release-scoped preview (most precise but most code).
> *   Overlapping Releases targeting the same entry on the same date have no automatic conflict resolution -- the last one to execute wins.
> *   Future-state preview requires a separate preview environment with no-cache policies and context-aware content resolution.

Scheduling content for the future is only half the problem. The other half is answering the question editors ask immediately after scheduling: "What will the site look like when this goes live?" If the answer is "you will find out when it publishes," you have a trust gap. Editors are scheduling content they cannot verify, which means they are either deploying blind or building elaborate workarounds like publishing to a hidden environment, taking screenshots, and then unpublishing. Neither approach scales.

Contentstack addresses this with capabilities that allow previewing future content states before their scheduled publish date. By combining Live Preview, entry timeline, the publish queue, and Release-aware preview, teams can validate exactly what visitors will see at a future point in time without prematurely pushing content to a live environment.

## The "time travel" concept

Traditional preview shows content in its current draft state. Time travel preview extends this by allowing you to see content as it will appear at a specific future date after scheduled publishes and Releases have executed.

Consider a retail brand preparing for the holiday season with this schedule distribution matrix:

Scheduled date

Content change

November 15

Publish "Holiday Collection" homepage hero

November 28

Release: Holiday Collection (12 entries)

December 1

Publish "Cyber Monday" promotional banner

December 15

Release: Digital Dawn Launch (8 entries)

December 26

Release: Post-holiday clearance (unpublish holiday content, publish clearance)

Without time travel preview, the editorial team has no way to verify what the homepage looks like on December 1 versus December 15 versus December 26. Each date represents a different combination of published and unpublished content, and the interactions between these scheduled actions may produce unexpected results. With time travel preview, an editor can select "December 15" and see the homepage with the Digital Dawn Launch content live, the Holiday Collection still active (or replaced, depending on the Release configuration), and campaign banners visible or expired. The preview renders the cumulative state at that point in time.

## Entry timeline: the publication history of a single entry

Every entry in Contentstack has a timeline that records its publication history: when it was published, to which environment, and when it was unpublished. This timeline is distinct from the version history (covered in the [versioning lesson](/course-4-preview-visual-builder-releases/module-4-2-scheduling-releases-versioning/02-versioning-comparison-rollback)). Version history tracks content changes (saves). The timeline tracks distribution events (publishes and unpublishes).

To view an entry's timeline:

1.  Open the entry in the entry editor.
2.  Look for the Timeline or Publish Details section.
3.  The timeline shows past publish events and any scheduled future publish actions.

The timeline answers infrastructure and history questions cleanly:

*   When was this entry last published to production?
*   Is there a scheduled publish pending for this entry?
*   Was this entry ever unpublished from staging, and when?

For the retail brand, the homepage hero entry's timeline might show the following parameters:

Date

Action

Environment

Version

Oct 1, 2025

Published

production

v5

Nov 15 (scheduled)

Publish

production

v8

This tells the editor that version 5 is currently live, and version 8 will automatically replace it on November 15.

## The publish queue: monitoring scheduled actions

The Publish Queue in Contentstack provides a centralized view of all pending, in-progress, and completed publish actions across the stack. This is where you monitor what is scheduled and when.

Navigate to Publish Queue in the left sidebar to see:

*   Pending items: scheduled publishes and Releases waiting to execute.
*   In Progress items: publish actions currently being processed.
*   Completed items: recently executed publish actions with their status (success or failure).

For the retail brand with five scheduled dates in November and December, the publish queue serves as the campaign calendar. The editorial director can open the queue and see every scheduled action, verify the timing, and identify potential conflicts (two Releases scheduled for the same minute, for instance).

The publish queue is also accessible via the Content Management API:

// check-scheduled-actions.ts  -  list all pending scheduled publishes
const response = await fetch(
  "https://api.contentstack.io/v3/publish-queue?status=scheduled",
  {
    headers: {
      api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
      authorization: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_MANAGEMENT\_TOKEN!,
    },
  }
);

const { queue } = await response.json();
queue.forEach((item: any) => {
  const name = item.entry?.title || item.asset?.title || item.release?.name;
  console.log(
    \`${name} → ${item.environment} | Scheduled: ${item.scheduled\_at}\`
  );
});

This programmatic access enables building a custom campaign dashboard that shows all scheduled content operations in one view, something the marketing team can reference without needing direct access to the Contentstack UI.

## Previewing scheduled content with Live Preview

Live Preview, covered in depth in the [preview requirements lesson](/course-4-preview-visual-builder-releases/module-4-1-live-preview-and-visual-builder/01-preview-requirements-and-concepts), renders draft content in the context of your frontend application. When combined with scheduled content, Live Preview becomes a future-state validation tool.

The core mechanism: entries that are staged for a scheduled publish exist in a saved (draft) state within Contentstack. Live Preview can render this draft state even though the entry has not yet been published. This means an editor can:

1.  Create or update an entry (e.g., the "Digital Dawn Launch" page).
2.  Add it to a Release scheduled for December 15.
3.  Open Live Preview to see how the entry renders in the frontend.

The preview shows the content as it will appear after publishing. The editor verifies layout, imagery, copy, and references before the scheduled date arrives.

> **Common pitfall:**
> 
> Standard Live Preview shows only the current entry's draft -- it does not composite all entries from a scheduled Release into a combined view. Approving a Release based on single-entry previews can miss cross-entry layout conflicts or broken references.

### Limitation: single-entry preview vs. full-state preview

Standard Live Preview shows the current entry in its draft state. It does not automatically composite all entries from a scheduled Release into a single preview view. If the Digital Dawn Launch Release contains eight entries (a page, three products, a navigation update, two hero components, and a product line), previewing any single entry shows that entry's draft content but not necessarily the combined effect of all eight changes on the full page. Achieving full-state preview requires additional implementation work, which we cover in the next section.

## Implementing Release-aware preview

To preview the complete future state of a page affected by a Release, your preview endpoint needs to serve content that reflects the Release's cumulative changes. This goes beyond standard Live Preview and involves architectural decisions in your frontend application.

### Approach 1: preview all draft content

The simplest approach is to configure your preview environment to always fetch draft content rather than published content. Since all entries in a scheduled Release have been saved (and are therefore in draft state), a preview endpoint that fetches draft content will naturally show the future state.

The examples choose specific destination endpoints (e.g., rest-preview.contentstack.com, cdn.contentstack.io). For EU/Azure/GCP stacks, use the matching regional hosts.

// preview-handler.ts  -  serve draft content for future-state preview
import contentstack from "@contentstack/delivery-sdk";
import { getContentstackEndpoints } from "@timbenniks/contentstack-endpoints";

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

export function createPreviewStack() {
  return contentstack.stack({
    apiKey: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
    deliveryToken: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW\_TOKEN!,
    environment: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_ENVIRONMENT!,
    region: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_REGION,
    live\_preview: {
      enable: true,
      preview\_token: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW\_TOKEN!,
      host: endpoints.preview,
    },
  });
}

// In your page route handler:
export async function getPreviewPageData(slug: string) {
  const stack = createPreviewStack();
  const result = await stack
    .contentType("page")
    .entry()
    .query()
    .where("url", slug)
    .find();

  return result.entries\[0\];
}

This approach works well when your preview environment is isolated and dedicated to editorial validation. The trade-off is that it shows all draft content, not just the content in a specific Release. If multiple editors are working on different content simultaneously, the preview might show changes that are not part of the Release being validated.

### Approach 2: date-parameterized preview

A more targeted approach is to build a preview endpoint that accepts a date parameter and filters content based on what will be published by that date. This requires your frontend to resolve scheduled actions and serve the appropriate content version.

// future-state-preview.ts  -  preview site state at a specific future date
interface PreviewContext {
  targetDate: string; // ISO date string
  environment: string;
  locale: string;
}

export async function getFutureStateContent(
  contentTypeUid: string,
  entryUid: string,
  ctx: PreviewContext
) {
  // Step 1: Check whether a scheduled publish exists for this entry
  // before the target date
  const queueResponse = await fetch(
    \`https://api.contentstack.io/v3/publish-queue?content\_type\_uid=${contentTypeUid}&entry\_uid=${entryUid}&status=scheduled\`,
    {
      headers: {
        api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
        authorization: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_MANAGEMENT\_TOKEN!,
      },
    }
  );

  const { queue } = await queueResponse.json();
  const scheduledBeforeTarget = queue.filter(
    (item: any) => new Date(item.scheduled\_at) <= new Date(ctx.targetDate)
  );

  if (scheduledBeforeTarget.length > 0) {
    // A scheduled publish exists  -  fetch the draft version
    // that will be published
    const entryResponse = await fetch(
      \`https://rest-preview.contentstack.com/v3/content\_types/${contentTypeUid}/entries/${entryUid}\`,
      {
        headers: {
          api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
          preview\_token: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW\_TOKEN!,
        },
      }
    );
    return entryResponse.json();
  }

  // No scheduled change  -  return the currently published version
  const entryResponse = await fetch(
    \`https://cdn.contentstack.io/v3/content\_types/${contentTypeUid}/entries/${entryUid}\`,
    {
      headers: {
        api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
        access\_token: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN!,
      },
    }
  );
  return entryResponse.json();
}

This approach resolves the correct content version based on the target date. For entries with scheduled changes before the target date, it serves the draft (future) version. For entries without pending changes, it serves the currently published version. The result is a composite view that reflects what the site will look like at the specified date.

### Approach 3: Release-scoped preview

The most precise approach is to build preview logic that is aware of specific Releases. Given a Release UID, the preview endpoint fetches all items in that Release and renders them together with the current published state of everything else.

// release-preview.ts  -  preview the combined effect of a specific Release
export async function getReleasePreviewData(releaseUid: string) {
  // Fetch all items in the Release
  const releaseResponse = await fetch(
    \`https://api.contentstack.io/v3/releases/${releaseUid}/items\`,
    {
      headers: {
        api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
        authorization: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_MANAGEMENT\_TOKEN!,
      },
    }
  );

  const { items } = await releaseResponse.json();

  // Build a map of entries that will change when this Release deploys
  const releaseEntries = new Map();
  items.forEach((item: any) => {
    releaseEntries.set(item.uid, {
      action: item.action,
      uid: item.uid,
    });
  });

  return releaseEntries;
}

// In your page rendering logic, use this map to decide
// whether to fetch draft or published content for each entry
export async function resolveEntryForPreview(
  entryUid: string,
  contentTypeUid: string,
  releaseEntries: Map
) {
  const releaseItem = releaseEntries.get(entryUid);

  if (releaseItem?.action === "unpublish") {
    // This entry will be removed  -  return null or placeholder
    return null;
  }

  if (releaseItem?.action === "publish") {
    // This entry is in the Release  -  fetch draft version
    const response = await fetch(
      \`https://rest-preview.contentstack.com/v3/content\_types/${contentTypeUid}/entries/${entryUid}\`,
      {
        headers: {
          api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
          preview\_token: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_PREVIEW\_TOKEN!,
        },
      }
    );
    return response.json();
  }

  // Not in the Release  -  fetch currently published version
  const response = await fetch(
    \`https://cdn.contentstack.io/v3/content\_types/${contentTypeUid}/entries/${entryUid}\`,
    {
      headers: {
        api\_key: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_API\_KEY!,
        access\_token: process.env.NEXT\_PUBLIC\_CONTENTSTACK\_DELIVERY\_TOKEN!,
      },
    }
  );
  return response.json();
}

This is the most accurate preview because it shows exactly what will change when a specific Release deploys, without being affected by unrelated draft content from other editors or future Releases.

## Combining Releases with Live Preview for campaign validation

The highest-value workflow combines Release management with Live Preview to give editors a complete campaign validation experience. Here is how this works for the retail brand's holiday campaign:

1.  **Assemble the Release:** the editorial team creates a Release named "Digital Dawn Launch - Dec 15" and adds all eight entries.
2.  **Preview individual entries:** each editor uses Live Preview to verify their individual entries render correctly (layout, images, copy).
3.  **Preview the composite page:** using a Release-aware preview endpoint, the editorial director previews the homepage as it will appear on December 15, with all eight entries composited together alongside unchanged published content.
4.  **Validate cross-entry interactions:** the director verifies that the new navigation item links to the new landing page, that the promotional banners do not overlap with existing banners, and that the hero image renders at the correct dimensions.
5.  **Approve and schedule:** after validation, the Release is scheduled with confidence that the future state has been verified.

This workflow collapses what would otherwise be a multi-day, multi-person coordination exercise into a structured preview-and-approve cycle.

## Handling multiple overlapping Releases

Real-world content operations rarely involve a single isolated Release. Veda has five scheduled dates in a six-week window, and some of these Releases affect the same pages. What happens when two Releases both modify the homepage hero?

This is where previewing becomes more nuanced:

*   Non-overlapping Releases: if Release A publishes a hero for November 28 and Release B publishes a different hero for December 15, the December 15 hero replaces the November 28 hero. Previewing each Release independently shows the correct state for each date.
*   Overlapping Releases: if two Releases scheduled for the same date both modify the homepage hero, the last one to execute wins. The publish queue processes items sequentially, and the final published version is the one that persists.

To prevent conflicts, the editorial team should review the publish queue for scheduling collisions. If two Releases target the same entry on the same date, the team must decide which takes precedence and adjust the other Release accordingly.

There is no automated conflict resolution for overlapping Releases. This is a content governance problem, not a technical one. The tools provide visibility (through the publish queue and Release item lists), but the decision about which content wins belongs to the editorial team. Consider establishing a Release review meeting for periods with heavy scheduling, like the holiday season, where the team walks through the publish queue and validates that no conflicts exist.

## Preview environment architecture considerations

Implementing future-state preview has explicit architectural implications for your frontend deployment configuration targets:

Concern

Production path

Preview path

Content source

Delivery API (published)

Preview API (draft) or mixed

Caching

Aggressive CDN caching

No cache or very short TTL

Authentication

Public (delivery token)

Restricted (preview token, IP allow)

URL routing

Standard routes

Standard routes + date/release params

Content resolution

Always published state

Published or draft, contextual

Your preview deployment should be a separate environment from production, with its own delivery/preview tokens and its own caching policy. Serving draft content through your production CDN is a security and correctness risk: unpublished content could leak to visitors, and cached draft responses could persist after publishing.

The preview environment should also support additional query parameters or headers for specifying the preview context (target date, Release UID, locale). These parameters drive the content resolution logic described in the implementation approaches above.

## Common mistakes

### Mistake 1: previewing individual entries and assuming the full page is correct

An editor previews a single entry from a Release and sees it renders correctly. They approve the Release. After deployment, they discover that another entry in the Release (a navigation change) conflicts with the previewed entry's layout. Always validate the composite page state, not just individual entries.

### Mistake 2: using the production environment for preview

Serving draft content through the production CDN to enable preview creates a content leak risk. Unpublished content may be served to real visitors, either through cache pollution or through unprotected preview URLs. Use a dedicated preview environment with access controls.

### Mistake 3: not accounting for overlapping scheduled actions

Two Releases are scheduled for the same date, and both modify the same entry. The team assumes both changes will apply, but only the last-published version persists. Review the publish queue for scheduling conflicts before approving Releases, especially during high-activity periods like product launches or seasonal campaigns.

## Summary

Previewing future content states bridges the gap between scheduling content and trusting that it will look correct when published. Entry timelines show the publication history and scheduled actions for individual entries. The publish queue provides a centralized view of all pending operations across the stack. Live Preview renders draft content for editorial validation, and Release-aware preview implementations can composite multiple future changes into a single, accurate view of the site at any target date. The architectural requirements for future-state preview include separate preview environments, context-aware content resolution, and cache policies that prioritize freshness over efficiency. When these elements are in place, editors can schedule with confidence rather than scheduling and hoping.

#### Key takeaways

- Connect **Previewing future states with timeline** 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.

## Resources & references

| Page | Companion Markdown |
| --- | --- |
| /courses/preview-visual-builder-and-releases/live-preview-and-visual-builder-overview | /academy/md/courses/preview-visual-builder-and-releases/live-preview-and-visual-builder-overview.md |
| /courses/preview-visual-builder-and-releases/preview-requirements-and-concepts | /academy/md/courses/preview-visual-builder-and-releases/preview-requirements-and-concepts.md |
| /courses/preview-visual-builder-and-releases/draft-vs-published-preview-host-routing-and-caching | /academy/md/courses/preview-visual-builder-and-releases/draft-vs-published-preview-host-routing-and-caching.md |
| /courses/preview-visual-builder-and-releases/ssr-and-csr-preview-patterns | /academy/md/courses/preview-visual-builder-and-releases/ssr-and-csr-preview-patterns.md |
| /courses/preview-visual-builder-and-releases/visual-builder-mental-model-and-implementation | /academy/md/courses/preview-visual-builder-and-releases/visual-builder-mental-model-and-implementation.md |
| /courses/preview-visual-builder-and-releases/visual-builder-graphql-localization-and-edge-cases | /academy/md/courses/preview-visual-builder-and-releases/visual-builder-graphql-localization-and-edge-cases.md |
| /courses/preview-visual-builder-and-releases/scheduling-releases-and-versioning-overview | /academy/md/courses/preview-visual-builder-and-releases/scheduling-releases-and-versioning-overview.md |
| /courses/preview-visual-builder-and-releases/releases-scheduling-and-coordinated-publishing | /academy/md/courses/preview-visual-builder-and-releases/releases-scheduling-and-coordinated-publishing.md |
| /courses/preview-visual-builder-and-releases/entry-versioning-comparison-and-rollback | /academy/md/courses/preview-visual-builder-and-releases/entry-versioning-comparison-and-rollback.md |
| /courses/preview-visual-builder-and-releases/previewing-future-states-with-timeline | /academy/md/courses/preview-visual-builder-and-releases/previewing-future-states-with-timeline.md |

## Supplement for indexing

### Content summary

Preview, Visual Builder, and Releases Build the editorial feedback loop that headless architectures do not give you for free: reliable preview, in-context editing, and coordinated future-state publishing. Who This Course… Preview, Visual Builder, and Releases Build the editorial feedback loop that headless architectures do not give you for free: reliable preview, in-context editing, and coordinated future-state publishing. Who This Course Is For This course is for developers responsible for preview environments, editor experience, release coordination, or frontend integrations that must reflect draft content safely. You Will Be Able To explain the architecture required for trustworthy Live Preview configure Visual Builder and preview paths around real frontend behavior manage scheduling, versioning, and release

### Retrieval tags

- Contentstack Academy
- preview-visual-builder-and-releases
- Live
- Preview
- and
- Visual
- Builder
- Overview
- requirements
- concepts
- Draft
- published
- host
- routing

### Indexing notes

Chunk at each "### Lesson NN — Title" heading; copy lesson_id and topics from the preceding HTML comment into chunk metadata for RAG filters.
Course slug: preview-visual-builder-and-releases. Union of lesson topic tokens: Live, Preview, and, Visual, Builder, Overview, requirements, concepts, Draft, published, preview, host, routing, SSR, CSR, patterns, mental, model, implementation, GraphQL, localization, edge, Scheduling, Releases, Versioning, scheduling, coordinated, publishing, Entry, versioning, comparison, rollback, Previewing, future, states, with, timeline.
Do not embed or retrieve LMS-only quiz items or mastery exam answer keys from this export.

### 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/` |
| Contentstack's official regions data | `https://artifacts.contentstack.com/regions.json` |
