# Visual Builder - GraphQL, localization, and edge cases

### About this export

| Field | Value |
| --- | --- |
| **content_type** | lesson |
| **platform** | contentstack-academy |
| **source_url** | https://www.contentstack.com/academy/courses/preview-visual-builder-and-releases/visual-builder-graphql-localization-and-edge-cases |
| **course_slug** | preview-visual-builder-and-releases |
| **lesson_slug** | visual-builder-graphql-localization-and-edge-cases |
| **markdown_file_url** | /academy/md/courses/preview-visual-builder-and-releases/visual-builder-graphql-localization-and-edge-cases.md |
| **generated_at** | 2026-08-03T11:49:51.615Z |

> 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":"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.

## Supplement for indexing

### Content summary

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

### Retrieval tags

- Visual
- Builder
- GraphQL
- localization
- and
- edge
- preview-visual-builder-and-releases
- lesson 06
- Visual Builder - GraphQL, localization, and edge cases
- preview-visual-builder-and-releases lesson

### Indexing notes

Index this lesson as a primary chunk tagged with lesson_id "06" and topics: [Visual, Builder, GraphQL, localization, and, edge].
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/` |
