Skip to content
Merged
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
34 changes: 33 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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))
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<script type="module" crossorigin="anonymous">
import ContentstackLivePreview from "https://esm.sh/@contentstack/live-preview-utils@4.5.0";
import ContentstackLivePreview from "https://esm.sh/@contentstack/live-preview-utils@4.5.1";

ContentstackLivePreview.init({
stackDetails: {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -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");

Expand Down
10 changes: 10 additions & 0 deletions src/livePreview/editButton/editButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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}`;
}

Expand Down
38 changes: 38 additions & 0 deletions src/utils/__test__/getCurrentPageUrl.test.ts
Original file line number Diff line number Diff line change
@@ -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&cslp-buttons=true"
);

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");
});
});
25 changes: 8 additions & 17 deletions src/utils/addLivePreviewQueryTags.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
35 changes: 35 additions & 0 deletions src/utils/getCurrentPageUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { PublicLogger } from "../logger/logger";
import { LIVE_PREVIEW_QUERY_PARAMS } from "./livePreviewQueryParams.constant";

/**
* 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 PARAMS_TO_DROP = [...LIVE_PREVIEW_QUERY_PARAMS, "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);
PARAMS_TO_DROP.forEach((param) => url.searchParams.delete(param));

return url.href;
} catch (error) {
PublicLogger.error("Error while reading the current page URL");
return "";
}
}
14 changes: 14 additions & 0 deletions src/utils/livePreviewQueryParams.constant.ts
Original file line number Diff line number Diff line change
@@ -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",
];
Loading