Skip to content

Commit 6f3d798

Browse files
authored
Merge pull request #608 from contentstack/stage_v4
Release v4.4.4
2 parents 1026d9b + e6ae2f6 commit 6f3d798

25 files changed

Lines changed: 825 additions & 144 deletions

CHANGELOG.md

Lines changed: 60 additions & 54 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ npm install @contentstack/live-preview-utils
2929

3030
### Load from a CDN (advanced)
3131

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

3434
```html
3535
<script type="module" crossorigin="anonymous">
36-
import ContentstackLivePreview from "https://esm.sh/@contentstack/live-preview-utils@4.4.3";
36+
import ContentstackLivePreview from "https://esm.sh/@contentstack/live-preview-utils@4.4.4";
3737
3838
ContentstackLivePreview.init({
3939
stackDetails: {
@@ -84,6 +84,7 @@ Full tables and examples: **[docs/live-preview-configs.md](docs/live-preview-con
8484
- [`onLiveEdit`](docs/live-preview-configs.md#onliveeditcallback---void): Trigger actions on live edits
8585
- [`onEntryChange`](docs/live-preview-configs.md#onentrychangecallback---void): Listen for entry updates
8686
- [`hash`](docs/live-preview-configs.md#hash): Access preview state identifier
87+
- [`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)
8788
- [`config`](docs/live-preview-configs.md#config): Includes runtime context (for example Live Preview / Timeline preview, Visual Editor, or independent)
8889

8990
The [configs table of contents](docs/live-preview-configs.md#contentstack-live-preview-utils-sdk-configs) also lists `setConfigFromParams` and `getGatsbyDataFormat` for deeper workflows.

docs/live-preview-configs.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ The init data has following structure
1818
- [`stackSdk`](#stacksdk)
1919
- [`onLiveEdit(callback: () => void)`](#onliveeditcallback---void)
2020
- [`onEntryChange(callback: () => void)`](#onentrychangecallback---void)
21+
- [`setPageContext(context)`](#setpagecontextcontext)
2122
- [hash](#hash)
2223
- [setConfigFromParams(config: ConstructorParameters\[0\])](#setconfigfromparamsconfig-constructorparameters0)
2324
- [`getGatsbyDataFormat(sdkQuery: IStackSdk, prefix: string)`](#getgatsbydataformatsdkquery-istacksdk-prefix-string)
@@ -323,6 +324,62 @@ const Footer = () => {
323324
onEntryChange(fetchData, { skipInitialRender: true });
324325
```
325326

327+
## `setPageContext(context)`
328+
329+
```ts
330+
setPageContext(context: { entryUid: string; contentTypeUid: string }): void
331+
```
332+
333+
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.
334+
335+
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.
336+
337+
Place the call alongside your existing `addEditableTags` call — both reference the same `entry` object, so there is no extra lookup.
338+
339+
| parameter | type | description |
340+
| --------------------- | -------- | ------------------------------------------------- |
341+
| `context.entryUid` | string | UID of the entry the current page is rendering. |
342+
| `context.contentTypeUid` | string | UID of that entry's content type. |
343+
344+
**For example:**
345+
346+
```js
347+
import ContentstackLivePreview from "@contentstack/live-preview-utils";
348+
import Utils from "@contentstack/utils";
349+
350+
// In your page component (e.g. inside useEffect, once the entry is fetched)
351+
Utils.addEditableTags(entry, "blog_post", true, "en-us");
352+
ContentstackLivePreview.setPageContext({
353+
entryUid: entry.uid,
354+
contentTypeUid: "blog_post",
355+
});
356+
```
357+
358+
> **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.
359+
360+
### Alternative ways to supply page context
361+
362+
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:
363+
364+
1. The value set via `ContentstackLivePreview.setPageContext()`.
365+
2. A `window.__CS_PAGE_CONTEXT__` global:
366+
367+
```ts
368+
window.__CS_PAGE_CONTEXT__ = {
369+
entryUid: "entry-123",
370+
contentTypeUid: "blog_post",
371+
};
372+
```
373+
374+
3. `<meta>` tags in the page `<head>`:
375+
376+
```html
377+
<meta name="contentstack:entry-uid" content="entry-123" />
378+
<meta name="contentstack:content-type-uid" content="blog_post" />
379+
```
380+
381+
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.
382+
326383
## hash
327384

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

main.mustache

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ Full tables and examples: **[docs/live-preview-configs.md](docs/live-preview-con
8484
- [`onLiveEdit`](docs/live-preview-configs.md#onliveeditcallback---void): Trigger actions on live edits
8585
- [`onEntryChange`](docs/live-preview-configs.md#onentrychangecallback---void): Listen for entry updates
8686
- [`hash`](docs/live-preview-configs.md#hash): Access preview state identifier
87+
- [`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)
8788
- [`config`](docs/live-preview-configs.md#config): Includes runtime context (for example Live Preview / Timeline preview, Visual Editor, or independent)
8889

8990
The [configs table of contents](docs/live-preview-configs.md#contentstack-live-preview-utils-sdk-configs) also lists `setConfigFromParams` and `getGatsbyDataFormat` for deeper workflows.

package-lock.json

Lines changed: 25 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@contentstack/live-preview-utils",
3-
"version": "4.4.3",
3+
"version": "4.4.4",
44
"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.",
55
"type": "module",
66
"types": "dist/legacy/index.d.ts",

src/common/inIframe.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ export function inIframe(): boolean {
88
}
99
}
1010

11+
export function inVisualEditor(): boolean{
12+
try {
13+
return inIframe() && window?.name == 'visual-editor'
14+
} catch (e) {
15+
return false;
16+
}
17+
}
18+
1119
export function isOpeningInNewTab(): boolean {
1220
try {
1321
if(hasWindow()) {

src/configManager/__test__/configManager.test.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Config, { updateConfigFromUrl } from "../configManager";
1+
import Config, { updateConfigFromUrl, syncToStackSdk } from "../configManager";
22
import { getDefaultConfig, getUserInitData } from "../config.default";
33
import { DeepSignal } from "deepsignal";
44
import { IConfig } from "../../types/types";
@@ -102,6 +102,73 @@ describe("config default flags", () => {
102102
});
103103
});
104104

105+
describe("syncToStackSdk", () => {
106+
beforeEach(() => {
107+
Config.reset();
108+
});
109+
110+
afterAll(() => {
111+
Config.reset();
112+
});
113+
114+
test("should set hash, stackSdkLivePreview.hash and stackSdkLivePreview.live_preview when hash is provided", () => {
115+
syncToStackSdk({ hash: "abc123" });
116+
117+
const config = Config.get();
118+
expect(config.stackSdk.live_preview.hash).toBe("abc123");
119+
expect(config.stackSdk.live_preview.live_preview).toBe("abc123");
120+
expect(config.stackSdk.live_preview.content_type_uid).toBeUndefined();
121+
expect(config.stackSdk.live_preview.entry_uid).toBeUndefined();
122+
});
123+
124+
test("should set content_type_uid on stackSdk when contentTypeUid is provided", () => {
125+
syncToStackSdk({ contentTypeUid: "blog" });
126+
127+
const config = Config.get();
128+
expect(config.stackSdk.live_preview.content_type_uid).toBe("blog");
129+
expect(config.stackSdk.live_preview.hash).toBeUndefined();
130+
expect(config.stackSdk.live_preview.entry_uid).toBeUndefined();
131+
});
132+
133+
test("should set entry_uid on stackSdk when entryUid is provided", () => {
134+
syncToStackSdk({ entryUid: "entry-42" });
135+
136+
const config = Config.get();
137+
expect(config.stackSdk.live_preview.entry_uid).toBe("entry-42");
138+
expect(config.stackSdk.live_preview.hash).toBeUndefined();
139+
expect(config.stackSdk.live_preview.content_type_uid).toBeUndefined();
140+
});
141+
142+
test("should set all three fields when all params are provided", () => {
143+
syncToStackSdk({ hash: "h1", contentTypeUid: "page", entryUid: "e1" });
144+
145+
const config = Config.get();
146+
expect(config.stackSdk.live_preview.hash).toBe("h1");
147+
expect(config.stackSdk.live_preview.live_preview).toBe("h1");
148+
expect(config.stackSdk.live_preview.content_type_uid).toBe("page");
149+
expect(config.stackSdk.live_preview.entry_uid).toBe("e1");
150+
});
151+
152+
test("should skip falsy values — null and undefined are ignored", () => {
153+
syncToStackSdk({ hash: null, contentTypeUid: undefined, entryUid: null });
154+
155+
const config = Config.get();
156+
expect(config.stackSdk.live_preview.hash).toBeUndefined();
157+
expect(config.stackSdk.live_preview.content_type_uid).toBeUndefined();
158+
expect(config.stackSdk.live_preview.entry_uid).toBeUndefined();
159+
});
160+
161+
test("should not overwrite existing stackSdk values for keys not passed", () => {
162+
syncToStackSdk({ hash: "first", contentTypeUid: "ct1", entryUid: "e1" });
163+
syncToStackSdk({ hash: "second" });
164+
165+
const config = Config.get();
166+
expect(config.stackSdk.live_preview.hash).toBe("second");
167+
expect(config.stackSdk.live_preview.content_type_uid).toBe("ct1");
168+
expect(config.stackSdk.live_preview.entry_uid).toBe("e1");
169+
});
170+
});
171+
105172
describe("update config from url", () => {
106173
let config: DeepSignal<IConfig>;
107174

0 commit comments

Comments
 (0)