diff --git a/docs/live-preview/overview.mdx b/docs/live-preview/overview.mdx index 5f7c7fe02..6a940f8c8 100644 --- a/docs/live-preview/overview.mdx +++ b/docs/live-preview/overview.mdx @@ -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 diff --git a/packages/next/src/views/LivePreview/index.client.tsx b/packages/next/src/views/LivePreview/index.client.tsx index 33c3c9997..d1c28a177 100644 --- a/packages/next/src/views/LivePreview/index.client.tsx +++ b/packages/next/src/views/LivePreview/index.client.tsx @@ -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 = ({ 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, diff --git a/templates/website/src/utilities/generatePreviewPath.ts b/templates/website/src/utilities/generatePreviewPath.ts index 9f71be07e..72093dd97 100644 --- a/templates/website/src/utilities/generatePreviewPath.ts +++ b/templates/website/src/utilities/generatePreviewPath.ts @@ -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 } diff --git a/templates/with-vercel-website/src/utilities/generatePreviewPath.ts b/templates/with-vercel-website/src/utilities/generatePreviewPath.ts index 9f71be07e..72093dd97 100644 --- a/templates/with-vercel-website/src/utilities/generatePreviewPath.ts +++ b/templates/with-vercel-website/src/utilities/generatePreviewPath.ts @@ -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 } diff --git a/test/live-preview/next-env.d.ts b/test/live-preview/next-env.d.ts index 40c3d6809..1b3be0840 100644 --- a/test/live-preview/next-env.d.ts +++ b/test/live-preview/next-env.d.ts @@ -2,4 +2,4 @@ /// // 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. diff --git a/test/live-preview/payload-types.ts b/test/live-preview/payload-types.ts index 6a9157119..1792c3981 100644 --- a/test/live-preview/payload-types.ts +++ b/test/live-preview/payload-types.ts @@ -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; diff --git a/test/live-preview/utilities/formatLivePreviewURL.ts b/test/live-preview/utilities/formatLivePreviewURL.ts index ede3d95f6..089e40ac3 100644 --- a/test/live-preview/utilities/formatLivePreviewURL.ts +++ b/test/live-preview/utilities/formatLivePreviewURL.ts @@ -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