Skip to content

Commit fcbc987

Browse files
authored
chore(templates): strengthen types in preview url gen (#14947)
The templates have no safety between the frontends entering and exiting draft mode, and the endpoints that handle those requests. We need to ensure that the search params that the endpoint relies on, e.g. `path`, are typed in the requests that send them.
1 parent 3dc6041 commit fcbc987

9 files changed

Lines changed: 52 additions & 52 deletions

File tree

docs/admin/preview.mdx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ The following arguments are provided to the `preview` function:
4949

5050
The `options` object contains the following properties:
5151

52-
| Path | Description |
53-
| ------------ | ----------------------------------------------------- |
54-
| **`locale`** | The current locale of the Document being edited. |
55-
| **`req`** | The Payload Request object. |
56-
| **`token`** | The JWT token of the currently authenticated user. |
52+
| Path | Description |
53+
| ------------ | -------------------------------------------------- |
54+
| **`locale`** | The current locale of the Document being edited. |
55+
| **`req`** | The Payload Request object. |
56+
| **`token`** | The JWT token of the currently authenticated user. |
5757

5858
If your application requires a fully qualified URL, such as within deploying to Vercel Preview Deployments, you can use the `req` property to build this URL:
5959

@@ -108,7 +108,7 @@ Then, create an API route that verifies the preview secret, authenticates the us
108108
`/app/preview/route.ts`
109109

110110
```ts
111-
import type { CollectionSlug, PayloadRequest } from 'payload'
111+
import type { PayloadRequest } from 'payload'
112112
import { getPayload } from 'payload'
113113

114114
import { draftMode } from 'next/headers'
@@ -130,8 +130,6 @@ export async function GET(
130130
const { searchParams } = new URL(req.url)
131131

132132
const path = searchParams.get('path')
133-
const collection = searchParams.get('collection') as CollectionSlug
134-
const slug = searchParams.get('slug')
135133
const previewSecret = searchParams.get('previewSecret')
136134

137135
if (previewSecret !== process.env.PREVIEW_SECRET) {
@@ -140,7 +138,7 @@ export async function GET(
140138
})
141139
}
142140

143-
if (!path || !collection || !slug) {
141+
if (!path) {
144142
return new Response('Insufficient search params', { status: 404 })
145143
}
146144

examples/draft-preview/src/app/(app)/preview/route.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,30 @@
1-
import type { CollectionSlug, PayloadRequest } from 'payload'
1+
import type { PayloadRequest } from 'payload'
22
import { getPayload } from 'payload'
33

44
import { draftMode } from 'next/headers'
55
import { redirect } from 'next/navigation'
6+
import { NextRequest } from 'next/server'
67

78
import configPromise from '@payload-config'
89

9-
export async function GET(
10-
req: {
11-
cookies: {
12-
get: (name: string) => {
13-
value: string
14-
}
15-
}
16-
} & Request,
17-
): Promise<Response> {
10+
export type PreviewSearchParams = {
11+
path: string
12+
previewSecret: string
13+
}
14+
15+
export async function GET(req: NextRequest): Promise<Response> {
1816
const payload = await getPayload({ config: configPromise })
1917

2018
const { searchParams } = new URL(req.url)
2119

2220
const path = searchParams.get('path')
23-
const collection = searchParams.get('collection') as CollectionSlug
24-
const slug = searchParams.get('slug')
2521
const previewSecret = searchParams.get('previewSecret')
2622

2723
if (previewSecret !== process.env.PREVIEW_SECRET) {
2824
return new Response('You are not allowed to preview this page', { status: 403 })
2925
}
3026

31-
if (!path || !collection || !slug) {
27+
if (!path) {
3228
return new Response('Insufficient search params', { status: 404 })
3329
}
3430

examples/draft-preview/src/collections/Pages/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { loggedIn } from './access/loggedIn'
55
import { publishedOrLoggedIn } from './access/publishedOrLoggedIn'
66
import { formatSlug } from './hooks/formatSlug'
77
import { revalidatePage } from './hooks/revalidatePage'
8+
import { PreviewSearchParams } from '@/app/(app)/preview/route'
89

910
export const Pages: CollectionConfig = {
1011
slug: 'pages',
@@ -18,11 +19,9 @@ export const Pages: CollectionConfig = {
1819
defaultColumns: ['title', 'slug', 'updatedAt'],
1920
preview: ({ slug, collection }: { slug: string; collection: CollectionSlug }) => {
2021
const encodedParams = new URLSearchParams({
21-
slug,
22-
collection,
2322
path: `/${slug}`,
2423
previewSecret: process.env.PREVIEW_SECRET || '',
25-
})
24+
} satisfies PreviewSearchParams)
2625

2726
return `${process.env.NEXT_PUBLIC_SERVER_URL}/preview?${encodedParams.toString()}`
2827
},

templates/ecommerce/src/app/(app)/next/preview/route.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
1-
import type { CollectionSlug, PayloadRequest } from 'payload'
1+
import type { PayloadRequest } from 'payload'
22
import { getPayload } from 'payload'
33

44
import { draftMode } from 'next/headers'
55
import { redirect } from 'next/navigation'
6+
import { NextRequest } from 'next/server'
67

78
import configPromise from '@payload-config'
89

9-
export async function GET(req: Request): Promise<Response> {
10+
export type PreviewSearchParams = {
11+
path: string
12+
previewSecret: string
13+
}
14+
15+
export async function GET(req: NextRequest): Promise<Response> {
1016
const payload = await getPayload({ config: configPromise })
1117

1218
const { searchParams } = new URL(req.url)
1319

1420
const path = searchParams.get('path')
15-
const collection = searchParams.get('collection') as CollectionSlug
16-
const slug = searchParams.get('slug')
1721
const previewSecret = searchParams.get('previewSecret')
1822

1923
if (previewSecret !== process.env.PREVIEW_SECRET) {
2024
return new Response('You are not allowed to preview this page', { status: 403 })
2125
}
2226

23-
if (!path || !collection || !slug) {
27+
if (!path) {
2428
return new Response('Insufficient search params', { status: 404 })
2529
}
2630

templates/ecommerce/src/utilities/generatePreviewPath.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { PreviewSearchParams } from '@/app/(frontend)/next/preview/route'
12
import { PayloadRequest, CollectionSlug } from 'payload'
23

34
const collectionPrefixMap: Partial<Record<CollectionSlug, string>> = {
4-
products: '/products',
5+
posts: '/posts',
56
pages: '',
67
}
78

@@ -12,17 +13,17 @@ type Props = {
1213
}
1314

1415
export const generatePreviewPath = ({ collection, slug }: Props) => {
15-
// Allow empty strings, e.g. for the homepage
1616
if (slug === undefined || slug === null) {
1717
return null
1818
}
1919

20+
// Encode to support slugs with special characters
21+
const encodedSlug = encodeURIComponent(slug)
22+
2023
const encodedParams = new URLSearchParams({
21-
slug,
22-
collection,
23-
path: `${collectionPrefixMap[collection]}/${slug}`,
24+
path: `${collectionPrefixMap[collection]}/${encodedSlug}`,
2425
previewSecret: process.env.PREVIEW_SECRET || '',
25-
})
26+
} satisfies PreviewSearchParams)
2627

2728
const url = `/next/preview?${encodedParams.toString()}`
2829

templates/website/src/app/(frontend)/next/preview/route.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CollectionSlug, PayloadRequest } from 'payload'
1+
import type { PayloadRequest } from 'payload'
22
import { getPayload } from 'payload'
33

44
import { draftMode } from 'next/headers'
@@ -7,21 +7,24 @@ import { NextRequest } from 'next/server'
77

88
import configPromise from '@payload-config'
99

10+
export type PreviewSearchParams = {
11+
path: string
12+
previewSecret: string
13+
}
14+
1015
export async function GET(req: NextRequest): Promise<Response> {
1116
const payload = await getPayload({ config: configPromise })
1217

1318
const { searchParams } = new URL(req.url)
1419

1520
const path = searchParams.get('path')
16-
const collection = searchParams.get('collection') as CollectionSlug
17-
const slug = searchParams.get('slug')
1821
const previewSecret = searchParams.get('previewSecret')
1922

2023
if (previewSecret !== process.env.PREVIEW_SECRET) {
2124
return new Response('You are not allowed to preview this page', { status: 403 })
2225
}
2326

24-
if (!path || !collection || !slug) {
27+
if (!path) {
2528
return new Response('Insufficient search params', { status: 404 })
2629
}
2730

templates/website/src/utilities/generatePreviewPath.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { PreviewSearchParams } from '@/app/(frontend)/next/preview/route'
12
import { PayloadRequest, CollectionSlug } from 'payload'
23

34
const collectionPrefixMap: Partial<Record<CollectionSlug, string>> = {
@@ -12,7 +13,6 @@ type Props = {
1213
}
1314

1415
export const generatePreviewPath = ({ collection, slug }: Props) => {
15-
// Allow empty strings, e.g. for the homepage
1616
if (slug === undefined || slug === null) {
1717
return null
1818
}
@@ -21,11 +21,9 @@ export const generatePreviewPath = ({ collection, slug }: Props) => {
2121
const encodedSlug = encodeURIComponent(slug)
2222

2323
const encodedParams = new URLSearchParams({
24-
slug: encodedSlug,
25-
collection,
2624
path: `${collectionPrefixMap[collection]}/${encodedSlug}`,
2725
previewSecret: process.env.PREVIEW_SECRET || '',
28-
})
26+
} satisfies PreviewSearchParams)
2927

3028
const url = `/next/preview?${encodedParams.toString()}`
3129

templates/with-vercel-website/src/app/(frontend)/next/preview/route.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CollectionSlug, PayloadRequest } from 'payload'
1+
import type { PayloadRequest } from 'payload'
22
import { getPayload } from 'payload'
33

44
import { draftMode } from 'next/headers'
@@ -7,21 +7,24 @@ import { NextRequest } from 'next/server'
77

88
import configPromise from '@payload-config'
99

10+
export type PreviewSearchParams = {
11+
path: string
12+
previewSecret: string
13+
}
14+
1015
export async function GET(req: NextRequest): Promise<Response> {
1116
const payload = await getPayload({ config: configPromise })
1217

1318
const { searchParams } = new URL(req.url)
1419

1520
const path = searchParams.get('path')
16-
const collection = searchParams.get('collection') as CollectionSlug
17-
const slug = searchParams.get('slug')
1821
const previewSecret = searchParams.get('previewSecret')
1922

2023
if (previewSecret !== process.env.PREVIEW_SECRET) {
2124
return new Response('You are not allowed to preview this page', { status: 403 })
2225
}
2326

24-
if (!path || !collection || !slug) {
27+
if (!path) {
2528
return new Response('Insufficient search params', { status: 404 })
2629
}
2730

templates/with-vercel-website/src/utilities/generatePreviewPath.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { PreviewSearchParams } from '@/app/(frontend)/next/preview/route'
12
import { PayloadRequest, CollectionSlug } from 'payload'
23

34
const collectionPrefixMap: Partial<Record<CollectionSlug, string>> = {
@@ -12,7 +13,6 @@ type Props = {
1213
}
1314

1415
export const generatePreviewPath = ({ collection, slug }: Props) => {
15-
// Allow empty strings, e.g. for the homepage
1616
if (slug === undefined || slug === null) {
1717
return null
1818
}
@@ -21,11 +21,9 @@ export const generatePreviewPath = ({ collection, slug }: Props) => {
2121
const encodedSlug = encodeURIComponent(slug)
2222

2323
const encodedParams = new URLSearchParams({
24-
slug: encodedSlug,
25-
collection,
2624
path: `${collectionPrefixMap[collection]}/${encodedSlug}`,
2725
previewSecret: process.env.PREVIEW_SECRET || '',
28-
})
26+
} satisfies PreviewSearchParams)
2927

3028
const url = `/next/preview?${encodedParams.toString()}`
3129

0 commit comments

Comments
 (0)