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:
Jacob Fletcher
2025-01-29 23:14:08 -05:00
committed by GitHub
parent 8f27f85023
commit 2b9ee62fc0
22 changed files with 1288 additions and 1343 deletions

View File

@@ -61,17 +61,17 @@ preview: (doc, { req }) => `${req.protocol}//${req.host}/${doc.slug}` // highlig
## Draft Preview
The Preview feature can be used to achieve "Draft Preview", where you enter into a "draft mode" after clicking the preview button. While in "draft mode", you can adjust your page's query to include the `draft: true` in its params. Payload will read this param on the request, and when present, send back a draft document as opposed to a published one based on the document's `_status` field.
The Preview feature can be used to achieve "Draft Preview". After clicking the preview button from the Admin Panel, you can enter into "draft mode" within your front-end application. This will allow you to adjust your page queries to include the `draft: true` param. When this param is present on the request, Payload will send back a draft document as opposed to a published one based on the document's `_status` field.
To enter draft mode, the URL provided to the `preview` function can point to a custom endpoint in your front-end application that sets a cookie or session variable to indicate that you are in "draft mode". This is framework specific, so the mechanisms here very from framework to framework although the underlying concept is the same.
To enter draft mode, the URL provided to the `preview` function can point to a custom endpoint in your front-end application that sets a cookie or session variable to indicate that draft mode is enabled. This is framework specific, so the mechanisms here very from framework to framework although the underlying concept is the same.
### Next.js
If you're using Next.js, you can do the following code to enter draft mode.
If you're using Next.js, you can do the following code to enter [Draft Mode](https://nextjs.org/docs/app/building-your-application/configuring/draft-mode).
#### Step 1: Create an API Route
#### Step 1: Format the Preview URL
First, format your `preview` function to point to a custom endpoint that you'll open on your front-end. This URL will include a few key query search params:
First, format your `admin.preview` function to point to a custom endpoint that you'll open on your front-end. This URL should include a few key query search params:
```ts
import type { CollectionConfig } from 'payload'
@@ -79,17 +79,15 @@ import type { CollectionConfig } from 'payload'
export const Pages: CollectionConfig = {
slug: 'pages',
admin: {
preview: ({ slug }, { req }) => {
const path = `/${slug}`
preview: ({ slug, collection }) => {
const encodedParams = new URLSearchParams({
slug,
collection,
path,
previewSecret: process.env.PREVIEW_SECRET
path: `/${slug}`,
previewSecret: process.env.PREVIEW_SECRET || ''
})
return `/next/preview?${encodedParams.toString()}` // highlight-line
return `/preview?${encodedParams.toString()}` // highlight-line
}
},
fields: [
@@ -105,18 +103,17 @@ export const Pages: CollectionConfig = {
Then, create an API route that verifies the preview secret, authenticates the user, and enters draft mode:
`/preview/route.ts`
`/app/preview/route.ts`
```ts
import type { CollectionSlug, PayloadRequest, getPayload } from 'payload'
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'
const payloadToken = 'payload-token'
export async function GET(
req: {
cookies: {
@@ -161,12 +158,13 @@ export async function GET(
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 })
}
// You can add additional checks here to see if the user is allowed to preview this page
draft.enable()
redirect(path)
@@ -177,7 +175,7 @@ export async function GET(
Finally, in your front-end application, you can detect draft mode and adjust your queries to include drafts:
`/pages/[slug].tsx`
`/app/[slug]/page.tsx`
```ts
export default async function Page({ params: paramsPromise }) {

View File

@@ -6,3 +6,6 @@ PAYLOAD_SECRET=YOUR_SECRET_HERE
# Used to configure CORS, format links and more. No trailing slash
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
# Used to validate the preview request
PREVIEW_SECRET=YOUR_SECRET_HERE

View File

@@ -1,6 +1,6 @@
# Payload Draft Preview Example
The [Payload Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview/payload) demonstrates how to implement draft preview in [Payload](https://github.com/payloadcms/payload) using [Versions](https://payloadcms.com/docs/versions/overview) and [Drafts](https://payloadcms.com/docs/versions/drafts). Draft preview allows you to see content on your front-end before it is published.
The [Payload Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview/payload) demonstrates how to implement [Draft Preview](https://payloadcms.com/docs/admin/preview#draft-preview) in [Payload](https://github.com/payloadcms/payload) using [Versions](https://payloadcms.com/docs/versions/overview) and [Drafts](https://payloadcms.com/docs/versions/drafts). With Draft Preview, you can navigate to your front-end application and enter "draft mode", where your queries are modified to fetch draft content instead of published content. This is useful for seeing how your content will look before being published.
## Quick Start

View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -3,6 +3,7 @@
"version": "1.0.0",
"description": "Payload preview example.",
"license": "MIT",
"type": "module",
"main": "dist/server.js",
"scripts": {
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",

File diff suppressed because it is too large Load Diff

View File

@@ -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,

View File

@@ -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')
}

View File

@@ -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)
}
}

View 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)
}

View File

@@ -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) {

View File

@@ -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',
},

View File

@@ -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'

View File

@@ -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 || []

View File

@@ -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

View File

@@ -1,4 +1,4 @@
import type { Page } from '../payload-types'
import type { Page } from '@payload-types'
export const examplePage: Partial<Page> = {
slug: 'example-page',

View File

@@ -1,4 +1,4 @@
import type { Page } from '../payload-types'
import type { Page } from '@payload-types'
export const examplePageDraft: Partial<Page> = {
richText: [

View File

@@ -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()}`
}

View File

@@ -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}`],
})

View File

@@ -30,6 +30,9 @@
"@payload-config": [
"./src/payload.config.ts"
],
"@payload-types": [
"./src/payload-types.ts"
],
"react": [
"./node_modules/@types/react"
],

View File

@@ -31,7 +31,7 @@
}
],
"paths": {
"@payload-config": ["./test/versions/config.ts"],
"@payload-config": ["./test/_community/config.ts"],
"@payloadcms/live-preview": ["./packages/live-preview/src"],
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],