Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions src/configManager/config.default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,6 @@ export function getDefaultConfig(): IConfig {
payload: [],
},
enableLivePreviewOutsideIframe: undefined,
pageContext: null,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,6 @@ describe("postMessageEvent.hooks", () => {
await sendInitializeLivePreviewPostMessageEvent();
await Promise.resolve();

expect(setConfigFromParams).not.toHaveBeenCalled();
expect(Config.set).not.toHaveBeenCalled();
expect(addParamsToUrl).not.toHaveBeenCalled();
expect(livePreviewPostMessage?.on).not.toHaveBeenCalledWith(
Expand Down
34 changes: 34 additions & 0 deletions src/preview/contentstack-live-preview-HOC.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { cloneDeep, isEmpty, pick } from "lodash-es";
import { v4 as uuidv4 } from "uuid";
import { inIframe } from "../common/inIframe";
import { getUserInitData } from "../configManager/config.default";
import Config, { updateConfigFromUrl } from "../configManager/configManager";
import LivePreview from "../livePreview/live-preview";
Expand All @@ -16,6 +17,11 @@ import { PublicLogger } from "../logger/logger";
import { handleWebCompare } from "../timeline/compare/compare";
import type { IExportedConfig, IInitData } from "../types/types";
import { VisualBuilder } from "../visualBuilder";
import visualBuilderPostMessage from "../visualBuilder/utils/visualBuilderPostMessage";
import {
IPageContextPostMessageEvent,
VisualBuilderPostMessageEvents,
} from "../visualBuilder/utils/types/postMessage.types";

class ContentstackLivePreview {
private static previewConstructors:
Expand Down Expand Up @@ -247,6 +253,34 @@ class ContentstackLivePreview {
);
}

/**
* Sets the page-level entry context for the current page.
* Used by the Visual Builder "Start Editing" button to know which entry
* the current page is rendering, enabling accurate VB navigation.
*
* Place this call alongside your existing `addEditableTags` call — both
* reference the same `entry` object so there is no extra lookup.
*
* @example
* ```js
* // In your page component / useEffect
* Utils.addEditableTags(entry, "blog_post", true, "en-us");
* ContentstackLivePreview.setPageContext({ entryUid: entry.uid, contentTypeUid: "blog_post" });
* ```
*/
static setPageContext(context: { entryUid: string; contentTypeUid: string }): void {
Config.set("pageContext", context);
// init() fires before async data fetching, so the INIT post-message has no
// entry context in CSR apps. Send it now so VB can update its current entry.
// Only send when inside an iframe — skip when the site opens in a plain browser tab.
if (inIframe()) {
visualBuilderPostMessage?.send<IPageContextPostMessageEvent>(
VisualBuilderPostMessageEvents.PAGE_CONTEXT,
{ entryUid: context.entryUid, contentTypeUid: context.contentTypeUid }
);
}
}

/**
* Retrieves the version of the SDK.
* @returns The version of the SDK as a string.
Expand Down
9 changes: 9 additions & 0 deletions src/types/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { IPageContext } from "./types";

declare global {
interface Window {
__CS_PAGE_CONTEXT__?: IPageContext;
}
}

export {};
6 changes: 6 additions & 0 deletions src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ export enum ILivePreviewWindowType {
INDEPENDENT = "independent",
}

export declare interface IPageContext {
entryUid: string;
contentTypeUid: string;
}

export declare interface IConfig {
ssr: boolean;
enable: boolean;
Expand All @@ -89,6 +94,7 @@ export declare interface IConfig {
};
collab: ICollabConfig["collab"];
enableLivePreviewOutsideIframe: boolean | undefined;
pageContext: IPageContext | null;
}


Expand Down
14 changes: 10 additions & 4 deletions src/visualBuilder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { generateStartEditingButton } from "./generators/generateStartEditingBut

import { addFocusOverlay } from "./generators/generateOverlay";
import { getEntryIdentifiersInCurrentPage } from "./utils/getEntryIdentifiersInCurrentPage";
import { resolvePageContext } from "./utils/resolvePageContext";
import visualBuilderPostMessage from "./utils/visualBuilderPostMessage";
import { VisualBuilderPostMessageEvents } from "./utils/types/postMessage.types";

Expand Down Expand Up @@ -308,11 +309,16 @@ export class VisualBuilder {
return;
}

const { entryUid, contentTypeUid } = resolvePageContext();
const initPayload: any = {
isSSR: config.ssr,
href: window.location.href,
entry_uid: entryUid,
content_type_uid: contentTypeUid,
};

visualBuilderPostMessage
?.send<IVisualBuilderInitEvent>("init", {
isSSR: config.ssr,
href: window.location.href,
})
?.send<IVisualBuilderInitEvent>("init", initPayload)
.then((data) => {
const {
windowType = ILivePreviewWindowType.BUILDER,
Expand Down
10 changes: 10 additions & 0 deletions src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Config from "../../configManager/configManager";
import { extractDetailsFromCslp, isValidCslp } from "../../cslp";
import { resolvePageContext } from "./resolvePageContext";

/**
* Returns the redirection URL for the Visual builder.
Expand Down Expand Up @@ -36,6 +37,15 @@ export default function getVisualBuilderRedirectionUrl(): URL {
searchParams.set("locale", localeToUse);
}

const { entryUid, contentTypeUid } = resolvePageContext();

if (entryUid) {
searchParams.set("entry_uid", entryUid);
}
if (contentTypeUid) {
searchParams.set("content_type_uid", contentTypeUid);
}

const completeURL = new URL(
`/#!/stack/${apiKey}/visual-editor?${searchParams.toString()}`,
appUrl
Expand Down
28 changes: 28 additions & 0 deletions src/visualBuilder/utils/resolvePageContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Config from "../../configManager/configManager";

// Meta tags are kept as a fallback because:
// - Next.js App Router's metadata/generateMetadata() and Nuxt's useHead() produce
// <meta> tags natively — there is no built-in hook for injecting window globals.
// - Strict CSP policies (no unsafe-inline) block inline <script> tags, but never meta tags.
export function resolvePageContext(): {
entryUid: string | undefined;
contentTypeUid: string | undefined;
} {
const configCtx = Config.get().pageContext;
return {
entryUid:
configCtx?.entryUid ??
window.__CS_PAGE_CONTEXT__?.entryUid ??
document
.querySelector('meta[name="contentstack:entry-uid"]')
?.getAttribute("content") ??
undefined,
contentTypeUid:
configCtx?.contentTypeUid ??
window.__CS_PAGE_CONTEXT__?.contentTypeUid ??
document
.querySelector('meta[name="contentstack:content-type-uid"]')
?.getAttribute("content") ??
undefined,
};
}
6 changes: 6 additions & 0 deletions src/visualBuilder/utils/types/postMessage.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,11 @@ export enum VisualBuilderPostMessageEvents {
COLLAB_THREAD_REOPEN = "collab-thread-reopen",
COLLAB_THREAD_HIGHLIGHT = "collab-thread-highlight",
TOGGLE_SCROLL = "toggle-scroll",
PAGE_CONTEXT = "page-context",
REQUEST_DISCUSSION_HIGHLIGHTS = "request-discussion-highlights",
}

export interface IPageContextPostMessageEvent {
entryUid: string;
contentTypeUid: string;
}
Loading