diff --git a/apps/live-next/app/alert-banner.tsx b/apps/live-next/app/alert-banner.tsx
index 225d7a0b2..7c0eb7902 100644
--- a/apps/live-next/app/alert-banner.tsx
+++ b/apps/live-next/app/alert-banner.tsx
@@ -1,20 +1,15 @@
'use client'
+import {useIsPresentationTool} from '@sanity/next-loader/hooks'
import {useRouter} from 'next/navigation'
-import {useSyncExternalStore, useTransition} from 'react'
+import {useTransition} from 'react'
import {disableDraftMode} from './actions'
-const emptySubscribe = () => () => {}
-
export default function AlertBanner() {
const router = useRouter()
const [pending, startTransition] = useTransition()
- const shouldShow = useSyncExternalStore(
- emptySubscribe,
- () => window.top === window,
- () => false,
- )
+ const shouldShow = useIsPresentationTool() === false
if (!shouldShow) return null
diff --git a/apps/live-next/app/draft-mode-status.tsx b/apps/live-next/app/draft-mode-status.tsx
index 05c32e047..bf09566d5 100644
--- a/apps/live-next/app/draft-mode-status.tsx
+++ b/apps/live-next/app/draft-mode-status.tsx
@@ -1,10 +1,18 @@
'use client'
-import {useDraftModeEnvironment, useDraftModePerspective} from '@sanity/next-loader/hooks'
+import {
+ useDraftModeEnvironment,
+ useDraftModePerspective,
+ useIsLivePreview,
+} from '@sanity/next-loader/hooks'
export function DraftModeStatus() {
+ const isLivePreview = useIsLivePreview()
const perspective = useDraftModePerspective()
const environment = useDraftModeEnvironment()
+
+ if (isLivePreview !== true) return null
+
return (
perspective: {perspective}
diff --git a/apps/live-next/app/layout.tsx b/apps/live-next/app/layout.tsx
index d5ea5cba9..48f1b7786 100644
--- a/apps/live-next/app/layout.tsx
+++ b/apps/live-next/app/layout.tsx
@@ -80,12 +80,8 @@ export default async function RootLayout({children}: {children: React.ReactNode}
>
- {(await draftMode()).isEnabled && (
- <>
-
-
- >
- )}
+ {(await draftMode()).isEnabled && }
+
{children}
diff --git a/apps/live-next/package.json b/apps/live-next/package.json
index 5406f0dfb..ed5c74b77 100644
--- a/apps/live-next/package.json
+++ b/apps/live-next/package.json
@@ -7,7 +7,7 @@
"build": "next build",
"debug": "NEXT_PRIVATE_DEBUG_CACHE=1 next build --profile && NEXT_PRIVATE_DEBUG_CACHE=1 next start -p 3009",
"predev": "npm run typegen",
- "dev": "next dev -p 3009 --turbo",
+ "dev": "next dev -p 3009",
"lint": "next lint",
"start": "next start",
"typegen": "sanity typegen generate"
diff --git a/packages/next-loader/src/client-components/live/SanityLive.tsx b/packages/next-loader/src/client-components/live/SanityLive.tsx
index 70daf3308..8e09b8393 100644
--- a/packages/next-loader/src/client-components/live/SanityLive.tsx
+++ b/packages/next-loader/src/client-components/live/SanityLive.tsx
@@ -40,6 +40,12 @@ export interface SanityLiveProps
tag: string
}
+// @TODO these should be reusable utils in visual-editing-helpers
+
+const isMaybePreviewIframe = () => window !== window.parent
+const isMaybePreviewWindow = () => !!window.opener
+const isMaybePresentation = () => isMaybePreviewIframe() || isMaybePreviewWindow()
+
/**
* @public
*/
@@ -207,21 +213,35 @@ export function SanityLive(props: SanityLiveProps): React.JSX.Element | null {
* 5. Notify what environment we're in, when in Draft Mode
*/
useEffect(() => {
- if (draftModeEnabled && loadComlink) {
- setEnvironment(opener ? 'presentation-window' : 'presentation-iframe')
- } else if (draftModeEnabled && token) {
+ // If we might be in Presentation Tool, then skip detecting here as it's handled later
+ if (isMaybePresentation()) return
+
+ // If we're definitely not in Presentation Tool, then we can set the environment as stand-alone live preview
+ // if we have both a browser token, and draft mode is enabled
+ if (draftModeEnabled && token) {
setEnvironment('live')
- } else {
- setEnvironment('unknown')
+ return
+ }
+ // If we're in draft mode, but don't have a browser token, then we're in static mode
+ // which means that published content is still live, but draft changes likely need manual refresh
+ if (draftModeEnabled) {
+ setEnvironment('static')
+ return
}
- }, [draftModeEnabled, loadComlink, token])
+
+ // Fallback to `unknown` otherwise, as we simply don't know how it's setup
+ setEnvironment('unknown')
+ return
+ }, [draftModeEnabled, token])
/**
* 6. If Presentation Tool is detected, load up the comlink and integrate with it
*/
useEffect(() => {
- if (window === parent && !opener) return
+ if (!isMaybePresentation()) return
const controller = new AbortController()
+ // Wait for a while to see if Presentation Tool is detected, before assuming the env to be stand-alone live preview
+ const timeout = setTimeout(() => setEnvironment('live'), 3_000)
window.addEventListener(
'message',
({data}: MessageEvent) => {
@@ -233,13 +253,18 @@ export function SanityLive(props: SanityLiveProps): React.JSX.Element | null {
'from' in data &&
data.from === 'presentation'
) {
+ clearTimeout(timeout)
+ setEnvironment(isMaybePreviewWindow() ? 'presentation-window' : 'presentation-iframe')
setLoadComlink(true)
controller.abort()
}
},
{signal: controller.signal},
)
- return () => controller.abort()
+ return () => {
+ clearTimeout(timeout)
+ controller.abort()
+ }
}, [])
/**
diff --git a/packages/next-loader/src/hooks/context.ts b/packages/next-loader/src/hooks/context.ts
index 54f9e4be4..903f49eef 100644
--- a/packages/next-loader/src/hooks/context.ts
+++ b/packages/next-loader/src/hooks/context.ts
@@ -32,6 +32,7 @@ export type DraftEnvironment =
| 'presentation-iframe'
| 'presentation-window'
| 'live'
+ | 'static'
| 'unknown'
/** @internal */
diff --git a/packages/next-loader/src/hooks/index.ts b/packages/next-loader/src/hooks/index.ts
index ee2b00673..6733636e5 100644
--- a/packages/next-loader/src/hooks/index.ts
+++ b/packages/next-loader/src/hooks/index.ts
@@ -3,3 +3,5 @@
export * from './useDraftMode'
export type {DraftPerspective, DraftEnvironment} from './context'
export type {ClientPerspective} from '@sanity/client'
+export * from './useIsPresentationTool'
+export * from './useIsLivePreview'
diff --git a/packages/next-loader/src/hooks/useIsLivePreview.ts b/packages/next-loader/src/hooks/useIsLivePreview.ts
new file mode 100644
index 000000000..ee761f6cf
--- /dev/null
+++ b/packages/next-loader/src/hooks/useIsLivePreview.ts
@@ -0,0 +1,25 @@
+import {useDraftModeEnvironment} from './useDraftMode'
+
+/**
+ * Detects if the application is considered to be in a "Live Preview" mode.
+ * Live Preview means that the application is either:
+ * - being previewed inside Sanity Presentation Tool
+ * - being previewed in Draft Mode, with a `browserToken` given to `defineLive`, also known as "Standalone Live Preview'"
+ * When in Live Preview mode, you typically want UI to update as new content comes in, without any manual intervention.
+ * This is very different from Live Production mode, where you usually want to delay updates that might cause layout shifts,
+ * to avoid interrupting the user that is consuming your content.
+ * This hook lets you adapt to this difference, making sure production doesn't cause layout shifts that worsen the UX,
+ * while in Live Preview mode layout shift is less of an issue and it's better for the editorial experience to auto refresh in real time.
+ *
+ * The hook returns `null` initially, to signal it doesn't yet know if it's live previewing or not.
+ * Then `true` if it is, and `false` otherwise.
+ * @public
+ */
+export function useIsLivePreview(): boolean | null {
+ const environment = useDraftModeEnvironment()
+ return environment === 'checking'
+ ? null
+ : environment === 'presentation-iframe' ||
+ environment === 'presentation-window' ||
+ environment === 'live'
+}
diff --git a/packages/next-loader/src/hooks/useIsPresentationTool.ts b/packages/next-loader/src/hooks/useIsPresentationTool.ts
new file mode 100644
index 000000000..caf6a9c39
--- /dev/null
+++ b/packages/next-loader/src/hooks/useIsPresentationTool.ts
@@ -0,0 +1,18 @@
+import {useDraftModeEnvironment} from './useDraftMode'
+
+/**
+ * Detects if the application is being previewed inside Sanity Presentation Tool.
+ * Presentation Tool can open the application in an iframe, or in a new window.
+ * When in this context there are some UI you usually don't want to show,
+ * for example a Draft Mode toggle, or a "Viewing draft content" indicators, these are unnecessary and add clutter to
+ * the editorial experience.
+ * The hook returns `null` initially, when it's not yet sure if the application is running inside Presentation Tool,
+ * then `true` if it is, and `false` otherwise.
+ * @public
+ */
+export function useIsPresentationTool(): boolean | null {
+ const environment = useDraftModeEnvironment()
+ return environment === 'checking'
+ ? null
+ : environment === 'presentation-iframe' || environment === 'presentation-window'
+}