chore(examples): misc improvements to the draft preview example (#10876)
There were a number of things wrong or could have been improved with the [Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview), namely: - The package.json was missing `"type": "modue"` which would throw ESM related import errors on startup - The preview secret was missing entirely, with pointless logic was written to throw an error if it missing in the search params as opposed to not matching the environment secret - The `/next/exit-preview` route was duplicated twice - The preview endpoint was unnecessarily querying the database for a matching document as opposed to letting the underlying page itself 404 as needed, and it was also throwing an inaccurate error message Some less critical changes were: - The page query was missing the `depth` and `limit` parameters which is best practice to optimize performance - The logic to format search params in the preview URL was unnecessarily complex - Utilities like `generatePreviewPath` and `getGlobals` were unnecessarily obfuscating simple functions - The `/preview` and `/exit-preview` routes were unecessarily nested within a `/next` page segment - Payload types weren't aliased
This commit is contained in:
@@ -3,7 +3,7 @@ import { notFound } from 'next/navigation'
|
||||
import { getPayload } from 'payload'
|
||||
import React, { cache, Fragment } from 'react'
|
||||
|
||||
import type { Page as PageType } from '../../../payload-types'
|
||||
import type { Page as PageType } from '@payload-types'
|
||||
|
||||
import { Gutter } from '../../../components/Gutter'
|
||||
import RichText from '../../../components/RichText'
|
||||
@@ -13,6 +13,7 @@ import classes from './index.module.scss'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
const pages = await payload.find({
|
||||
collection: 'pages',
|
||||
draft: false,
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { draftMode } from 'next/headers'
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
const draft = await draftMode()
|
||||
draft.disable()
|
||||
return new Response('Draft mode is disabled')
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import type { CollectionSlug, PayloadRequest } from 'payload'
|
||||
|
||||
import { draftMode } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
import configPromise from '../../../../payload.config'
|
||||
|
||||
const payloadToken = 'payload-token'
|
||||
|
||||
export async function GET(
|
||||
req: {
|
||||
cookies: {
|
||||
get: (name: string) => {
|
||||
value: string
|
||||
}
|
||||
}
|
||||
} & Request,
|
||||
): Promise<Response> {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
const token = req.cookies.get(payloadToken)?.value
|
||||
const { searchParams } = new URL(req.url)
|
||||
const path = searchParams.get('path')
|
||||
const collection = searchParams.get('collection') as CollectionSlug
|
||||
const slug = searchParams.get('slug')
|
||||
|
||||
const previewSecret = searchParams.get('previewSecret')
|
||||
|
||||
if (previewSecret) {
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
} else {
|
||||
if (!path) {
|
||||
return new Response('No path provided', { status: 404 })
|
||||
}
|
||||
|
||||
if (!collection) {
|
||||
return new Response('No path provided', { status: 404 })
|
||||
}
|
||||
|
||||
if (!slug) {
|
||||
return new Response('No path provided', { status: 404 })
|
||||
}
|
||||
|
||||
if (!path.startsWith('/')) {
|
||||
return new Response('This endpoint can only be used for internal previews', { status: 500 })
|
||||
}
|
||||
|
||||
let user
|
||||
|
||||
try {
|
||||
user = await payload.auth({
|
||||
req: req as unknown as PayloadRequest,
|
||||
headers: req.headers,
|
||||
})
|
||||
} catch (error) {
|
||||
payload.logger.error({ err: error }, 'Error verifying token for live preview')
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
const draft = await draftMode()
|
||||
|
||||
// You can add additional checks here to see if the user is allowed to preview this page
|
||||
if (!user) {
|
||||
draft.disable()
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
// Verify the given slug exists
|
||||
try {
|
||||
const docs = await payload.find({
|
||||
collection,
|
||||
draft: true,
|
||||
where: {
|
||||
slug: {
|
||||
equals: slug,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!docs.docs.length) {
|
||||
return new Response('Document not found', { status: 404 })
|
||||
}
|
||||
} catch (error) {
|
||||
payload.logger.error({
|
||||
err: error,
|
||||
msg: 'Error verifying token for live preview:',
|
||||
})
|
||||
}
|
||||
|
||||
draft.enable()
|
||||
|
||||
redirect(path)
|
||||
}
|
||||
}
|
||||
63
examples/draft-preview/src/app/(app)/preview/route.ts
Normal file
63
examples/draft-preview/src/app/(app)/preview/route.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { CollectionSlug, PayloadRequest } from 'payload'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
import { draftMode } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
import configPromise from '@payload-config'
|
||||
|
||||
export async function GET(
|
||||
req: {
|
||||
cookies: {
|
||||
get: (name: string) => {
|
||||
value: string
|
||||
}
|
||||
}
|
||||
} & Request,
|
||||
): Promise<Response> {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const { searchParams } = new URL(req.url)
|
||||
|
||||
const path = searchParams.get('path')
|
||||
const collection = searchParams.get('collection') as CollectionSlug
|
||||
const slug = searchParams.get('slug')
|
||||
const previewSecret = searchParams.get('previewSecret')
|
||||
|
||||
if (previewSecret !== process.env.PREVIEW_SECRET) {
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
if (!path || !collection || !slug) {
|
||||
return new Response('Insufficient search params', { status: 404 })
|
||||
}
|
||||
|
||||
if (!path.startsWith('/')) {
|
||||
return new Response('This endpoint can only be used for relative previews', { status: 500 })
|
||||
}
|
||||
|
||||
let user
|
||||
|
||||
try {
|
||||
user = await payload.auth({
|
||||
req: req as unknown as PayloadRequest,
|
||||
headers: req.headers,
|
||||
})
|
||||
} catch (error) {
|
||||
payload.logger.error({ err: error }, 'Error verifying token for live preview')
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
const draft = await draftMode()
|
||||
|
||||
if (!user) {
|
||||
draft.disable()
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
// You can add additional checks here to see if the user is allowed to preview this page
|
||||
|
||||
draft.enable()
|
||||
|
||||
redirect(path)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import type { CollectionAfterChangeHook } from 'payload'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
import type { Page } from '../../../payload-types'
|
||||
import type { Page } from '@payload-types'
|
||||
|
||||
export const revalidatePage: CollectionAfterChangeHook<Page> = ({ doc, previousDoc, req }) => {
|
||||
if (req.context.skipRevalidate) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import type { CollectionConfig, CollectionSlug } from 'payload'
|
||||
|
||||
import richText from '../../fields/richText'
|
||||
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
|
||||
import { loggedIn } from './access/loggedIn'
|
||||
import { publishedOrLoggedIn } from './access/publishedOrLoggedIn'
|
||||
import { formatSlug } from './hooks/formatSlug'
|
||||
@@ -17,12 +16,15 @@ export const Pages: CollectionConfig = {
|
||||
},
|
||||
admin: {
|
||||
defaultColumns: ['title', 'slug', 'updatedAt'],
|
||||
preview: (doc) => {
|
||||
const path = generatePreviewPath({
|
||||
slug: typeof doc?.slug === 'string' ? doc.slug : '',
|
||||
collection: 'pages',
|
||||
preview: ({ slug, collection }: { slug: string; collection: CollectionSlug }) => {
|
||||
const encodedParams = new URLSearchParams({
|
||||
slug,
|
||||
collection,
|
||||
path: `/${slug}`,
|
||||
previewSecret: process.env.PREVIEW_SECRET || '',
|
||||
})
|
||||
return `${process.env.NEXT_PUBLIC_SERVER_URL}${path}`
|
||||
|
||||
return `${process.env.NEXT_PUBLIC_SERVER_URL}/preview?${encodedParams.toString()}`
|
||||
},
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import type { Page } from '../../payload-types'
|
||||
import type { Page } from '@payload-types'
|
||||
|
||||
import { Button } from '../Button'
|
||||
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import { getPayload } from 'payload'
|
||||
import configPromise from '@payload-config'
|
||||
|
||||
import type { MainMenu } from '../../payload-types'
|
||||
import type { MainMenu } from '@payload-types'
|
||||
|
||||
import { getCachedGlobal } from '../../utilities/getGlobals'
|
||||
import { CMSLink } from '../CMSLink'
|
||||
import { Gutter } from '../Gutter'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export async function Header() {
|
||||
const header: MainMenu = await getCachedGlobal('main-menu', 1)()
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const header: MainMenu = await payload.findGlobal({
|
||||
slug: 'main-menu',
|
||||
depth: 1,
|
||||
})
|
||||
|
||||
const navItems = header?.navItems || []
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Page } from '../payload-types'
|
||||
import type { Page } from '@payload-types'
|
||||
|
||||
// Used for pre-seeded content so that the homepage is not empty
|
||||
// @ts-expect-error: Page type is not fully compatible with the provided object structure
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Page } from '../payload-types'
|
||||
import type { Page } from '@payload-types'
|
||||
|
||||
export const examplePage: Partial<Page> = {
|
||||
slug: 'example-page',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Page } from '../payload-types'
|
||||
import type { Page } from '@payload-types'
|
||||
|
||||
export const examplePageDraft: Partial<Page> = {
|
||||
richText: [
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { CollectionSlug } from 'payload'
|
||||
|
||||
const collectionPrefixMap: Partial<Record<CollectionSlug, string>> = {
|
||||
pages: '',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
collection: keyof typeof collectionPrefixMap
|
||||
slug: string
|
||||
}
|
||||
|
||||
export const generatePreviewPath = ({ slug, collection }: Props) => {
|
||||
const path = `${collectionPrefixMap[collection]}/${slug}`
|
||||
|
||||
const params = {
|
||||
slug,
|
||||
collection,
|
||||
path,
|
||||
}
|
||||
|
||||
const encodedParams = new URLSearchParams()
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
encodedParams.append(key, value)
|
||||
})
|
||||
|
||||
return `/next/preview?${encodedParams.toString()}`
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { Config } from 'src/payload-types'
|
||||
|
||||
import { unstable_cache } from 'next/cache'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
import configPromise from '../payload.config'
|
||||
|
||||
type Global = keyof Config['globals']
|
||||
|
||||
async function getGlobal(slug: Global, depth = 0) {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const global = await payload.findGlobal({
|
||||
slug,
|
||||
depth,
|
||||
})
|
||||
|
||||
return global
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a unstable_cache function mapped with the cache tag for the slug
|
||||
*/
|
||||
export const getCachedGlobal = (slug: Global, depth = 0) =>
|
||||
unstable_cache(async () => getGlobal(slug, depth), [slug], {
|
||||
tags: [`global_${slug}`],
|
||||
})
|
||||
Reference in New Issue
Block a user