From 68e62b608f86959ede04e65fe18f7e9e634a350c Mon Sep 17 00:00:00 2001 From: Kirtesh Suthar Date: Thu, 21 May 2026 19:09:03 +0530 Subject: [PATCH 1/2] feat: add page context helper for custom URL handling in Visual Builder --- src/configManager/config.default.ts | 1 + src/preview/contentstack-live-preview-HOC.ts | 34 +++++++++++++++++++ src/types/global.d.ts | 9 +++++ src/types/types.ts | 6 ++++ src/visualBuilder/index.ts | 14 +++++--- .../utils/getVisualBuilderRedirectionUrl.ts | 10 ++++++ src/visualBuilder/utils/resolvePageContext.ts | 28 +++++++++++++++ .../utils/types/postMessage.types.ts | 6 ++++ 8 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 src/types/global.d.ts create mode 100644 src/visualBuilder/utils/resolvePageContext.ts diff --git a/src/configManager/config.default.ts b/src/configManager/config.default.ts index 5a62351b..cdbb7215 100644 --- a/src/configManager/config.default.ts +++ b/src/configManager/config.default.ts @@ -112,5 +112,6 @@ export function getDefaultConfig(): IConfig { payload: [], }, enableLivePreviewOutsideIframe: undefined, + pageContext: null, }; } diff --git a/src/preview/contentstack-live-preview-HOC.ts b/src/preview/contentstack-live-preview-HOC.ts index 7906cba9..9c08aa59 100644 --- a/src/preview/contentstack-live-preview-HOC.ts +++ b/src/preview/contentstack-live-preview-HOC.ts @@ -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"; @@ -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: @@ -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( + VisualBuilderPostMessageEvents.PAGE_CONTEXT, + { entryUid: context.entryUid, contentTypeUid: context.contentTypeUid } + ); + } + } + /** * Retrieves the version of the SDK. * @returns The version of the SDK as a string. diff --git a/src/types/global.d.ts b/src/types/global.d.ts new file mode 100644 index 00000000..ca9a58b0 --- /dev/null +++ b/src/types/global.d.ts @@ -0,0 +1,9 @@ +import type { IPageContext } from "./types"; + +declare global { + interface Window { + __CS_PAGE_CONTEXT__?: IPageContext; + } +} + +export {}; diff --git a/src/types/types.ts b/src/types/types.ts index 93602ecd..ad504d17 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -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; @@ -88,6 +93,7 @@ export declare interface IConfig { }; collab: ICollabConfig["collab"]; enableLivePreviewOutsideIframe: boolean | undefined; + pageContext: IPageContext | null; } diff --git a/src/visualBuilder/index.ts b/src/visualBuilder/index.ts index 7040bba7..f484f1a2 100644 --- a/src/visualBuilder/index.ts +++ b/src/visualBuilder/index.ts @@ -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"; @@ -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("init", { - isSSR: config.ssr, - href: window.location.href, - }) + ?.send("init", initPayload) .then((data) => { const { windowType = ILivePreviewWindowType.BUILDER, diff --git a/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts b/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts index 70a76b19..06162d8e 100644 --- a/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts +++ b/src/visualBuilder/utils/getVisualBuilderRedirectionUrl.ts @@ -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. @@ -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 diff --git a/src/visualBuilder/utils/resolvePageContext.ts b/src/visualBuilder/utils/resolvePageContext.ts new file mode 100644 index 00000000..538ed4f1 --- /dev/null +++ b/src/visualBuilder/utils/resolvePageContext.ts @@ -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 +// tags natively — there is no built-in hook for injecting window globals. +// - Strict CSP policies (no unsafe-inline) block inline