@decocms/nextjs 7.31.0 → 7.31.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/nextjs",
3
- "version": "7.31.0",
3
+ "version": "7.31.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -29,8 +29,8 @@
29
29
  "lint:unused": "knip"
30
30
  },
31
31
  "dependencies": {
32
- "@decocms/blocks": "7.31.0",
33
- "@decocms/blocks-admin": "7.31.0"
32
+ "@decocms/blocks": "7.31.2",
33
+ "@decocms/blocks-admin": "7.31.2"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "next": ">=15.0.0",
@@ -1,5 +1,5 @@
1
1
  import { renderToString } from "react-dom/server";
2
- import { describe, expect, it } from "vitest";
2
+ import { afterEach, describe, expect, it } from "vitest";
3
3
 
4
4
  import { DraftPreviewBadge } from "./DraftPreviewBadge";
5
5
 
@@ -13,4 +13,20 @@ describe("DraftPreviewBadge (render)", () => {
13
13
  const html = renderToString(<DraftPreviewBadge pointer="fila.vtex.app@v1" />);
14
14
  expect(html).toContain("data:image/png;base64,");
15
15
  });
16
+
17
+ describe("embedded in an iframe", () => {
18
+ const realTop = window.top;
19
+ afterEach(() => {
20
+ Object.defineProperty(window, "top", {
21
+ value: realTop,
22
+ configurable: true,
23
+ });
24
+ });
25
+
26
+ it("renders nothing — Studio's own preview surface already has its own chrome", () => {
27
+ Object.defineProperty(window, "top", { value: {}, configurable: true });
28
+ const html = renderToString(<DraftPreviewBadge pointer="fila.vtex.app@v1" />);
29
+ expect(html).toBe("");
30
+ });
31
+ });
16
32
  });
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
- import { buildExitUrl, buildShareUrl } from "./DraftPreviewBadge";
3
+ import { buildExitUrl, buildShareUrl, isFramed } from "./DraftPreviewBadge";
4
4
 
5
5
  describe("buildShareUrl", () => {
6
6
  it("pins the exact draft version so a recipient sees the same working tree", () => {
@@ -35,3 +35,21 @@ describe("buildExitUrl", () => {
35
35
  );
36
36
  });
37
37
  });
38
+
39
+ describe("isFramed", () => {
40
+ it("is false for a same top window — the ordinary top-level tab case", () => {
41
+ const win = {} as typeof window;
42
+ win.top = win;
43
+ expect(isFramed(win)).toBe(false);
44
+ });
45
+
46
+ it("is true when top differs — embedded in another page's iframe", () => {
47
+ const win = {} as typeof window;
48
+ win.top = {} as typeof window; // a distinct object: the parent frame
49
+ expect(isFramed(win)).toBe(true);
50
+ });
51
+
52
+ it("fails open (not framed) with no window — SSR / no hydration", () => {
53
+ expect(isFramed(undefined)).toBe(false);
54
+ });
55
+ });
@@ -15,6 +15,13 @@
15
15
  * the host site's CSS/Tailwind), and a very high z-index, so it renders the
16
16
  * same on any consumer site. Only mounted when a draft is active
17
17
  * (`DraftPreviewIndicator` gates it), so it never costs ordinary traffic.
18
+ *
19
+ * Hidden inside an iframe (`isFramed`): Studio's own preview surface embeds
20
+ * this exact draft render in an iframe that already has its own chrome
21
+ * (toolbar, version status) — floating a second "you're in preview" badge
22
+ * inside that frame would be redundant clutter, not a signal a reviewer
23
+ * needs. A reviewer following a shared preview link in their own top-level
24
+ * tab still sees it; only the embedded case is suppressed.
18
25
  */
19
26
  import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
20
27
  import { DECO_MARK_DATA_URI } from "./decoMark";
@@ -49,6 +56,19 @@ export function buildExitUrl(href: string): string {
49
56
  return url.toString();
50
57
  }
51
58
 
59
+ /**
60
+ * Whether this render is embedded in another page's iframe — Studio's own
61
+ * preview surface, primarily. `window.top !== window` is a same-origin
62
+ * reference comparison, so it never throws cross-origin (unlike reading a
63
+ * cross-origin frame's properties). No `window` (SSR, or a consumer site
64
+ * that never hydrates this component) reports not-framed — fails open,
65
+ * matching the badge's "renders the same on any consumer site" guarantee.
66
+ * Pure and exported for tests.
67
+ */
68
+ export function isFramed(win: typeof window | undefined = globalThis.window): boolean {
69
+ return typeof win !== "undefined" && win.top !== win;
70
+ }
71
+
52
72
  const DECO_GREEN = "#0b3d1e";
53
73
  /** Toast background — the deco lime. Text sits on it in dark green for contrast. */
54
74
  const DECO_LIME = "#d0ec1a";
@@ -137,6 +157,12 @@ export function DraftPreviewBadge({ pointer }: DraftPreviewBadgeProps) {
137
157
  }
138
158
  }, [pointer]);
139
159
 
160
+ // Checked inline, not cached in state behind an effect: it's a free
161
+ // reference compare, and computing it during render means the client's
162
+ // first (hydration) pass already agrees with what it will show — no
163
+ // post-hydration flash for the common (unframed) case.
164
+ if (isFramed()) return null;
165
+
140
166
  return (
141
167
  <div
142
168
  ref={rootRef}