fix(next): does not format top-level domains within admin.preview or livePreview.url functions (#9831)

Fixes #9830. Continuation of #9755 and #9746. Instead of automatically
appending TLDs to the `admin.preview` and the `livePreview.url` URLs, we
should instead ensure that `req` is passed through these functions, so
that you can have full control over the format of this URL without
Payload imposing any of its own formatting.
This commit is contained in:
Jacob Fletcher
2024-12-09 11:54:20 -05:00
committed by GitHub
parent 84abfdf84a
commit e095222a9c
16 changed files with 63 additions and 44 deletions

View File

@@ -108,14 +108,20 @@ export const Posts: CollectionConfig = {
} }
``` ```
The `preview` property resolves to a string that points to your front-end application with additional URL parameters. This can be an absolute URL or a relative path. If you are using a relative path, Payload will prepend the application's origin onto it, creating a fully qualified URL. The `preview` property resolves to a string that points to your front-end application with additional URL parameters. This can be an absolute URL or a relative path.
The preview function receives two arguments: The preview function receives two arguments:
| Argument | Description | | Argument | Description |
| --- | --- | | --- | --- |
| **`doc`** | The Document being edited. | | **`doc`** | The Document being edited. |
| **`ctx`** | An object containing `locale` and `token` properties. The `token` is the currently logged-in user's JWT. | | **`ctx`** | An object containing `locale`, `token`, and `req` properties. The `token` is the currently logged-in user's JWT. |
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:
```ts
preview: (doc, { req }) => `${req.protocol}//${req.host}/${doc.slug}` // highlight-line
```
<Banner type="success"> <Banner type="success">
<strong>Note:</strong> <strong>Note:</strong>

View File

@@ -54,8 +54,6 @@ _\* An asterisk denotes that a property is required._
The `url` property resolves to a string that points to your front-end application. This value is used as the `src` attribute of the iframe rendering your front-end. Once loaded, the Admin Panel will communicate directly with your app through `window.postMessage` events. The `url` property resolves to a string that points to your front-end application. This value is used as the `src` attribute of the iframe rendering your front-end. Once loaded, the Admin Panel will communicate directly with your app through `window.postMessage` events.
This can be an absolute URL or a relative path. If you are using a relative path, Payload will prepend the application's origin onto it, creating a fully qualified URL. This is useful for Vercel preview deployments, for example, where URLs are not known ahead of time.
To set the URL, use the `admin.livePreview.url` property in your [Payload Config](../configuration/overview): To set the URL, use the `admin.livePreview.url` property in your [Payload Config](../configuration/overview):
```ts ```ts
@@ -107,8 +105,16 @@ The following arguments are provided to the `url` function:
| Path | Description | | Path | Description |
| ------------------ | ----------------------------------------------------------------------------------------------------------------- | | ------------------ | ----------------------------------------------------------------------------------------------------------------- |
| **`data`** | The data of the Document being edited. This includes changes that have not yet been saved. | | **`data`** | The data of the Document being edited. This includes changes that have not yet been saved. |
| **`documentInfo`** | Information about the Document being edited like collection slug. [More details](../admin/hooks#usedocumentinfo). |
| **`locale`** | The locale currently being edited (if applicable). [More details](../configuration/localization). | | **`locale`** | The locale currently being edited (if applicable). [More details](../configuration/localization). |
| **`collectionConfig`** | The Collection Admin Config of the Document being edited. [More details](../admin/collections). |
| **`globalConfig`** | The Global Admin Config of the Document being edited. [More details](../admin/globals). |
| **`req`** | The Payload Request object. |
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:
```ts
url: (doc, { req }) => `${req.protocol}//${req.host}/${doc.slug}` // highlight-line
```
### Breakpoints ### Breakpoints

View File

@@ -11,7 +11,7 @@ export const preview: CollectionRouteHandlerWithID = async ({ id, collection, re
const { searchParams } = req const { searchParams } = req
const depth = searchParams.get('depth') const depth = searchParams.get('depth')
const result = await findByIDOperation({ const doc = await findByIDOperation({
id, id,
collection, collection,
depth: isNumber(depth) ? Number(depth) : undefined, depth: isNumber(depth) ? Number(depth) : undefined,
@@ -29,16 +29,11 @@ export const preview: CollectionRouteHandlerWithID = async ({ id, collection, re
if (typeof generatePreviewURL === 'function') { if (typeof generatePreviewURL === 'function') {
try { try {
previewURL = await generatePreviewURL(result, { previewURL = await generatePreviewURL(doc, {
locale: req.locale, locale: req.locale,
req, req,
token, token,
}) })
// Support relative URLs by prepending the origin, if necessary
if (previewURL && previewURL.startsWith('/')) {
previewURL = `${req.protocol}//${req.host}${previewURL}`
}
} catch (err) { } catch (err) {
return routeError({ return routeError({
collection, collection,

View File

@@ -11,7 +11,7 @@ export const preview: GlobalRouteHandler = async ({ globalConfig, req }) => {
const { searchParams } = req const { searchParams } = req
const depth = searchParams.get('depth') const depth = searchParams.get('depth')
const result = await findOneOperation({ const doc = await findOneOperation({
slug: globalConfig.slug, slug: globalConfig.slug,
depth: isNumber(depth) ? Number(depth) : undefined, depth: isNumber(depth) ? Number(depth) : undefined,
draft: searchParams.get('draft') === 'true', draft: searchParams.get('draft') === 'true',
@@ -29,7 +29,7 @@ export const preview: GlobalRouteHandler = async ({ globalConfig, req }) => {
if (typeof generatePreviewURL === 'function') { if (typeof generatePreviewURL === 'function') {
try { try {
previewURL = await generatePreviewURL(result, { previewURL = await generatePreviewURL(doc, {
locale: req.locale, locale: req.locale,
req, req,
token, token,

View File

@@ -36,21 +36,21 @@ export const LivePreviewView: PayloadServerReactComponent<EditViewComponent> = a
}, },
] ]
let url = const url =
typeof livePreviewConfig?.url === 'function' typeof livePreviewConfig?.url === 'function'
? await livePreviewConfig.url({ ? await livePreviewConfig.url({
collectionConfig, collectionConfig,
data: doc, data: doc,
globalConfig, globalConfig,
locale, locale,
req,
/**
* @deprecated
* Use `req.payload` instead. This will be removed in the next major version.
*/
payload: initPageResult.req.payload, payload: initPageResult.req.payload,
}) })
: livePreviewConfig?.url : livePreviewConfig?.url
// Support relative URLs by prepending the origin, if necessary
if (url && url.startsWith('/')) {
url = `${initPageResult.req.protocol}//${initPageResult.req.host}${url}`
}
return <LivePreviewClient breakpoints={breakpoints} initialData={doc} url={url} /> return <LivePreviewClient breakpoints={breakpoints} initialData={doc} url={url} />
} }

View File

@@ -148,7 +148,12 @@ export type LivePreviewConfig = {
data: Record<string, any> data: Record<string, any>
globalConfig?: SanitizedGlobalConfig globalConfig?: SanitizedGlobalConfig
locale: Locale locale: Locale
/**
* @deprecated
* Use `req.payload` instead. This will be removed in the next major version.
*/
payload: Payload payload: Payload
req: PayloadRequest
}) => Promise<string> | string) }) => Promise<string> | string)
| string | string
} }

View File

@@ -48,14 +48,12 @@ export const Pages: CollectionConfig<'pages'> = {
return path return path
}, },
}, },
preview: (data) => { preview: (data, { req }) =>
const path = generatePreviewPath({ generatePreviewPath({
slug: typeof data?.slug === 'string' ? data.slug : '', slug: typeof data?.slug === 'string' ? data.slug : '',
collection: 'pages', collection: 'pages',
}) req,
}),
return path
},
useAsTitle: 'title', useAsTitle: 'title',
}, },
fields: [ fields: [

View File

@@ -54,19 +54,18 @@ export const Posts: CollectionConfig<'posts'> = {
const path = generatePreviewPath({ const path = generatePreviewPath({
slug: typeof data?.slug === 'string' ? data.slug : '', slug: typeof data?.slug === 'string' ? data.slug : '',
collection: 'posts', collection: 'posts',
// req, TODO: thread `req` once 3.5.1 is out, see notes in `generatePreviewPath`
}) })
return path return path
}, },
}, },
preview: (data) => { preview: (data, { req }) =>
const path = generatePreviewPath({ generatePreviewPath({
slug: typeof data?.slug === 'string' ? data.slug : '', slug: typeof data?.slug === 'string' ? data.slug : '',
collection: 'posts', collection: 'posts',
}) req,
}),
return path
},
useAsTitle: 'title', useAsTitle: 'title',
}, },
fields: [ fields: [

View File

@@ -1,4 +1,4 @@
import { CollectionSlug } from 'payload' import { PayloadRequest, CollectionSlug } from 'payload'
const collectionPrefixMap: Partial<Record<CollectionSlug, string>> = { const collectionPrefixMap: Partial<Record<CollectionSlug, string>> = {
posts: '/posts', posts: '/posts',
@@ -8,9 +8,10 @@ const collectionPrefixMap: Partial<Record<CollectionSlug, string>> = {
type Props = { type Props = {
collection: keyof typeof collectionPrefixMap collection: keyof typeof collectionPrefixMap
slug: string slug: string
req?: PayloadRequest // TODO: make this required once 3.5.1 is out, it's a new argument in that version
} }
export const generatePreviewPath = ({ collection, slug }: Props) => { export const generatePreviewPath = ({ collection, slug, req }: Props) => {
const path = `${collectionPrefixMap[collection]}/${slug}` const path = `${collectionPrefixMap[collection]}/${slug}`
const params = { const params = {
@@ -25,5 +26,12 @@ export const generatePreviewPath = ({ collection, slug }: Props) => {
encodedParams.append(key, value) encodedParams.append(key, value)
}) })
return `/next/preview?${encodedParams.toString()}` let url = `/next/preview?${encodedParams.toString()}`
// TODO: remove this check once 3.5.1 is out, see note above
if (req) {
url = `${req.protocol}//${req.host}${url}`
}
return url
} }

View File

@@ -5,7 +5,7 @@ import React from 'react'
import type { Page as PageType } from '../../../../payload-types.js' import type { Page as PageType } from '../../../../payload-types.js'
import { renderedPageTitleID } from '../../../../shared.js' import { localizedPageTitleID, renderedPageTitleID } from '../../../../shared.js'
import { PAYLOAD_SERVER_URL } from '../../_api/serverURL.js' import { PAYLOAD_SERVER_URL } from '../../_api/serverURL.js'
import { Blocks } from '../../_components/Blocks/index.js' import { Blocks } from '../../_components/Blocks/index.js'
import { Gutter } from '../../_components/Gutter/index.js' import { Gutter } from '../../_components/Gutter/index.js'
@@ -38,7 +38,7 @@ export const PageClient: React.FC<{
/> />
<Gutter> <Gutter>
<div id={renderedPageTitleID}>{`For Testing: ${data.title}`}</div> <div id={renderedPageTitleID}>{`For Testing: ${data.title}`}</div>
<div id={renderedPageTitleID}> <div id={localizedPageTitleID}>
{`For Testing (Localized): ${typeof data.relationToLocalized === 'string' ? data.relationToLocalized : data.relationToLocalized?.localizedTitle}`} {`For Testing (Localized): ${typeof data.relationToLocalized === 'string' ? data.relationToLocalized : data.relationToLocalized?.localizedTitle}`}
</div> </div>
</Gutter> </Gutter>

View File

@@ -46,7 +46,7 @@ export async function generateStaticParams() {
try { try {
const ssrPages = await getDocs<Page>(ssrAutosavePagesSlug) const ssrPages = await getDocs<Page>(ssrAutosavePagesSlug)
return ssrPages?.map(({ slug }) => slug) return ssrPages?.map(({ slug }) => slug)
} catch (error) { } catch (_err) {
return [] return []
} }
} }

View File

@@ -46,7 +46,7 @@ export async function generateStaticParams() {
try { try {
const ssrPages = await getDocs<Page>(ssrPagesSlug) const ssrPages = await getDocs<Page>(ssrPagesSlug)
return ssrPages?.map(({ slug }) => slug) return ssrPages?.map(({ slug }) => slug)
} catch (error) { } catch (_err) {
return [] return []
} }
} }

View File

@@ -46,7 +46,7 @@ export async function generateStaticParams() {
try { try {
const ssrPages = await getDocs<Page>(ssrAutosavePagesSlug) const ssrPages = await getDocs<Page>(ssrAutosavePagesSlug)
return ssrPages?.map(({ slug }) => slug) return ssrPages?.map(({ slug }) => slug)
} catch (error) { } catch (_err) {
return [] return []
} }
} }

View File

@@ -46,7 +46,7 @@ export async function generateStaticParams() {
try { try {
const ssrPages = await getDocs<Page>(ssrPagesSlug) const ssrPages = await getDocs<Page>(ssrPagesSlug)
return ssrPages?.map(({ slug }) => slug) return ssrPages?.map(({ slug }) => slug)
} catch (error) { } catch (_err) {
return [] return []
} }
} }

View File

@@ -22,3 +22,5 @@ export const desktopBreakpoint = {
} }
export const renderedPageTitleID = 'rendered-page-title' export const renderedPageTitleID = 'rendered-page-title'
export const localizedPageTitleID = 'localized-page-title'

View File

@@ -3,15 +3,15 @@ import type { LivePreviewConfig } from 'payload'
export const formatLivePreviewURL: LivePreviewConfig['url'] = async ({ export const formatLivePreviewURL: LivePreviewConfig['url'] = async ({
data, data,
collectionConfig, collectionConfig,
payload, req,
}) => { }) => {
let baseURL = '/live-preview' let baseURL = `${req.protocol}//${req.host}/live-preview`
// You can run async requests here, if needed // You can run async requests here, if needed
// For example, multi-tenant apps may need to lookup additional data // For example, multi-tenant apps may need to lookup additional data
if (data.tenant) { if (data.tenant) {
try { try {
const fullTenant = await payload const fullTenant = await req.payload
.find({ .find({
collection: 'tenants', collection: 'tenants',
where: { where: {