From 7cb20e8457aff780f398a0816aecb9edeea06df8 Mon Sep 17 00:00:00 2001 From: Kirtesh Suthar Date: Mon, 10 Aug 2026 17:07:50 +0530 Subject: [PATCH 1/4] 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/4] 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", +]; From 183006aceafe3e31c828d74345b928a99a3d4273 Mon Sep 17 00:00:00 2001 From: Kirtesh Suthar Date: Thu, 10 Sep 2026 15:44:35 +0530 Subject: [PATCH 3/4] 4.5.1 --- CHANGELOG.md | 34 +++++++++++++++++++++++++++++++++- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18eae937..e7a59903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,35 @@ # Changelog +## [v4.5.1](https://github.com/contentstack/live-preview-sdk/compare/v4.5.0...v4.5.1) + +> 10 September 2026 + +### Fixes + +- fix(security): bump dompurify to patch XSS vulnerability (Hitesh Shetty - [#638](https://github.com/contentstack/live-preview-sdk/pull/638)) +- fix(edit-button): send the page the editor was on to the CMS (Kirtesh Suthar - [#635](https://github.com/contentstack/live-preview-sdk/pull/635)) + +### Chores And Housekeeping + +- chore: sync develop_v4 with stage_v4 (Kirtesh Suthar - [#645](https://github.com/contentstack/live-preview-sdk/pull/645)) +- chore(tests): standardize api key placeholder values in specs (Hitesh Shetty - [#639](https://github.com/contentstack/live-preview-sdk/pull/639)) + +### General Changes + +- Develop v4 (Kirtesh Suthar - [#640](https://github.com/contentstack/live-preview-sdk/pull/640)) + +### Chores And Housekeeping + +- chore: merge stage_v4 into develop_v4 (Kirtesh Suthar - [e1dee0f](https://github.com/contentstack/live-preview-sdk/commit/e1dee0f33c15fbbeb9d7842ad70aba7610801524)) +- chore(deps): apply npm audit fix (hitesh-shetty-cstk - [ec144b9](https://github.com/contentstack/live-preview-sdk/commit/ec144b934935185f40efccfdcd09d4ac153222c8)) + +### Refactoring and Updates + +- refactor(utils): one list for live preview's own query params (Kirtesh Suthar - [c179114](https://github.com/contentstack/live-preview-sdk/commit/c1791141dd7f22430f4f96d8070f4354dc9b641e)) + ## [v4.5.0](https://github.com/contentstack/live-preview-sdk/compare/v4.4.5...v4.5.0) -> 5 August 2026 +> 11 August 2026 ### New Features @@ -15,12 +42,17 @@ ### General Changes +- Release v4.5.0 (Karan Bhavesh Gandhi - [#632](https://github.com/contentstack/live-preview-sdk/pull/632)) - develop to stage 6 aug release (Karan Bhavesh Gandhi - [#631](https://github.com/contentstack/live-preview-sdk/pull/631)) ### Fixes - fix(VB-1797): keep dir=auto working on the pseudo editable element (Karan Gandhi - [a06ad34](https://github.com/contentstack/live-preview-sdk/commit/a06ad3403106cad06d899034f76d2d9a180b7460)) +### Documentation Changes + +- docs: bump README CDN URL to 4.5.0 (Karan Gandhi - [76f5ca7](https://github.com/contentstack/live-preview-sdk/commit/76f5ca73387965c74b54bf032ea45a8d2ca1bf82)) + ### General Changes - sca-scan.yml (Aravind Kumar - [9852a61](https://github.com/contentstack/live-preview-sdk/commit/9852a61b443e8fd75cef3fedf95f1a72b648b874)) diff --git a/package-lock.json b/package-lock.json index c996d37d..15b27b0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@contentstack/live-preview-utils", - "version": "4.5.0", + "version": "4.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@contentstack/live-preview-utils", - "version": "4.5.0", + "version": "4.5.1", "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.7.2", diff --git a/package.json b/package.json index e210bf2b..350bee73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/live-preview-utils", - "version": "4.5.0", + "version": "4.5.1", "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", From e93cc36a182d084fed01b3ee5b3262a51fb159fd Mon Sep 17 00:00:00 2001 From: Kirtesh Suthar Date: Thu, 10 Sep 2026 15:44:48 +0530 Subject: [PATCH 4/4] fix: update CDN version to 4.5.1 in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0acab9b5..a136de21 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,11 @@ npm install @contentstack/live-preview-utils ### Load from a CDN (advanced) -Pin the version to match your app (update `4.5.0` when you upgrade): +Pin the version to match your app (update `4.5.1` when you upgrade): ```html