@@ -8,17 +8,21 @@ import {urlForOpenGraphImage} from '@/sanity/lib/utils'
88import type { Metadata , Viewport } from 'next'
99import { toPlainText , type PortableTextBlock } from 'next-sanity'
1010import { VisualEditing } from 'next-sanity/visual-editing'
11- import { draftMode } from 'next/headers'
11+ import { cookies , draftMode } from 'next/headers'
1212import { Suspense } from 'react'
1313import { Toaster } from 'sonner'
14- import { handleError } from './client-functions'
14+ import { handleError , revalidateSyncTags } from './client-functions'
1515import { DraftModeToast } from './DraftModeToast'
1616import { SpeedInsights } from '@vercel/speed-insights/next'
17+ import { resolvePerspectiveFromCookie } from 'next-sanity/experimental/live'
18+ import type { SettingsQueryResult } from '@/sanity.types'
1719
1820export async function generateMetadata ( ) : Promise < Metadata > {
21+ const jar = await cookies ( )
22+ const perspective = await resolvePerspectiveFromCookie ( { cookies : jar } )
1923 const [ { data : settings } , { data : homePage } ] = await Promise . all ( [
20- sanityFetch ( { query : settingsQuery , stega : false } ) ,
21- sanityFetch ( { query : homePageQuery , stega : false } ) ,
24+ sanityFetch ( { query : settingsQuery , stega : false , perspective } ) ,
25+ sanityFetch ( { query : homePageQuery , stega : false , perspective } ) ,
2226 ] )
2327
2428 const ogImage = urlForOpenGraphImage (
@@ -43,31 +47,32 @@ export const viewport: Viewport = {
4347 themeColor : '#000' ,
4448}
4549
46- export default async function IndexRoute ( { children} : { children : React . ReactNode } ) {
47- const { data } = await sanityFetch ( { query : settingsQuery } )
50+ export default async function PersonalLayout ( { children} : { children : React . ReactNode } ) {
51+ const isDraftMode = ( await draftMode ( ) ) . isEnabled
4852 return (
4953 < >
5054 < div className = "flex min-h-screen flex-col bg-white text-black" >
51- < Navbar data = { data } />
52- < div className = "mt-20 flex-grow px-4 md:px-16 lg:px-32" > { children } </ div >
53- < footer className = "bottom-0 w-full bg-white py-12 text-center md:py-20" >
54- { data ?. footer && (
55- < CustomPortableText
56- id = { data . _id }
57- type = { data . _type }
58- path = { [ 'footer' ] }
59- paragraphClasses = "text-md md:text-xl"
60- value = { data . footer as unknown as PortableTextBlock [ ] }
61- />
55+ < Suspense
56+ // @TODO add a fallback that reduces layout shift
57+ fallback = { null }
58+ >
59+ { isDraftMode ? (
60+ < DynamicLayout > { children } </ DynamicLayout >
61+ ) : (
62+ < CachedLayout > { children } </ CachedLayout >
6263 ) }
63- </ footer >
64+ </ Suspense >
6465 < Suspense >
6566 < IntroTemplate />
6667 </ Suspense >
6768 </ div >
6869 < Toaster />
69- < SanityLive onError = { handleError } />
70- { ( await draftMode ( ) ) . isEnabled && (
70+ < SanityLive
71+ onError = { handleError }
72+ // @TODO re-enable when the BYPASS on draft mode situation is figured out
73+ // revalidateSyncTags={revalidateSyncTags}
74+ />
75+ { isDraftMode && (
7176 < >
7277 < DraftModeToast />
7378 < VisualEditing />
@@ -77,3 +82,56 @@ export default async function IndexRoute({children}: {children: React.ReactNode}
7782 </ >
7883 )
7984}
85+
86+ /**
87+ * Resolves cookies for the perspective in draft mode, important that it's wrapped in a <Suspense> boundary for PPR to work its magic
88+ */
89+ async function DynamicLayout ( { children} : { children : React . ReactNode } ) {
90+ const jar = await cookies ( )
91+ const perspective = await resolvePerspectiveFromCookie ( { cookies : jar } )
92+ const { data} = await sanityFetch ( { query : settingsQuery , perspective} )
93+ return < CachedContent data = { data } > { children } </ CachedContent >
94+ }
95+
96+ /**
97+ * Runs in production, takes full advantage of prerender and PPR
98+ */
99+ async function CachedLayout ( { children} : { children : React . ReactNode } ) {
100+ 'use cache'
101+
102+ const { data} = await sanityFetch ( { query : settingsQuery , perspective : 'published' , stega : false } )
103+
104+ return < CachedContent data = { data } > { children } </ CachedContent >
105+ }
106+
107+ /**
108+ * Shared by both the cached and dynamic layouts, it is its own cache layer so that even if the `sanityFetch` is
109+ * revalidated and refetched, the component output doesn't change if `data` is the same.
110+ */
111+ async function CachedContent ( {
112+ children,
113+ data,
114+ } : {
115+ children : React . ReactNode
116+ data : SettingsQueryResult
117+ } ) {
118+ 'use cache'
119+
120+ return (
121+ < >
122+ < Navbar data = { data } />
123+ < div className = "mt-20 flex-grow px-4 md:px-16 lg:px-32" > { children } </ div >
124+ < footer className = "bottom-0 w-full bg-white py-12 text-center md:py-20" >
125+ { data ?. footer && (
126+ < CustomPortableText
127+ id = { data . _id }
128+ type = { data . _type }
129+ path = { [ 'footer' ] }
130+ paragraphClasses = "text-md md:text-xl"
131+ value = { data . footer as unknown as PortableTextBlock [ ] }
132+ />
133+ ) }
134+ </ footer >
135+ </ >
136+ )
137+ }
0 commit comments