# SSR and CSR preview patterns

### About this export

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

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

<!-- ai_metadata: {"lesson_id":"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.

## Supplement for indexing

### Content summary

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

### Retrieval tags

- SSR
- and
- CSR
- preview
- patterns
- preview-visual-builder-and-releases
- lesson 04
- SSR and CSR preview patterns
- preview-visual-builder-and-releases lesson

### Indexing notes

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

### Asset references

_No image or video thumbnail URLs were extracted._

### External links

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