From 91a0f90649cc2ec30fe87f6ef9fcd394fce624c5 Mon Sep 17 00:00:00 2001 From: James Mikrut Date: Mon, 10 Feb 2025 13:20:34 -0500 Subject: [PATCH] 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 --- docs/live-preview/overview.mdx | 4 +- .../src/views/LivePreview/index.client.tsx | 15 +++++- .../src/utilities/generatePreviewPath.ts | 9 +--- .../src/utilities/generatePreviewPath.ts | 9 +--- test/live-preview/next-env.d.ts | 2 +- test/live-preview/payload-types.ts | 54 +++++++++++++++++++ .../utilities/formatLivePreviewURL.ts | 2 +- 7 files changed, 77 insertions(+), 18 deletions(-) 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