Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
68e62b6
feat: add page context helper for custom URL handling in Visual Builder
kirtesh-cstk May 21, 2026
15ab0c6
fix(VP-556): replace deprecated unload event with pagehide in page tr…
kirtesh-cstk May 25, 2026
fc2fcae
fix: added update to stop the double firing of init in vb
contentstackMridul May 25, 2026
c508c47
test: resolve test failure due to mock failure
contentstackMridul May 26, 2026
e747746
fix: fixed the sideEffect issue due to setConfigFromParams
contentstackMridul May 26, 2026
5b73339
test: added test cases for remaining live preivew and post message fu…
contentstackMridul May 26, 2026
d968e05
test: added remaining gap tests from VB-1504
contentstackMridul May 27, 2026
d3d2a09
Merge pull request #602 from contentstack/VB-1677
contentstackMridul May 29, 2026
a46d025
Merge pull request #600 from contentstack/VB-1667
contentstackMridul May 29, 2026
87648b9
Merge pull request #603 from contentstack/VB-1504_remaining_gaps
contentstackMridul Jun 3, 2026
682ade3
Merge branch 'develop_v4' into feat-add-page-context-helper-for-custo…
kirtesh-cstk Jun 8, 2026
7d28b7c
chore: remove erroneous expection line
kirtesh-cstk Jun 8, 2026
66a7802
fix: handle postMessage failures in setPageContext
kirtesh-cstk Jun 9, 2026
8225388
Merge pull request #605 from contentstack/feat-add-page-context-helpe…
kirtesh-cstk Jun 9, 2026
da52771
Merge pull request #599 from contentstack/VP-556-replace-deprecated-u…
kirtesh-cstk Jun 9, 2026
91c6c6e
fix: add missing expection in test while merging code
kirtesh-cstk Jun 9, 2026
1304bb0
Merge pull request #606 from contentstack/develop_v4
kirtesh-cstk Jun 10, 2026
5c63c2f
chore: update docs and readme
kirtesh-cstk Jun 10, 2026
fa866d7
chore: update missing doc change in mustache file
kirtesh-cstk Jun 10, 2026
70ab029
chore: revert manual version upgrade
kirtesh-cstk Jun 10, 2026
b5e83e5
4.4.4
kirtesh-cstk Jun 10, 2026
0fb29c9
Merge remote-tracking branch 'origin/stage_v4' into develop_v4
kirtesh-cstk Jun 10, 2026
7656954
Merge pull request #607 from contentstack/develop_v4
kirtesh-cstk Jun 10, 2026
e6ae2f6
Merge branch 'main' into stage_v4
kirtesh-cstk Jun 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 60 additions & 54 deletions CHANGELOG.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ npm install @contentstack/live-preview-utils

### Load from a CDN (advanced)

Pin the version to match your app (update `4.4.3` when you upgrade):
Pin the version to match your app (update `4.4.4` when you upgrade):

```html
<script type="module" crossorigin="anonymous">
import ContentstackLivePreview from "https://esm.sh/@contentstack/live-preview-utils@4.4.3";
import ContentstackLivePreview from "https://esm.sh/@contentstack/live-preview-utils@4.4.4";

ContentstackLivePreview.init({
stackDetails: {
Expand Down Expand Up @@ -84,6 +84,7 @@ Full tables and examples: **[docs/live-preview-configs.md](docs/live-preview-con
- [`onLiveEdit`](docs/live-preview-configs.md#onliveeditcallback---void): Trigger actions on live edits
- [`onEntryChange`](docs/live-preview-configs.md#onentrychangecallback---void): Listen for entry updates
- [`hash`](docs/live-preview-configs.md#hash): Access preview state identifier
- [`setPageContext`](docs/live-preview-configs.md#setpagecontextcontext): Tell Visual Builder which entry the current page renders so “Start Editing” targets the right entry (needed for custom-URL pages)
- [`config`](docs/live-preview-configs.md#config): Includes runtime context (for example Live Preview / Timeline preview, Visual Editor, or independent)

The [configs table of contents](docs/live-preview-configs.md#contentstack-live-preview-utils-sdk-configs) also lists `setConfigFromParams` and `getGatsbyDataFormat` for deeper workflows.
Expand Down
57 changes: 57 additions & 0 deletions docs/live-preview-configs.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The init data has following structure
- [`stackSdk`](#stacksdk)
- [`onLiveEdit(callback: () => void)`](#onliveeditcallback---void)
- [`onEntryChange(callback: () => void)`](#onentrychangecallback---void)
- [`setPageContext(context)`](#setpagecontextcontext)
- [hash](#hash)
- [setConfigFromParams(config: ConstructorParameters\[0\])](#setconfigfromparamsconfig-constructorparameters0)
- [`getGatsbyDataFormat(sdkQuery: IStackSdk, prefix: string)`](#getgatsbydataformatsdkquery-istacksdk-prefix-string)
Expand Down Expand Up @@ -323,6 +324,62 @@ const Footer = () => {
onEntryChange(fetchData, { skipInitialRender: true });
```

## `setPageContext(context)`

```ts
setPageContext(context: { entryUid: string; contentTypeUid: string }): void
```

The `setPageContext()` method tells the Visual Builder which entry the current page is rendering. The Visual Builder uses this context so that the **"Start Editing"** button (and the `init` handshake) target the correct entry, even when a page is served from a custom URL that the SDK cannot map to an entry on its own.

This context is used in two places: on the initial `init` handshake, so the Visual Builder can confirm whether the entry it has open matches the page rendered in the iframe; and on the "Start Editing" button click, so it can navigate to the correct entry in the Visual Editor. Because `init()` runs before your async data fetch resolves, the handshake may carry no entry context on its own — calling `setPageContext()` once your entry is available sends the context to the Visual Builder so it can update its current entry.

Place the call alongside your existing `addEditableTags` call — both reference the same `entry` object, so there is no extra lookup.

| parameter | type | description |
| --------------------- | -------- | ------------------------------------------------- |
| `context.entryUid` | string | UID of the entry the current page is rendering. |
| `context.contentTypeUid` | string | UID of that entry's content type. |

**For example:**

```js
import ContentstackLivePreview from "@contentstack/live-preview-utils";
import Utils from "@contentstack/utils";

// In your page component (e.g. inside useEffect, once the entry is fetched)
Utils.addEditableTags(entry, "blog_post", true, "en-us");
ContentstackLivePreview.setPageContext({
entryUid: entry.uid,
contentTypeUid: "blog_post",
});
```

> **Note:** When the page is loaded inside the Visual Builder/Live Preview iframe, the context is also sent to Contentstack via a post message. Outside an iframe, the context is only stored locally and used when generating the "Start Editing" redirection URL.

### Alternative ways to supply page context

If you cannot call `setPageContext()` directly (for example, when no JavaScript hook is available for injecting runtime values), the SDK resolves the page context from the following sources, in order of precedence:

1. The value set via `ContentstackLivePreview.setPageContext()`.
2. A `window.__CS_PAGE_CONTEXT__` global:

```ts
window.__CS_PAGE_CONTEXT__ = {
entryUid: "entry-123",
contentTypeUid: "blog_post",
};
```

3. `<meta>` tags in the page `<head>`:

```html
<meta name="contentstack:entry-uid" content="entry-123" />
<meta name="contentstack:content-type-uid" content="blog_post" />
```

Meta tags are a useful fallback for frameworks that render `<meta>` natively (for example, Next.js App Router `metadata`/`generateMetadata()` or Nuxt `useHead()`) and for sites with a strict Content Security Policy (`no unsafe-inline`) that blocks inline `<script>` tags but allows meta tags.

## hash

The `hash` property returns the live preview hash of the entry. It returns an empty string if the page is not opened in live preivew pane.
Expand Down
1 change: 1 addition & 0 deletions main.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Full tables and examples: **[docs/live-preview-configs.md](docs/live-preview-con
- [`onLiveEdit`](docs/live-preview-configs.md#onliveeditcallback---void): Trigger actions on live edits
- [`onEntryChange`](docs/live-preview-configs.md#onentrychangecallback---void): Listen for entry updates
- [`hash`](docs/live-preview-configs.md#hash): Access preview state identifier
- [`setPageContext`](docs/live-preview-configs.md#setpagecontextcontext): Tell Visual Builder which entry the current page renders so “Start Editing” targets the right entry (needed for custom-URL pages)
- [`config`](docs/live-preview-configs.md#config): Includes runtime context (for example Live Preview / Timeline preview, Visual Editor, or independent)

The [configs table of contents](docs/live-preview-configs.md#contentstack-live-preview-utils-sdk-configs) also lists `setConfigFromParams` and `getGatsbyDataFormat` for deeper workflows.
Expand Down
28 changes: 25 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@contentstack/live-preview-utils",
"version": "4.4.3",
"version": "4.4.4",
"description": "Contentstack provides the Live Preview SDK to establish a communication channel between the various Contentstack SDKs and your website, transmitting live changes to the preview pane.",
"type": "module",
"types": "dist/legacy/index.d.ts",
Expand Down
8 changes: 8 additions & 0 deletions src/common/inIframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ export function inIframe(): boolean {
}
}

export function inVisualEditor(): boolean{
try {
return inIframe() && window?.name == 'visual-editor'
} catch (e) {
return false;
}
}

export function isOpeningInNewTab(): boolean {
try {
if(hasWindow()) {
Expand Down
69 changes: 68 additions & 1 deletion src/configManager/__test__/configManager.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Config, { updateConfigFromUrl } from "../configManager";
import Config, { updateConfigFromUrl, syncToStackSdk } from "../configManager";
import { getDefaultConfig, getUserInitData } from "../config.default";
import { DeepSignal } from "deepsignal";
import { IConfig } from "../../types/types";
Expand Down Expand Up @@ -102,6 +102,73 @@ describe("config default flags", () => {
});
});

describe("syncToStackSdk", () => {
beforeEach(() => {
Config.reset();
});

afterAll(() => {
Config.reset();
});

test("should set hash, stackSdkLivePreview.hash and stackSdkLivePreview.live_preview when hash is provided", () => {
syncToStackSdk({ hash: "abc123" });

const config = Config.get();
expect(config.stackSdk.live_preview.hash).toBe("abc123");
expect(config.stackSdk.live_preview.live_preview).toBe("abc123");
expect(config.stackSdk.live_preview.content_type_uid).toBeUndefined();
expect(config.stackSdk.live_preview.entry_uid).toBeUndefined();
});

test("should set content_type_uid on stackSdk when contentTypeUid is provided", () => {
syncToStackSdk({ contentTypeUid: "blog" });

const config = Config.get();
expect(config.stackSdk.live_preview.content_type_uid).toBe("blog");
expect(config.stackSdk.live_preview.hash).toBeUndefined();
expect(config.stackSdk.live_preview.entry_uid).toBeUndefined();
});

test("should set entry_uid on stackSdk when entryUid is provided", () => {
syncToStackSdk({ entryUid: "entry-42" });

const config = Config.get();
expect(config.stackSdk.live_preview.entry_uid).toBe("entry-42");
expect(config.stackSdk.live_preview.hash).toBeUndefined();
expect(config.stackSdk.live_preview.content_type_uid).toBeUndefined();
});

test("should set all three fields when all params are provided", () => {
syncToStackSdk({ hash: "h1", contentTypeUid: "page", entryUid: "e1" });

const config = Config.get();
expect(config.stackSdk.live_preview.hash).toBe("h1");
expect(config.stackSdk.live_preview.live_preview).toBe("h1");
expect(config.stackSdk.live_preview.content_type_uid).toBe("page");
expect(config.stackSdk.live_preview.entry_uid).toBe("e1");
});

test("should skip falsy values — null and undefined are ignored", () => {
syncToStackSdk({ hash: null, contentTypeUid: undefined, entryUid: null });

const config = Config.get();
expect(config.stackSdk.live_preview.hash).toBeUndefined();
expect(config.stackSdk.live_preview.content_type_uid).toBeUndefined();
expect(config.stackSdk.live_preview.entry_uid).toBeUndefined();
});

test("should not overwrite existing stackSdk values for keys not passed", () => {
syncToStackSdk({ hash: "first", contentTypeUid: "ct1", entryUid: "e1" });
syncToStackSdk({ hash: "second" });

const config = Config.get();
expect(config.stackSdk.live_preview.hash).toBe("second");
expect(config.stackSdk.live_preview.content_type_uid).toBe("ct1");
expect(config.stackSdk.live_preview.entry_uid).toBe("e1");
});
});

describe("update config from url", () => {
let config: DeepSignal<IConfig>;

Expand Down
Loading
Loading