Skip to content

Commit 7e8ebcd

Browse files
committed
test cache components
1 parent 55b9c3b commit 7e8ebcd

File tree

19 files changed

+13307
-19380
lines changed

19 files changed

+13307
-19380
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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ import {studioUrl} from '@/sanity/lib/api'
33
import {sanityFetch} from '@/sanity/lib/live'
44
import {homePageQuery} from '@/sanity/lib/queries'
55
import Link from 'next/link'
6+
import {resolvePerspectiveFromCookie} from 'next-sanity/experimental/live'
7+
import {cookies} from 'next/headers'
68

79
export default async function IndexRoute() {
8-
const {data} = await sanityFetch({query: homePageQuery})
10+
const jar = await cookies()
11+
const perspective = await resolvePerspectiveFromCookie({cookies: jar})
12+
const {data} = await sanityFetch({query: homePageQuery, perspective})
913

1014
if (!data) {
1115
return (

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
}

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.

next.config.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
1-
import {NextConfig} from 'next'
1+
import type {NextConfig} from 'next'
22

33
const config: NextConfig = {
4-
// Helps catch bugs
5-
reactStrictMode: true,
64
experimental: {
75
// Speeds up performance by automatically generating useMemo and useCallback in client components
86
reactCompiler: true,
7+
// Required by `next-sanity/experimental/live`
8+
cacheComponents: true,
9+
cacheLife: {
10+
default: {
11+
// Sanity Live handles on-demand revalidation, so the default 15min time based revalidation is too short
12+
revalidate: 60 * 60 * 24 * 90, // 90 days
13+
},
14+
},
915
},
16+
// Helps catch bugs
17+
reactStrictMode: true,
1018
images: {
1119
remotePatterns: [{hostname: 'cdn.sanity.io'}],
1220
},

0 commit comments

Comments
 (0)