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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
2
examples/draft-preview/next-env.d.ts
vendored
2
examples/draft-preview/next-env.d.ts
vendored
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
2326
examples/draft-preview/pnpm-lock.yaml
generated
2326
examples/draft-preview/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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}`],
|
||||
})
|
||||
@@ -30,6 +30,9 @@
|
||||
"@payload-config": [
|
||||
"./src/payload.config.ts"
|
||||
],
|
||||
"@payload-types": [
|
||||
"./src/payload-types.ts"
|
||||
],
|
||||
"react": [
|
||||
"./node_modules/@types/react"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user