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:
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user