From 7cb20e8457aff780f398a0816aecb9edeea06df8 Mon Sep 17 00:00:00 2001 From: Kirtesh Suthar Date: Mon, 10 Aug 2026 17:07:50 +0530 Subject: [PATCH 1/2] fix(edit-button): send the page the editor was on to the CMS The edit button linked to the entry and field but said nothing about the page it was clicked from. The CMS cannot work that out from the entry alone: a referenced entry can be rendered on several pages, and a nested one has no page among the entries that directly reference it. Without it the live preview panel falls back to the environment base URL. The redirect URL now carries the current page as preview-url, which the CMS already understands. Live preview's own query parameters are stripped so only the page remains. Co-Authored-By: Claude --- .../__test__/editButtonAction.test.ts | 6 +-- src/livePreview/editButton/editButton.ts | 10 +++++ src/utils/__test__/getCurrentPageUrl.test.ts | 38 ++++++++++++++++ src/utils/getCurrentPageUrl.ts | 43 +++++++++++++++++++ 4 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 src/utils/__test__/getCurrentPageUrl.test.ts create mode 100644 src/utils/getCurrentPageUrl.ts diff --git a/src/livePreview/editButton/__test__/editButtonAction.test.ts b/src/livePreview/editButton/__test__/editButtonAction.test.ts index 4c468439..dfd8e674 100644 --- a/src/livePreview/editButton/__test__/editButtonAction.test.ts +++ b/src/livePreview/editButton/__test__/editButtonAction.test.ts @@ -201,7 +201,7 @@ describe("cslp tooltip", () => { singularEditButton?.click(); const expectedRedirectUrl = - "https://app.contentstack.com/#!/stack/sample-api-key/content-type/content-type-1/en-us/entry/entry-uid-1/edit?branch=main&preview-field=field-title&preview-locale=en-us&preview-environment=sample-environment"; + "https://app.contentstack.com/#!/stack/sample-api-key/content-type/content-type-1/en-us/entry/entry-uid-1/edit?branch=main&preview-field=field-title&preview-locale=en-us&preview-environment=sample-environment&preview-url=http%3A%2F%2Flocalhost%3A3000%2F"; expect(window.open).toHaveBeenCalledWith(expectedRedirectUrl, "_blank"); @@ -241,7 +241,7 @@ describe("cslp tooltip", () => { singularEditButton?.click(); const expectedRedirectUrl = - "https://app.contentstack.com/#!/stack/sample-api-key/content-type/content-type-1/en-us/entry/entry-uid-1/edit?branch=dev&preview-field=field-title&preview-locale=en-us&preview-environment=sample-environment"; + "https://app.contentstack.com/#!/stack/sample-api-key/content-type/content-type-1/en-us/entry/entry-uid-1/edit?branch=dev&preview-field=field-title&preview-locale=en-us&preview-environment=sample-environment&preview-url=http%3A%2F%2Flocalhost%3A3000%2F"; expect(window.open).toHaveBeenCalledWith(expectedRedirectUrl, "_blank"); @@ -283,7 +283,7 @@ describe("cslp tooltip", () => { singularEditButton?.click(); const expectedRedirectUrl = - "https://app.contentstack.com/#!/stack/sample-api-key/content-type/content-type-1/en-us/entry/entry-uid-1/variant/variant-uid-1/edit?branch=dev&preview-field=field-title&preview-locale=en-us&preview-environment=sample-environment"; + "https://app.contentstack.com/#!/stack/sample-api-key/content-type/content-type-1/en-us/entry/entry-uid-1/variant/variant-uid-1/edit?branch=dev&preview-field=field-title&preview-locale=en-us&preview-environment=sample-environment&preview-url=http%3A%2F%2Flocalhost%3A3000%2F"; expect(window.open).toHaveBeenCalledWith(expectedRedirectUrl, "_blank"); diff --git a/src/livePreview/editButton/editButton.ts b/src/livePreview/editButton/editButton.ts index e3c8303d..3228813e 100644 --- a/src/livePreview/editButton/editButton.ts +++ b/src/livePreview/editButton/editButton.ts @@ -12,6 +12,7 @@ import { import livePreviewPostMessage from "../eventManager/livePreviewEventManager"; import { EDIT_BUTTON_TOOLTIP_ID } from "./editButton.constant"; import { isOpeningInTimeline } from "../../utils"; +import { getCurrentPageUrl } from "../../utils/getCurrentPageUrl"; function calculateEditButtonPosition( currentHoveredElement: HTMLElement, @@ -556,6 +557,15 @@ export class LivePreviewEditButton { url.searchParams.append("preview-locale", locale ?? "en-us"); url.searchParams.append("preview-environment", environment); + // The page the editor was on. A referenced entry can be rendered on more + // than one page, and a nested one has no page among its direct + // references at all, so the CMS cannot work this out from the entry + // alone — without it the preview falls back to the base URL. + const pageUrl = getCurrentPageUrl(); + if (pageUrl) { + url.searchParams.append("preview-url", pageUrl); + } + return `${url.origin}/${url.hash}${url.search}`; } diff --git a/src/utils/__test__/getCurrentPageUrl.test.ts b/src/utils/__test__/getCurrentPageUrl.test.ts new file mode 100644 index 00000000..ad05d9f7 --- /dev/null +++ b/src/utils/__test__/getCurrentPageUrl.test.ts @@ -0,0 +1,38 @@ +import { getCurrentPageUrl } from "../getCurrentPageUrl"; + +describe("getCurrentPageUrl", () => { + const setHref = (href: string) => { + Object.defineProperty(window, "location", { + value: new URL(href), + writable: true, + }); + }; + + it("should return the page URL as-is when there are no live preview params", () => { + setHref("https://example.com/products/shoes"); + + expect(getCurrentPageUrl()).toBe("https://example.com/products/shoes"); + }); + + it("should drop live preview's own query params", () => { + setHref( + "https://example.com/page?live_preview=abc&content_type_uid=hero&entry_uid=blt1&preview_timestamp=123" + ); + + expect(getCurrentPageUrl()).toBe("https://example.com/page"); + }); + + it("should keep the site's own query params", () => { + setHref("https://example.com/search?q=fountains&live_preview=abc"); + + expect(getCurrentPageUrl()).toBe( + "https://example.com/search?q=fountains" + ); + }); + + it("should keep the path that distinguishes one page from another", () => { + setHref("https://example.com/vp1995-riverside-gardens"); + + expect(getCurrentPageUrl()).toContain("/vp1995-riverside-gardens"); + }); +}); diff --git a/src/utils/getCurrentPageUrl.ts b/src/utils/getCurrentPageUrl.ts new file mode 100644 index 00000000..e89fe726 --- /dev/null +++ b/src/utils/getCurrentPageUrl.ts @@ -0,0 +1,43 @@ +import { PublicLogger } from "../logger/logger"; + +/** + * Query parameters live preview adds to the page itself. They describe the + * preview session, not the page, so they are dropped before the URL is handed + * back to the CMS. + */ +const LIVE_PREVIEW_QUERY_PARAMS = [ + "live_preview", + "content_type_uid", + "entry_uid", + "preview_timestamp", + "preview_variant", + "cslp-buttons", +]; + +/** + * The URL of the page the visitor is on, without live preview's own query + * parameters. + * + * The CMS uses this to keep the preview on the page the editor clicked Edit + * from. It cannot derive that from the entry: a referenced entry can appear on + * several pages, and a nested one (page -> hero -> image) has no page among its + * direct references at all. + * + * Returns an empty string outside a browser or if the URL cannot be parsed, so + * callers can simply omit the parameter. + */ +export function getCurrentPageUrl(): string { + try { + if (typeof window === "undefined" || !window.location?.href) return ""; + + const url = new URL(window.location.href); + LIVE_PREVIEW_QUERY_PARAMS.forEach((param) => + url.searchParams.delete(param) + ); + + return url.href; + } catch (error) { + PublicLogger.error("Error while reading the current page URL"); + return ""; + } +} From c1791141dd7f22430f4f96d8070f4354dc9b641e Mon Sep 17 00:00:00 2001 From: Kirtesh Suthar Date: Wed, 12 Aug 2026 16:45:36 +0530 Subject: [PATCH 2/2] refactor(utils): one list for live preview's own query params The copier in addLivePreviewQueryTags and the stripper in getCurrentPageUrl are inverse operations on the same set of parameters, and each carried its own copy of it. Adding a parameter to one and not the other would have let a preview parameter ride back into preview-url. Both now read LIVE_PREVIEW_QUERY_PARAMS. getCurrentPageUrl drops that list plus cslp-buttons, which the edit button reads off the page URL but must not be forwarded onto internal links. Also drops preview_variant from the strip list. It appears nowhere else in src, so it was guarding against a parameter that does not exist. The copier no longer requires content_type_uid and entry_uid to be present together. Neither the SDK nor the panel produces a URL with one and not the other, and setConfigFromParams already reads them independently. Co-Authored-By: Claude --- src/utils/__test__/getCurrentPageUrl.test.ts | 2 +- src/utils/addLivePreviewQueryTags.ts | 25 +++++++------------- src/utils/getCurrentPageUrl.ts | 20 +++++----------- src/utils/livePreviewQueryParams.constant.ts | 14 +++++++++++ 4 files changed, 29 insertions(+), 32 deletions(-) create mode 100644 src/utils/livePreviewQueryParams.constant.ts diff --git a/src/utils/__test__/getCurrentPageUrl.test.ts b/src/utils/__test__/getCurrentPageUrl.test.ts index ad05d9f7..4cf6e799 100644 --- a/src/utils/__test__/getCurrentPageUrl.test.ts +++ b/src/utils/__test__/getCurrentPageUrl.test.ts @@ -16,7 +16,7 @@ describe("getCurrentPageUrl", () => { it("should drop live preview's own query params", () => { setHref( - "https://example.com/page?live_preview=abc&content_type_uid=hero&entry_uid=blt1&preview_timestamp=123" + "https://example.com/page?live_preview=abc&content_type_uid=hero&entry_uid=blt1&preview_timestamp=123&cslp-buttons=true" ); expect(getCurrentPageUrl()).toBe("https://example.com/page"); diff --git a/src/utils/addLivePreviewQueryTags.ts b/src/utils/addLivePreviewQueryTags.ts index fc1014a7..f403b54c 100644 --- a/src/utils/addLivePreviewQueryTags.ts +++ b/src/utils/addLivePreviewQueryTags.ts @@ -1,27 +1,18 @@ import { PublicLogger } from "../logger/logger"; +import { LIVE_PREVIEW_QUERY_PARAMS } from "./livePreviewQueryParams.constant"; export function addLivePreviewQueryTags(link: string): string { try { const docUrl: URL = new URL(document.location.href); const newUrl: URL = new URL(link); - const livePreviewHash: string | null = - docUrl.searchParams.get("live_preview"); - const ctUid: string | null = - docUrl.searchParams.get("content_type_uid"); - const entryUid: string | null = docUrl.searchParams.get("entry_uid"); - const previewTimestamp: string | null = docUrl.searchParams.get("preview_timestamp"); - if (livePreviewHash) { - newUrl.searchParams.set("live_preview", livePreviewHash); - } - if(ctUid && entryUid){ - newUrl.searchParams.set("content_type_uid", ctUid); - newUrl.searchParams.set("entry_uid", entryUid); - } - if (previewTimestamp) { - newUrl.searchParams.set("preview_timestamp", previewTimestamp); - } + LIVE_PREVIEW_QUERY_PARAMS.forEach((param) => { + const value: string | null = docUrl.searchParams.get(param); + if (value) { + newUrl.searchParams.set(param, value); + } + }); return newUrl.href; } catch (error) { PublicLogger.error("Error while adding live preview to URL"); return link; } -} \ No newline at end of file +} diff --git a/src/utils/getCurrentPageUrl.ts b/src/utils/getCurrentPageUrl.ts index e89fe726..b13a689c 100644 --- a/src/utils/getCurrentPageUrl.ts +++ b/src/utils/getCurrentPageUrl.ts @@ -1,18 +1,12 @@ import { PublicLogger } from "../logger/logger"; +import { LIVE_PREVIEW_QUERY_PARAMS } from "./livePreviewQueryParams.constant"; /** - * Query parameters live preview adds to the page itself. They describe the - * preview session, not the page, so they are dropped before the URL is handed - * back to the CMS. + * The preview session parameters plus `cslp-buttons`, which the edit button reads + * off the page URL. All of them describe the preview, not the page, so they are + * dropped before the URL is handed back to the CMS. */ -const LIVE_PREVIEW_QUERY_PARAMS = [ - "live_preview", - "content_type_uid", - "entry_uid", - "preview_timestamp", - "preview_variant", - "cslp-buttons", -]; +const PARAMS_TO_DROP = [...LIVE_PREVIEW_QUERY_PARAMS, "cslp-buttons"]; /** * The URL of the page the visitor is on, without live preview's own query @@ -31,9 +25,7 @@ export function getCurrentPageUrl(): string { if (typeof window === "undefined" || !window.location?.href) return ""; const url = new URL(window.location.href); - LIVE_PREVIEW_QUERY_PARAMS.forEach((param) => - url.searchParams.delete(param) - ); + PARAMS_TO_DROP.forEach((param) => url.searchParams.delete(param)); return url.href; } catch (error) { diff --git a/src/utils/livePreviewQueryParams.constant.ts b/src/utils/livePreviewQueryParams.constant.ts new file mode 100644 index 00000000..97854142 --- /dev/null +++ b/src/utils/livePreviewQueryParams.constant.ts @@ -0,0 +1,14 @@ +/** + * Query parameters live preview adds to the page URL. They describe the preview + * session, not the page. + * + * Single source for the two inverse operations on them: `addLivePreviewQueryTags` + * carries them onto internal links, `getCurrentPageUrl` drops them before the URL + * goes back to the CMS. Add a parameter here and both sides pick it up. + */ +export const LIVE_PREVIEW_QUERY_PARAMS = [ + "live_preview", + "content_type_uid", + "entry_uid", + "preview_timestamp", +];