fix(next): allows relative live preview urls (#11083)

We now properly allow relative live preview URLs which is handy if
you're deploying on a platform like Vercel and do not know what the
preview domain is going to end up being at build time.

This PR also removes some problematic code in the website template which
hard-codes the protocol to `https://` in production even if you're
running locally.

Fixes #11070
This commit is contained in:
James Mikrut
2025-02-10 13:20:34 -05:00
committed by GitHub
parent b15a7e3c72
commit 91a0f90649
7 changed files with 77 additions and 18 deletions

View File

@@ -109,7 +109,9 @@ The following arguments are provided to the `url` function:
| **`globalConfig`** | The Global Admin Config of the Document being edited. [More details](../configuration/globals#admin-options). |
| **`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:
You can return either an absolute URL or relative URL from this function. If you don't know the URL of your frontend at build-time, you can return a relative URL, and in that case, Payload will automatically construct an absolute URL by injecting the protocol, domain, and port from your browser window. Returning a relative URL is helpful for platforms like Vercel where you may have preview deployment URLs that are unknown at build time.
If your application requires a fully qualified URL, or you are attempting to preview with a frontend on a different domain, you can use the `req` property to build this URL:
```ts
url: ({ data, req }) => `${req.protocol}//${req.host}/${data.slug}` // highlight-line

View File

@@ -61,6 +61,14 @@ type Props = {
readonly serverURL: string
} & DocumentSlots
const getAbsoluteUrl = (url) => {
try {
return new URL(url, window.location.origin).href
} catch {
return url
}
}
const PreviewView: React.FC<Props> = ({
collectionConfig,
config,
@@ -552,7 +560,7 @@ export const LivePreviewClient: React.FC<
readonly url: string
} & DocumentSlots
> = (props) => {
const { breakpoints, url } = props
const { breakpoints, url: incomingUrl } = props
const { collectionSlug, globalSlug } = useDocumentInfo()
const {
@@ -564,6 +572,11 @@ export const LivePreviewClient: React.FC<
getEntityConfig,
} = useConfig()
const url =
incomingUrl.startsWith('http://') || incomingUrl.startsWith('https://')
? incomingUrl
: getAbsoluteUrl(incomingUrl)
const { isPopupOpen, openPopupWindow, popupRef } = usePopupWindow({
eventType: 'payload-live-preview',
url,

View File

@@ -11,7 +11,7 @@ type Props = {
req: PayloadRequest
}
export const generatePreviewPath = ({ collection, slug, req }: Props) => {
export const generatePreviewPath = ({ collection, slug }: Props) => {
const encodedParams = new URLSearchParams({
slug,
collection,
@@ -19,12 +19,7 @@ export const generatePreviewPath = ({ collection, slug, req }: Props) => {
previewSecret: process.env.PREVIEW_SECRET || '',
})
const isProduction =
process.env.NODE_ENV === 'production' || Boolean(process.env.VERCEL_PROJECT_PRODUCTION_URL)
const protocol = isProduction ? 'https:' : req.protocol
const url = `${protocol}//${req.host}/next/preview?${encodedParams.toString()}`
const url = `/next/preview?${encodedParams.toString()}`
return url
}

View File

@@ -11,7 +11,7 @@ type Props = {
req: PayloadRequest
}
export const generatePreviewPath = ({ collection, slug, req }: Props) => {
export const generatePreviewPath = ({ collection, slug }: Props) => {
const encodedParams = new URLSearchParams({
slug,
collection,
@@ -19,12 +19,7 @@ export const generatePreviewPath = ({ collection, slug, req }: Props) => {
previewSecret: process.env.PREVIEW_SECRET || '',
})
const isProduction =
process.env.NODE_ENV === 'production' || Boolean(process.env.VERCEL_PROJECT_PRODUCTION_URL)
const protocol = isProduction ? 'https:' : req.protocol
const url = `${protocol}//${req.host}/next/preview?${encodedParams.toString()}`
const url = `/next/preview?${encodedParams.toString()}`
return url
}

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

@@ -135,6 +135,9 @@ export interface Page {
} | null);
url?: string | null;
label: string;
/**
* Choose how the link should be rendered.
*/
appearance?: ('primary' | 'secondary') | null;
};
id?: string | null;
@@ -169,6 +172,9 @@ export interface Page {
} | null);
url?: string | null;
label: string;
/**
* Choose how the link should be rendered.
*/
appearance?: ('default' | 'primary' | 'secondary') | null;
};
id?: string | null;
@@ -202,12 +208,18 @@ export interface Page {
value: string | Post;
}[]
| null;
/**
* This field is auto-populated after-read
*/
populatedDocs?:
| {
relationTo: 'posts';
value: string | Post;
}[]
| null;
/**
* This field is auto-populated after-read
*/
populatedDocsTotal?: number | null;
id?: string | null;
blockName?: string | null;
@@ -367,6 +379,9 @@ export interface Post {
} | null);
url?: string | null;
label: string;
/**
* Choose how the link should be rendered.
*/
appearance?: ('primary' | 'secondary') | null;
};
id?: string | null;
@@ -401,6 +416,9 @@ export interface Post {
} | null);
url?: string | null;
label: string;
/**
* Choose how the link should be rendered.
*/
appearance?: ('default' | 'primary' | 'secondary') | null;
};
id?: string | null;
@@ -434,12 +452,18 @@ export interface Post {
value: string | Post;
}[]
| null;
/**
* This field is auto-populated after-read
*/
populatedDocs?:
| {
relationTo: 'posts';
value: string | Post;
}[]
| null;
/**
* This field is auto-populated after-read
*/
populatedDocsTotal?: number | null;
id?: string | null;
blockName?: string | null;
@@ -510,6 +534,9 @@ export interface Ssr {
} | null);
url?: string | null;
label: string;
/**
* Choose how the link should be rendered.
*/
appearance?: ('primary' | 'secondary') | null;
};
id?: string | null;
@@ -544,6 +571,9 @@ export interface Ssr {
} | null);
url?: string | null;
label: string;
/**
* Choose how the link should be rendered.
*/
appearance?: ('default' | 'primary' | 'secondary') | null;
};
id?: string | null;
@@ -577,12 +607,18 @@ export interface Ssr {
value: string | Post;
}[]
| null;
/**
* This field is auto-populated after-read
*/
populatedDocs?:
| {
relationTo: 'posts';
value: string | Post;
}[]
| null;
/**
* This field is auto-populated after-read
*/
populatedDocsTotal?: number | null;
id?: string | null;
blockName?: string | null;
@@ -641,6 +677,9 @@ export interface SsrAutosave {
} | null);
url?: string | null;
label: string;
/**
* Choose how the link should be rendered.
*/
appearance?: ('primary' | 'secondary') | null;
};
id?: string | null;
@@ -675,6 +714,9 @@ export interface SsrAutosave {
} | null);
url?: string | null;
label: string;
/**
* Choose how the link should be rendered.
*/
appearance?: ('default' | 'primary' | 'secondary') | null;
};
id?: string | null;
@@ -708,12 +750,18 @@ export interface SsrAutosave {
value: string | Post;
}[]
| null;
/**
* This field is auto-populated after-read
*/
populatedDocs?:
| {
relationTo: 'posts';
value: string | Post;
}[]
| null;
/**
* This field is auto-populated after-read
*/
populatedDocsTotal?: number | null;
id?: string | null;
blockName?: string | null;
@@ -1345,6 +1393,9 @@ export interface Header {
} | null);
url?: string | null;
label: string;
/**
* Choose how the link should be rendered.
*/
appearance?: ('default' | 'primary' | 'secondary') | null;
};
id?: string | null;
@@ -1375,6 +1426,9 @@ export interface Footer {
} | null);
url?: string | null;
label: string;
/**
* Choose how the link should be rendered.
*/
appearance?: ('default' | 'primary' | 'secondary') | null;
};
id?: string | null;

View File

@@ -5,7 +5,7 @@ export const formatLivePreviewURL: LivePreviewConfig['url'] = async ({
collectionConfig,
req,
}) => {
let baseURL = `${req.protocol}//${req.host}/live-preview`
let baseURL = `/live-preview`
// You can run async requests here, if needed
// For example, multi-tenant apps may need to lookup additional data