> This page is for Experience Orchestration.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://contentful.com/developers/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://contentful.com/developers/docs/_mcp/server.

# Set up live preview

> Connect your website to session-based live preview with the Experiences SDK.

> **Info**
>
> Experiences SDK is currently available in alpha. APIs are unstable and subject to change.

Session-based live preview shows your current Experience Orchestration canvas state on your website. Content, design, and slot changes can appear before you save. It supports experiences, experience fragments, components, and experience templates.

Live preview always renders an experience, including when you preview a component or an experience template.

The canvas creates a preview session and adds `preview_session_id` to your configured preview URL. Your website fetches the session's current experience and receives updates through the SDK. You can use the same preview route for all supported canvas types.

For routes, authentication requirements, and WebSocket behavior, see the [Preview Session API reference](/developers/docs/references/preview-session-api/overview).

Follow the steps below to connect a new preview website. If the website already supports sessions, start at [Configure preview URLs](#2-configure-preview-urls).

## 1. Connect your preview website

You need a website that can render Experience Orchestration content and an SDK configuration with your registered components and templates. The editor user needs permission to create a preview session.

Use the **Content Preview API access token** from **Settings > API keys** in the Contentful web app.

> **Info**
>
> The Content Delivery API access token cannot be used to fetch preview sessions or receive live updates.

Use the adapter for your framework, or the framework-independent live preview client for a custom renderer.

| Framework       | Package                                | Live integration          |
| --------------- | -------------------------------------- | ------------------------- |
| React           | `@contentful/experiences-react`        | `useLivePreview`          |
| Svelte          | `@contentful/experiences-svelte`       | `useLivePreview`          |
| Angular         | `@contentful/experiences-angular`      | `injectLivePreview`       |
| Custom renderer | `@contentful/experiences-live-preview` | `createLivePreviewClient` |

Read `preview_session_id` from the website URL and pass it to the SDK as `sessionId`. Supply the same `spaceId` and `environmentId` as the canvas, and use the Content Preview API token as `previewToken`.

The examples use `experienceConfig` for your registered components and templates, and `previewConfig` for your website's configuration:

```ts
export const previewConfig = {
  spaceId: '<space-id>',
  environmentId: '<environment-id>',
  previewToken: '<content-preview-api-token>',
};
```

#### Next.js

This example uses `fetchPreviewSession` to retrieve the session's experience:

```tsx
import { fetchPreviewSession } from '@contentful/experiences-react';
import { experienceConfig } from '../experience-config';
import { previewConfig } from '../preview-config';
import { Preview } from './Preview';

export const dynamic = 'force-dynamic';

export default async function PreviewPage({ searchParams }: {
  searchParams: Promise<{ preview_session_id?: string }>;
}) {
  const { preview_session_id: sessionId } = await searchParams;
  const { spaceId, environmentId, previewToken } = previewConfig;

  if (!spaceId || !environmentId || !previewToken || !sessionId) {
    throw new Error('Preview requires a space, environment, token, and session ID.');
  }

  const currentExperience = await fetchPreviewSession(
    { spaceId, environmentId, sessionId },
    { previewToken },
    { config: experienceConfig },
  );

  return (
    <Preview
      initialPlan={currentExperience}
      previewSessionOptions={{ spaceId, environmentId, sessionId, previewToken }}
    />
  );
}
```

`useLivePreview` resolves incoming updates for `ClientExperienceRenderer`:

```tsx
'use client';

import {
  ClientExperienceRenderer,
  useLivePreview,
  type PortableRenderPlan,
  type PreviewSessionOptions,
} from '@contentful/experiences-react';
import { experienceConfig } from '../experience-config';

export function Preview({ initialPlan, previewSessionOptions }: {
  initialPlan: PortableRenderPlan;
  previewSessionOptions: PreviewSessionOptions;
}) {
  const preview = useLivePreview({
    initialPlan,
    previewSessionOptions,
    resolveOptions: { config: experienceConfig },
  });

  return (
    <>
      {preview.error && (
        <p role="status">
          Live updates are unavailable. Showing the last valid preview.
        </p>
      )}
      <ClientExperienceRenderer
        experience={preview.data}
        config={experienceConfig}
      />
    </>
  );
}
```

#### React

This example uses `fetchPreviewSession` to retrieve the session's experience and `useLivePreview` to resolve live updates:

```tsx
import {
  ClientExperienceRenderer,
  fetchPreviewSession,
  useLivePreview,
  type PortableRenderPlan,
  type PreviewSessionOptions,
} from '@contentful/experiences-react';
import { experienceConfig } from './experience-config';
import { previewConfig } from './preview-config';

export function Preview({ initialPlan, previewSessionOptions }: {
  initialPlan: PortableRenderPlan;
  previewSessionOptions: PreviewSessionOptions;
}) {
  const preview = useLivePreview({
    initialPlan,
    previewSessionOptions,
    resolveOptions: { config: experienceConfig },
  });

  return (
    <>
      {preview.error && (
        <p role="status">
          Live updates are unavailable. Showing the last valid preview.
        </p>
      )}
      <ClientExperienceRenderer
        experience={preview.data}
        config={experienceConfig}
      />
    </>
  );
}

export async function loadPreview(url: URL) {
  const sessionId = url.searchParams.get('preview_session_id');
  if (!sessionId) throw new Error('Preview requires a session ID.');

  const previewSessionOptions = {
    ...previewConfig,
    sessionId,
  };
  const initialPlan = await fetchPreviewSession(
    previewSessionOptions,
    { previewToken: previewSessionOptions.previewToken },
    { config: experienceConfig },
  );

  return { initialPlan, previewSessionOptions };
}
```

#### SvelteKit

This example uses `fetchPreviewSession` to retrieve the session's experience:

```ts
import { fetchPreviewSession } from '@contentful/experiences-svelte';
import { experienceConfig } from '$lib/experience-config';
import { previewConfig } from '$lib/preview-config';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ url }) => {
  const sessionId = url.searchParams.get('preview_session_id');
  const { spaceId, environmentId, previewToken } = previewConfig;

  if (!spaceId || !environmentId || !previewToken || !sessionId) {
    throw new Error('Preview requires a space, environment, token, and session ID.');
  }

  const initialPlan = await fetchPreviewSession(
    { spaceId, environmentId, sessionId },
    { previewToken },
    { config: experienceConfig },
  );

  return {
    initialPlan,
    previewSessionOptions: { spaceId, environmentId, sessionId, previewToken },
  };
};
```

`useLivePreview` resolves incoming updates for `ClientExperienceRenderer`:

```svelte
<script lang="ts">
  import {
    ClientExperienceRenderer,
    useLivePreview,
  } from '@contentful/experiences-svelte';
  import { experienceConfig } from '$lib/experience-config';
  import type { PageData } from './$types';

  let { data }: { data: PageData } = $props();

  const preview = useLivePreview(() => ({
    initialPlan: data.initialPlan,
    previewSessionOptions: data.previewSessionOptions,
    resolveOptions: { config: experienceConfig },
  }));
</script>

{#if preview.error}
  <p role="status">
    Live updates are unavailable. Showing the last valid preview.
  </p>
{/if}
<ClientExperienceRenderer experience={preview.data} config={experienceConfig} />
```

`useLivePreview` takes an options getter so it can react to changes in the options.

#### Angular

This example uses `fetchPreviewSession` to retrieve the session's experience:

```ts
import { fetchPreviewSession } from '@contentful/experiences-angular';
import { experienceConfig } from './experience-config';
import { previewConfig } from './preview-config';

export async function loadPreview(url: URL) {
  const sessionId = url.searchParams.get('preview_session_id');
  if (!sessionId) throw new Error('Preview requires a session ID.');

  const previewSessionOptions = { ...previewConfig, sessionId };
  const initialPlan = await fetchPreviewSession(
    previewSessionOptions,
    { previewToken: previewSessionOptions.previewToken },
    { config: experienceConfig },
  );

  return { initialPlan, previewSessionOptions };
}
```

`injectLivePreview` resolves incoming updates for `ClientExperienceRenderer`:

```ts
import { Component, input } from '@angular/core';
import {
  ClientExperienceRenderer,
  injectLivePreview,
  type PortableRenderPlan,
  type PreviewSessionOptions,
} from '@contentful/experiences-angular';
import { experienceConfig } from './experience-config';

@Component({
  selector: 'app-preview',
  standalone: true,
  imports: [ClientExperienceRenderer],
  template: `
    @if (preview.error()) {
      <p role="status">
        Live updates are unavailable. Showing the last valid preview.
      </p>
    }
    <cf-experience [experience]="preview.data()" [config]="config" />
  `,
})
export class PreviewComponent {
  readonly initialPlan = input.required<PortableRenderPlan>();
  readonly previewSessionOptions = input.required<PreviewSessionOptions>();
  protected readonly config = experienceConfig;
  protected readonly preview = injectLivePreview(() => ({
    initialPlan: this.initialPlan(),
    previewSessionOptions: this.previewSessionOptions(),
    resolveOptions: { config: experienceConfig },
  }));
}
```

Call `injectLivePreview` in an injection context, such as a component field initializer. It takes an options getter and returns signals: call `preview.data()` and `preview.error()` to read them. For a server-rendered Angular route, see the [Angular SDK example](https://github.com/contentful/experiences/tree/main/examples/angular).

#### Custom renderer

This example fetches the session with `fetchPreviewSession`, renders the returned experience, and subscribes to live updates.

The example uses your configured `ContentfulExperiences` instance from `@contentful/experiences-web` as `experiences`. Your application's `renderExperience(experience)` function displays the resolved experience.

```ts
import {
  createLivePreviewClient,
  fetchPreviewSession,
  sendPreviewStatus,
  type PreviewSessionOptions,
} from '@contentful/experiences-live-preview';
import type {
  ContentfulExperiences,
  PortableRenderPlan,
} from '@contentful/experiences-web';
import { experienceConfig } from './experience-config';

export async function startPreview(
  previewSessionOptions: PreviewSessionOptions & { sessionId: string; previewToken: string },
  experiences: ContentfulExperiences,
  renderExperience: (experience: PortableRenderPlan) => void,
) {
  const currentExperience = await fetchPreviewSession(
    previewSessionOptions,
    { previewToken: previewSessionOptions.previewToken },
    { config: experienceConfig },
  );
  renderExperience(currentExperience);

  const previewClient = createLivePreviewClient(previewSessionOptions);
  const unsubscribeStatus = previewClient.subscribeStatus(sendPreviewStatus);
  const unsubscribe = previewClient.subscribe(() => {
    const { data, error } = previewClient.getResult();
    if (error) {
      console.error(error);
      return;
    }
    if (!data) return;

    void experiences.resolveExperience(data, { config: experienceConfig })
      .then(renderExperience)
      .catch(console.error);
  });

  return () => {
    unsubscribe();
    unsubscribeStatus();
  };
}
```

`getResult().data` contains an experience payload, not a resolved experience. `experiences.resolveExperience(data, { config: experienceConfig })` converts it to a `PortableRenderPlan`, using the same configuration as the session fetch. The example keeps the displayed experience while resolving updates, or if resolution fails. Your renderer decides how to handle `experience.diagnostics`. Your application should discard older results if async updates finish out of order.

Connect `subscribeStatus` to `sendPreviewStatus` so the canvas can coordinate preview updates. Call the returned cleanup function when leaving the preview route to stop both subscriptions. Your renderer should also discard pending results when the preview closes. See the [framework-independent SDK reference](https://github.com/contentful/experiences/tree/main/packages/live-preview) for the fetch and client APIs.

### Fetch and live updates

`fetchPreviewSession` returns the resolved experience for the current session state. Live subscriptions receive subsequent experience payloads, which must be resolved with the same configuration before rendering.

Framework adapters handle resolution and keep the last valid experience if the connection fails. Custom integrations handle resolution and keep the last valid experience in their renderer.

If the SDK reports that live updates are unavailable, the canvas reloads the preview iframe after each successful session update. Without a live preview integration, the canvas reloads the iframe after you save.

The live subscription requires `previewToken` in the browser.

If your website requires Contentful credentials to remain server-side, implement a backend relay for live updates. Keep tokens out of the configured preview URL.

For cross-space content, [encode the referenced spaces' Content Preview API tokens](/developers/docs/references/content-preview-api/resource-links#extra-header-for-cross-space-resolution). Pass the same value as `resourceResolution` in the first `fetchPreviewSession` argument and in `previewSessionOptions` for live updates.

See the [SDK guide](https://github.com/contentful/experiences#readme) for renderer configuration.

## 2. Configure preview URLs

### Allow embedding

Contentful displays your preview website in an iframe. If the iframe shows "Refused to connect", check the preview page's response headers in your browser's developer tools: open **Network**, select the page request, then open **Headers > Response Headers**.

Update your preview website's security headers:

* Remove the `X-Frame-Options` header from the preview page's response.
* If you use `Content-Security-Policy`, allow `https://app.contentful.com` in its `frame-ancestors` directive. Keep the other directives in your existing policy.

```http
Content-Security-Policy: frame-ancestors https://app.contentful.com;
```

If your preview website uses an authentication cookie, set `SameSite=None` and `Secure` so the browser can send it inside the iframe:

```http
Set-Cookie: auth=<value>; SameSite=None; Secure
```

### Add preview URLs

In the Contentful web app, go to **Settings** and select **Preview** settings. Click **+ Create preview platform** to add a preview configuration.
Under **Select one entity type**, select **All Experience entities** to use a single preview URL for all experiences, experience fragments, components, and experience templates.

Enter your website's preview URL:

```text
https://your-preview-website.example/preview
```

The canvas appends `preview_session_id` to this URL.

An **All Experience entities** URL can include `{entity.sys.*}` tokens for website routing, for example `{entity.sys.id}` and `{entity.sys.type}`. The canvas replaces each token with the corresponding value from the entity being previewed. The session SDK doesn't require these tokens.

Alternatively, select **Experience** to configure a target preview URL for experiences that use a selected experience template. You can also set a fallback URL for experiences that use other templates.

Save your changes and reload the canvas. If you cannot change preview settings, contact your space administrator.

## 3. Open and verify live preview

In an experience or experience fragment canvas, select your website under **Preview URL**. Edit content, a design property, or a slot. The website should update before you save.

In a component or experience template canvas, open **Settings > View settings > Live preview**. Enable it and select your website.

> **Info**
>
> The browser remembers the **Live preview** setting separately for components and experience templates.

For supported entry and asset parameters, use **Select content to preview** to choose sample content for the session.

## Troubleshooting

| What you see                                                                    | What to check                                                                                                                                                                                            |
| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The page shows saved content instead of unsaved edits.                          | Read `preview_session_id` from the URL and pass it as `sessionId` to `fetchPreviewSession`. Fetching the saved experience with `fetchExperience` doesn't retrieve the session's unsaved edits.           |
| The website loads in a separate tab but not inside the canvas.                  | Follow [Allow embedding](#allow-embedding) to configure your website's security headers and authentication cookies.                                                                                      |
| The initial preview renders, but no updates arrive and no error is reported.    | Supply both `sessionId` and `previewToken` in `previewSessionOptions`. Without them, the SDK doesn't open a connection. A custom client also needs a `subscribe` listener to start receiving updates.    |
| Live updates stop and the preview reports `LivePreviewConnectionError`.         | The live connection failed or its retries were exhausted. The last valid preview remains available. Check the preview token, session ID, and `sessionHost`, then reload the preview iframe to reconnect. |
| The preview stays unchanged after an invalid update or a session error message. | The SDK ignores these messages and keeps the last valid data without reporting a connection error. Set `previewSessionOptions.debug` to `true` to log ignored messages in the browser console.           |
| `fetchPreviewSession` throws `NotFoundError`.                                   | The session wasn't found. Check `spaceId`, `environmentId`, and `sessionId`. If the session was deleted or expired, reopen the canvas and use the new preview session URL.                               |
| `fetchPreviewSession` throws `PreviewSessionFetchError`.                        | The initial request failed or returned an invalid response. Inspect the error's `message` and `cause`. Check your Content Preview API token, permissions, and HTTP `host`, then retry.                   |
| The preview iframe refreshes only after you save.                               | Use a framework adapter, or connect `previewClient.subscribeStatus(sendPreviewStatus)` for a custom renderer so the canvas can coordinate updates.                                                       |

## Session and connection details

* The default HTTP host is `https://preview.xdn.contentful.com`. Override it with `host` in the fetch options. The default WebSocket host is `wss://preview.xdn.contentful.com`. Override it with `sessionHost` in the subscription options.
* Sessions expire 30 days after creation or the last successful session update. Viewing a session doesn't extend its lifetime.
* Up to 20 preview connections can use one session. Active connections must use the same preview token and resource-resolution value.