diff --git a/README.md b/README.md index 00840c3d..d2c09aff 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ Production needs the webhook. [Revalidate webhook](#revalidate-webhook) sets it - `SANITY_API_READ_TOKEN` on the site: a Viewer token, so the site can read drafts. - `SANITY_STUDIO_PREVIEW_ORIGIN` on the Studio: the site's origin. Default `http://localhost:3000`. `SANITY_STUDIO_WEB_PREVIEW_ORIGINS` adds more, such as deploy previews. -- `NEXT_PUBLIC_SANITY_STUDIO_URL` on the site: the Studio's URL, so the site lets the Studio frame it and click-to-edit opens the right Studio. Default `http://localhost:3333`; `*.sanity.studio` is always allowed. +- `NEXT_PUBLIC_SANITY_STUDIO_URL` on the site: the Studio's URL, so the site lets the Studio frame it and click-to-edit opens the right Studio. Default `http://localhost:3333`; `*.sanity.studio` and the Sanity Dashboard (`*.sanity.io`) are always allowed. Site and Studio must read the same dataset, or the preview shows other content. diff --git a/web/.env.example b/web/.env.example index d70eb1a2..53522c4b 100644 --- a/web/.env.example +++ b/web/.env.example @@ -28,7 +28,8 @@ SANITY_API_READ_TOKEN= SANITY_REVALIDATE_SECRET= # Studio URL (BUILD): click-to-edit links and CSP `frame-ancestors`. Default -# `http://localhost:3333`; `*.sanity.studio` is always allowed. In production, the +# `http://localhost:3333`; `*.sanity.studio` and the Sanity Dashboard (`*.sanity.io`) +# are always allowed. In production, the # Studio's `SANITY_STUDIO_HOSTING_URL`. # NEXT_PUBLIC_SANITY_STUDIO_URL=https://studio.your-domain.tld diff --git a/web/next.config.ts b/web/next.config.ts index ee0bef1f..7dfc36b9 100644 --- a/web/next.config.ts +++ b/web/next.config.ts @@ -14,10 +14,10 @@ function originOf(raw: string | undefined): string | null { /** * Origin of the hosted Studio, for `frame-ancestors` — Sanity Presentation - * embeds this site in an iframe on the Studio domain. `*.sanity.studio` and - * the local Studio are always allowed; a custom Studio domain is picked up - * from `NEXT_PUBLIC_SANITY_STUDIO_URL` (the same variable the Sanity client - * uses for stega links). + * embeds this site in an iframe on the Studio domain. `*.sanity.studio`, the + * Sanity Dashboard and the local Studio are always allowed; a custom Studio + * domain is picked up from `NEXT_PUBLIC_SANITY_STUDIO_URL` (the same variable + * the Sanity client uses for stega links). */ const studioOrigin = originOf(process.env.NEXT_PUBLIC_SANITY_STUDIO_URL); @@ -101,7 +101,11 @@ const analyticsSources = { * manifests/segments/thumbnails, analytics, and the dev server's HMR * websocket. * - `frame-ancestors` must stay a header (a `` CSP cannot carry it): - * it is what lets Sanity Presentation embed the site. + * it is what lets Sanity Presentation embed the site. The browser checks + * every ancestor, not only the parent: a `*.sanity.studio` URL redirects + * into the Sanity Dashboard (`www.sanity.io`), which frames the Studio, + * which frames this site. A Studio opened from the Dashboard on its own + * domain sits in the same chain, so `*.sanity.io` is listed as well. */ function contentSecurityPolicy(): string { const directives: Record = { @@ -151,6 +155,7 @@ function contentSecurityPolicy(): string { "'self'", "http://localhost:3333", "https://*.sanity.studio", + "https://*.sanity.io", ...(studioOrigin ? [studioOrigin] : []), ], "worker-src": ["'self'", "blob:"], diff --git a/web/src/components/sanity/DisableDraftMode.tsx b/web/src/components/sanity/DisableDraftMode.tsx index 4e7f7d44..094323d0 100644 --- a/web/src/components/sanity/DisableDraftMode.tsx +++ b/web/src/components/sanity/DisableDraftMode.tsx @@ -4,16 +4,22 @@ import { useIsPresentationTool } from "next-sanity/hooks"; import { useEffect } from "react"; /** - * Drafts are for the Presentation preview only. Once the page is confirmed to run outside the - * Presentation tool (`false`, not the `null` "still detecting" state), Draft Mode is switched - * off through a full navigation, so the page re-renders on the server with published content. - * Inside Presentation (iframe or "open preview" window) nothing happens. + * Drafts are for the Presentation preview only. A top-level page that Presentation does not + * drive (`false`, not the `null` "still detecting" state) leaves Draft Mode through a full + * navigation, so it re-renders on the server with published content. + * + * Inside a frame or a window another page opened, `false` only means the handshake has not + * finished: `` reports it after one second without a connection, and the Studio + * can take longer (it may itself sit in the Sanity Dashboard's frame). Leaving Draft Mode there + * would cut Presentation off, and in its cross-site iframe the disable route cannot even clear + * the partitioned cookie — so nothing happens. */ export function DisableDraftMode() { const isPresentationTool = useIsPresentationTool(); useEffect(() => { if (isPresentationTool !== false) return; + if (window.self !== window.top || window.opener) return; const { pathname, search, hash } = window.location; const target = `${pathname}${search}${hash}`; window.location.href = `/api/draft-mode/disable?redirect=${encodeURIComponent(target)}`;