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..4cf6e799 --- /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&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"); + }); +}); 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 new file mode 100644 index 00000000..b13a689c --- /dev/null +++ b/src/utils/getCurrentPageUrl.ts @@ -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 ""; + } +} 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", +];