@@ -8,17 +8,23 @@ 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'
1414import { handleError } 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 isDraftMode = ( await draftMode ( ) ) . isEnabled
22+ const perspective = ( await isDraftMode )
23+ ? await resolvePerspectiveFromCookie ( { cookies : await cookies ( ) } )
24+ : 'published'
1925 const [ { data : settings } , { data : homePage } ] = await Promise . all ( [
20- sanityFetch ( { query : settingsQuery , stega : false } ) ,
21- sanityFetch ( { query : homePageQuery , stega : false } ) ,
26+ sanityFetch ( { query : settingsQuery , perspective , stega : false } ) ,
27+ sanityFetch ( { query : homePageQuery , perspective , stega : false } ) ,
2228 ] )
2329
2430 const ogImage = urlForOpenGraphImage (
@@ -43,31 +49,25 @@ export const viewport: Viewport = {
4349 themeColor : '#000' ,
4450}
4551
46- export default async function IndexRoute ( { children} : { children : React . ReactNode } ) {
47- const { data } = await sanityFetch ( { query : settingsQuery } )
52+ export default async function PersonalLayout ( { children} : { children : React . ReactNode } ) {
53+ const isDraftMode = ( await draftMode ( ) ) . isEnabled
4854 return (
4955 < >
5056 < 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- />
57+ < Suspense
58+ // @TODO add a fallback that reduces layout shift
59+ fallback = { null }
60+ >
61+ { isDraftMode ? (
62+ < DynamicLayout > { children } </ DynamicLayout >
63+ ) : (
64+ < CachedLayout > { children } </ CachedLayout >
6265 ) }
63- </ footer >
64- < Suspense >
65- < IntroTemplate />
6666 </ Suspense >
6767 </ div >
6868 < Toaster />
6969 < SanityLive onError = { handleError } />
70- { ( await draftMode ( ) ) . isEnabled && (
70+ { isDraftMode && (
7171 < >
7272 < DraftModeToast />
7373 < VisualEditing />
@@ -77,3 +77,56 @@ export default async function IndexRoute({children}: {children: React.ReactNode}
7777 </ >
7878 )
7979}
80+
81+ /**
82+ * Resolves cookies for the perspective in draft mode, important that it's wrapped in a <Suspense> boundary for PPR to work its magic
83+ */
84+ async function DynamicLayout ( { children} : { children : React . ReactNode } ) {
85+ const jar = await cookies ( )
86+ const perspective = await resolvePerspectiveFromCookie ( { cookies : jar } )
87+ const { data} = await sanityFetch ( { query : settingsQuery , perspective} )
88+ return < CachedContent data = { data } > { children } </ CachedContent >
89+ }
90+
91+ /**
92+ * Runs in production, takes full advantage of prerender and PPR
93+ */
94+ async function CachedLayout ( { children} : { children : React . ReactNode } ) {
95+ 'use cache'
96+
97+ const { data} = await sanityFetch ( { query : settingsQuery , perspective : 'published' , stega : false } )
98+
99+ return < CachedContent data = { data } > { children } </ CachedContent >
100+ }
101+
102+ /**
103+ * Shared by both the cached and dynamic layouts, it is its own cache layer so that even if the `sanityFetch` is
104+ * revalidated and refetched, the component output doesn't change if `data` is the same.
105+ */
106+ async function CachedContent ( {
107+ children,
108+ data,
109+ } : {
110+ children : React . ReactNode
111+ data : SettingsQueryResult
112+ } ) {
113+ 'use cache'
114+
115+ return (
116+ < >
117+ < Navbar data = { data } />
118+ < div className = "mt-20 flex-grow px-4 md:px-16 lg:px-32" > { children } </ div >
119+ < footer className = "bottom-0 w-full bg-white py-12 text-center md:py-20" >
120+ { data ?. footer && (
121+ < CustomPortableText
122+ id = { data . _id }
123+ type = { data . _type }
124+ path = { [ 'footer' ] }
125+ paragraphClasses = "text-md md:text-xl"
126+ value = { data . footer as unknown as PortableTextBlock [ ] }
127+ />
128+ ) }
129+ </ footer >
130+ </ >
131+ )
132+ }
0 commit comments