Skip to content

Commit 8021a82

Browse files
committed
test cache components
1 parent 55b9c3b commit 8021a82

File tree

20 files changed

+13341
-19394
lines changed

20 files changed

+13341
-19394
lines changed

app/(personal)/[slug]/page.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import {sanityFetch} from '@/sanity/lib/live'
44
import {pagesBySlugQuery, slugsByTypeQuery} from '@/sanity/lib/queries'
55
import type {Metadata, ResolvingMetadata} from 'next'
66
import {toPlainText, type PortableTextBlock} from 'next-sanity'
7-
import {draftMode} from 'next/headers'
7+
import {cookies, draftMode} from 'next/headers'
88
import {notFound} from 'next/navigation'
9+
import {resolvePerspectiveFromCookie} from 'next-sanity/experimental/live'
910

1011
type Props = {
1112
params: Promise<{slug: string}>
@@ -15,10 +16,13 @@ export async function generateMetadata(
1516
{params}: Props,
1617
parent: ResolvingMetadata,
1718
): Promise<Metadata> {
19+
const jar = await cookies()
20+
const perspective = await resolvePerspectiveFromCookie({cookies: jar})
1821
const {data: page} = await sanityFetch({
1922
query: pagesBySlugQuery,
2023
params,
2124
stega: false,
25+
perspective,
2226
})
2327

2428
return {
@@ -38,7 +42,9 @@ export async function generateStaticParams() {
3842
}
3943

4044
export default async function PageSlugRoute({params}: Props) {
41-
const {data} = await sanityFetch({query: pagesBySlugQuery, params})
45+
const jar = await cookies()
46+
const perspective = await resolvePerspectiveFromCookie({cookies: jar})
47+
const {data} = await sanityFetch({query: pagesBySlugQuery, params, perspective})
4248

4349
// Only show the 404 page if we're in production, when in draft mode we might be about to create a page on this slug, and live reload won't work on the 404 route
4450
if (!data?._id && !(await draftMode()).isEnabled) {

app/(personal)/client-functions.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client'
22

3+
import {type SyncTag} from '@sanity/client'
34
import {isCorsOriginError} from 'next-sanity/live'
45
import {toast} from 'sonner'
56

@@ -27,3 +28,16 @@ export function handleError(error: unknown) {
2728
})
2829
}
2930
}
31+
32+
export async function revalidateSyncTags(tags: SyncTag[]): Promise<'refresh'> {
33+
const url = new URL('/api/revalidate-sync-tags', window.location.origin)
34+
for (const tag of tags) {
35+
url.searchParams.append('tag', tag)
36+
}
37+
const response = await fetch(url, {method: 'POST'})
38+
if (!response.ok) {
39+
throw new Error('Failed to revalidate sync tags')
40+
}
41+
console.log('revalidating sync tags', tags)
42+
return 'refresh'
43+
}

app/(personal)/layout.tsx

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,21 @@ import {urlForOpenGraphImage} from '@/sanity/lib/utils'
88
import type {Metadata, Viewport} from 'next'
99
import {toPlainText, type PortableTextBlock} from 'next-sanity'
1010
import {VisualEditing} from 'next-sanity/visual-editing'
11-
import {draftMode} from 'next/headers'
11+
import {cookies, draftMode} from 'next/headers'
1212
import {Suspense} from 'react'
1313
import {Toaster} from 'sonner'
14-
import {handleError} from './client-functions'
14+
import {handleError, revalidateSyncTags} from './client-functions'
1515
import {DraftModeToast} from './DraftModeToast'
1616
import {SpeedInsights} from '@vercel/speed-insights/next'
17+
import {resolvePerspectiveFromCookie} from 'next-sanity/experimental/live'
18+
import type {SettingsQueryResult} from '@/sanity.types'
1719

1820
export 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+
}

app/(personal)/page.tsx

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,35 @@
11
import {HomePage} from '@/components/HomePage'
2-
import {studioUrl} from '@/sanity/lib/api'
32
import {sanityFetch} from '@/sanity/lib/live'
43
import {homePageQuery} from '@/sanity/lib/queries'
5-
import Link from 'next/link'
4+
import {resolvePerspectiveFromCookie} from 'next-sanity/experimental/live'
5+
import {cookies, draftMode} from 'next/headers'
6+
import {Suspense} from 'react'
67

78
export default async function IndexRoute() {
8-
const {data} = await sanityFetch({query: homePageQuery})
9+
const isDraftMode = (await draftMode()).isEnabled
910

10-
if (!data) {
11-
return (
12-
<div className="text-center">
13-
You don&rsquo;t have a homepage yet,{' '}
14-
<Link href={`${studioUrl}/structure/home`} className="underline">
15-
create one now
16-
</Link>
17-
!
18-
</div>
19-
)
20-
}
11+
return (
12+
<Suspense
13+
// @TODO add a fallback that reduces layout shift
14+
fallback={null}
15+
>
16+
{isDraftMode ? <DynamicHomePage /> : <CachedHomePage />}
17+
</Suspense>
18+
)
19+
}
20+
21+
async function DynamicHomePage() {
22+
const jar = await cookies()
23+
const perspective = await resolvePerspectiveFromCookie({cookies: jar})
24+
const {data} = await sanityFetch({query: homePageQuery, perspective})
25+
26+
return <HomePage data={data} />
27+
}
28+
29+
async function CachedHomePage() {
30+
'use cache'
31+
32+
const {data} = await sanityFetch({query: homePageQuery, perspective: 'published', stega: false})
2133

2234
return <HomePage data={data} />
2335
}

app/(personal)/projects/[slug]/page.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import {projectBySlugQuery, slugsByTypeQuery} from '@/sanity/lib/queries'
77
import {urlForOpenGraphImage} from '@/sanity/lib/utils'
88
import type {Metadata, ResolvingMetadata} from 'next'
99
import {createDataAttribute, toPlainText} from 'next-sanity'
10-
import {draftMode} from 'next/headers'
10+
import {cookies, draftMode} from 'next/headers'
1111
import Link from 'next/link'
1212
import {notFound} from 'next/navigation'
13+
import {resolvePerspectiveFromCookie} from 'next-sanity/experimental/live'
1314

1415
type Props = {
1516
params: Promise<{slug: string}>
@@ -19,9 +20,12 @@ export async function generateMetadata(
1920
{params}: Props,
2021
parent: ResolvingMetadata,
2122
): Promise<Metadata> {
23+
const jar = await cookies()
24+
const perspective = await resolvePerspectiveFromCookie({cookies: jar})
2225
const {data: project} = await sanityFetch({
2326
query: projectBySlugQuery,
2427
params,
28+
perspective,
2529
stega: false,
2630
})
2731
const ogImage = urlForOpenGraphImage(
@@ -51,7 +55,9 @@ export async function generateStaticParams() {
5155
}
5256

5357
export default async function ProjectSlugRoute({params}: Props) {
54-
const {data} = await sanityFetch({query: projectBySlugQuery, params})
58+
const jar = await cookies()
59+
const perspective = await resolvePerspectiveFromCookie({cookies: jar})
60+
const {data} = await sanityFetch({query: projectBySlugQuery, params, perspective})
5561

5662
// Only show the 404 page if we're in production, when in draft mode we might be about to create a project on this slug, and live reload won't work on the 404 route
5763
if (!data?._id && !(await draftMode()).isEnabled) {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type {NextRequest} from 'next/server'
2+
import {unstable_expireTag as expireTag} from 'next/cache'
3+
import {draftMode} from 'next/headers'
4+
5+
export async function POST(request: NextRequest) {
6+
const {isEnabled: isDraftModeEnabled} = await draftMode()
7+
const tags = request.nextUrl.searchParams
8+
.getAll('tag')
9+
.map((tag) => (isDraftModeEnabled ? `drafts:${tag}` : `sanity:${tag}`))
10+
11+
// If in draft mode all tags must be prefixed with 'draft:'
12+
// if ((await draftMode()).isEnabled) {
13+
// if (!tags.every((tag) => tag.startsWith('drafts:'))) {
14+
// return Response.json(
15+
// {error: 'All tags must be prefixed with "drafts:" in draft mode'},
16+
// {status: 400},
17+
// )
18+
// }
19+
// } else if (!tags.every((tag) => tag.startsWith('sanity:'))) {
20+
// return Response.json(
21+
// {error: 'All tags must be prefixed with "sanity:" in production mode'},
22+
// {status: 400},
23+
// )
24+
// }
25+
26+
expireTag(...tags)
27+
console.log(`<SanityLive /> expired tags: ${tags.join(', ')}`)
28+
29+
return Response.json(tags)
30+
}

app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const mono = IBM_Plex_Mono({
1919
weight: ['500', '700'],
2020
})
2121

22-
export default async function RootLayout({children}: {children: React.ReactNode}) {
22+
export default function RootLayout({children}: {children: React.ReactNode}) {
2323
return (
2424
<html lang="en" className={`${mono.variable} ${sans.variable} ${serif.variable}`}>
2525
<body>{children}</body>

app/studio/[[...index]]/page.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use cache'
2+
13
/**
24
* This route is responsible for the built-in authoring environment using Sanity Studio v3.
35
* All routes under /studio will be handled by this file using Next.js' catch-all routes:
@@ -9,11 +11,16 @@
911

1012
import config from '@/sanity.config'
1113
import {NextStudio} from 'next-sanity/studio'
14+
import {unstable_cacheLife as cacheLife} from 'next/cache'
1215

13-
export const dynamic = 'force-static'
14-
15-
export {metadata, viewport} from 'next-sanity/studio'
16-
17-
export default function StudioPage() {
18-
return <NextStudio config={config} />
16+
export default async function StudioPage() {
17+
cacheLife('max')
18+
return (
19+
<>
20+
<meta name="referrer" content="same-origin" />
21+
<meta name="robots" content="noindex" />
22+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
23+
<NextStudio config={config} />
24+
</>
25+
)
1926
}

components/HomePage.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,20 @@ export interface HomePageProps {
1212
}
1313

1414
export async function HomePage({data}: HomePageProps) {
15+
if (!data) {
16+
return (
17+
<div className="text-center">
18+
You don&rsquo;t have a homepage yet,{' '}
19+
<Link href={`${studioUrl}/structure/home`} className="underline">
20+
create one now
21+
</Link>
22+
!
23+
</div>
24+
)
25+
}
26+
1527
// Default to an empty object to allow previews on non-existent documents
16-
const {overview = [], showcaseProjects = [], title = ''} = data ?? {}
28+
const {overview = [], showcaseProjects = [], title = ''} = data
1729

1830
const dataAttribute =
1931
data?._id && data?._type

next-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3-
/// <reference path="./.next/types/routes.d.ts" />
3+
import "./.next/types/routes.d.ts";
44

55
// NOTE: This file should not be edited
66
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

0 commit comments

Comments
 (0)