Skip to content

Commit

Permalink
feat(live-preview): supports relative urls for dynamic preview deploy…
Browse files Browse the repository at this point in the history
…ments (#9746)

When deploying to Vercel, preview deployment URLs are dynamically
generated. This breaks Live Preview within those deployments because
there is no mechanism by which we can detect and set that URL within
Payload. Although Vercel provides various environment variables at our
disposal, they provide no concrete identifier for exactly _which_ URL is
being currently previewed (you an access the same deployment from a
number of different URLs).

The fix is to support _relative_ live preview URLs, that way Payload can
prepend the application's top-level domain dynamically at render-time in
order to create a fully qualified URL. So when you visit a Vercel
preview deployment, for example, that deployment's unique URL is used to
load the iframe of the preview window, instead of the application's
root/production domain. Note: this does not fix multi-tenancy
single-domain setups, as those still require a static top-level domain
for each tenant.
  • Loading branch information
jacobsfletch authored Dec 4, 2024
1 parent 8e26824 commit f12b4dc
Show file tree
Hide file tree
Showing 14 changed files with 57 additions and 37 deletions.
2 changes: 2 additions & 0 deletions docs/live-preview/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ _\* An asterisk denotes that a property is required._

The `url` property is 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 resolve it relative to the application's origin 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):

```ts
Expand Down
7 changes: 6 additions & 1 deletion packages/next/src/views/LivePreview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const LivePreviewView: PayloadServerReactComponent<EditViewComponent> = a
},
]

const url =
let url =
typeof livePreviewConfig?.url === 'function'
? await livePreviewConfig.url({
collectionConfig,
Expand All @@ -47,5 +47,10 @@ export const LivePreviewView: PayloadServerReactComponent<EditViewComponent> = a
})
: 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} />
}
4 changes: 3 additions & 1 deletion test/live-preview/app/live-preview/(pages)/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-restricted-exports */
import { notFound } from 'next/navigation.js'
import React from 'react'

Expand Down Expand Up @@ -32,10 +33,11 @@ export default async function Page({ params: paramsPromise }: Args) {

export async function generateStaticParams() {
process.env.PAYLOAD_DROP_DATABASE = 'false'

try {
const pages = await getDocs<Page>('pages')
return pages?.map(({ slug }) => slug)
} catch (error) {
} catch (_err) {
return []
}
}
4 changes: 2 additions & 2 deletions test/live-preview/app/live-preview/_api/getDoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ export const getDoc = async <T>(args: {
return docs[0] as T
}
} catch (err) {
console.log('Error getting doc', err)
throw new Error(`Error getting doc: ${err.message}`)
}

throw new Error('Error getting doc')
throw new Error('No doc found')
}
12 changes: 7 additions & 5 deletions test/live-preview/app/live-preview/_api/getDocs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import config from '@payload-config'
import { getPayload } from 'payload'
import { type CollectionSlug, getPayload } from 'payload'

export const getDocs = async <T>(collection: string): Promise<T[]> => {
export const getDocs = async <T>(collection: CollectionSlug): Promise<T[]> => {
const payload = await getPayload({ config })

try {
Expand All @@ -11,10 +11,12 @@ export const getDocs = async <T>(collection: string): Promise<T[]> => {
limit: 100,
})

return docs as T[]
if (docs) {
return docs as T[]
}
} catch (err) {
console.error(err)
throw new Error(`Error getting docs: ${err.message}`)
}

throw new Error('Error getting docs')
throw new Error('No docs found')
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export async function Footer() {
<img
alt="Payload Logo"
className={classes.logo}
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/payload-logo-light.svg"
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-logo-light.svg"
/>
</picture>
</Link>
Expand All @@ -30,7 +30,7 @@ export async function Footer() {
return <CMSLink key={i} {...link} />
})}
<Link href="/admin">Admin</Link>
<Link href="https://github.com/payloadcms/payload/tree/main/templates/ecommerce">
<Link href="https://github.com/payloadcms/payload/tree/main/test/live-preview">
Source Code
</Link>
<Link href="https://github.com/payloadcms/payload">Payload</Link>
Expand Down
16 changes: 9 additions & 7 deletions test/live-preview/app/live-preview/_components/Link/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import LinkWithDefault from 'next/link.js'
import NextLinkImport from 'next/link.js'
import React from 'react'

import type { Page, Post } from '../../../../payload-types.js'
import type { Props as ButtonProps } from '../Button/index.js'

import { Button } from '../Button/index.js'

const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
const NextLink = (NextLinkImport.default || NextLinkImport) as typeof NextLinkImport.default

type CMSLinkType = {
appearance?: ButtonProps['appearance']
Expand Down Expand Up @@ -36,20 +36,22 @@ export const CMSLink: React.FC<CMSLinkType> = ({
}) => {
const href =
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
? `/${reference.value.slug}`
? `/live-preview/${reference.value.slug}`
: url

if (!href) return null
if (!href) {
return null
}

if (!appearance) {
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}

if (href || url) {
return (
<Link {...newTabProps} className={className} href={href || url || ''}>
<NextLink {...newTabProps} className={className} href={href || url || ''}>
{label && label}
{children && children}
</Link>
{children || null}
</NextLink>
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-restricted-exports */
import { notFound } from 'next/navigation.js'
import React from 'react'

Expand Down Expand Up @@ -32,10 +33,11 @@ export default async function Page({ params: paramsPromise }: Args) {

export async function generateStaticParams() {
process.env.PAYLOAD_DROP_DATABASE = 'false'

try {
const pages = await getDocs<Page>('pages')
return pages?.map(({ slug }) => slug)
} catch (error) {
} catch (_err) {
return []
}
}
4 changes: 2 additions & 2 deletions test/live-preview/prod/app/live-preview/_api/getDoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ export const getDoc = async <T>(args: {
return docs[0] as T
}
} catch (err) {
console.log('Error getting doc', err)
throw new Error(`Error getting doc: ${err.message}`)
}

throw new Error('Error getting doc')
throw new Error('No doc found')
}
12 changes: 7 additions & 5 deletions test/live-preview/prod/app/live-preview/_api/getDocs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import config from '@payload-config'
import { getPayload } from 'payload'
import { type CollectionSlug, getPayload } from 'payload'

export const getDocs = async <T>(collection: string): Promise<T[]> => {
export const getDocs = async <T>(collection: CollectionSlug): Promise<T[]> => {
const payload = await getPayload({ config })

try {
Expand All @@ -11,10 +11,12 @@ export const getDocs = async <T>(collection: string): Promise<T[]> => {
limit: 100,
})

return docs as T[]
if (docs) {
return docs as T[]
}
} catch (err) {
console.error(err)
throw new Error(`Error getting docs: ${err.message}`)
}

throw new Error('Error getting docs')
throw new Error('No docs found')
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export async function Footer() {
<img
alt="Payload Logo"
className={classes.logo}
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/payload-logo-light.svg"
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-logo-light.svg"
/>
</picture>
</Link>
Expand All @@ -30,7 +30,7 @@ export async function Footer() {
return <CMSLink key={i} {...link} />
})}
<Link href="/admin">Admin</Link>
<Link href="https://github.com/payloadcms/payload/tree/main/templates/ecommerce">
<Link href="https://github.com/payloadcms/payload/tree/main/test/live-preview">
Source Code
</Link>
<Link href="https://github.com/payloadcms/payload">Payload</Link>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import LinkWithDefault from 'next/link.js'
import NextLinkImport from 'next/link.js'
import React from 'react'

import type { Page, Post } from '../../../../../payload-types.js'
import type { Props as ButtonProps } from '../Button/index.js'

import { Button } from '../Button/index.js'

const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
const NextLink = (NextLinkImport.default || NextLinkImport) as typeof NextLinkImport.default

type CMSLinkType = {
appearance?: ButtonProps['appearance']
Expand Down Expand Up @@ -36,20 +36,22 @@ export const CMSLink: React.FC<CMSLinkType> = ({
}) => {
const href =
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
? `/${reference.value.slug}`
? `/live-preview/${reference.value.slug}`
: url

if (!href) return null
if (!href) {
return null
}

if (!appearance) {
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}

if (href || url) {
return (
<Link {...newTabProps} className={className} href={href || url || ''}>
<NextLink {...newTabProps} className={className} href={href || url || ''}>
{label && label}
{children && children}
</Link>
{children || null}
</NextLink>
)
}
}
Expand Down
2 changes: 1 addition & 1 deletion test/live-preview/seed/posts-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { postsSlug } from '../shared.js'

export const postsPage: Partial<Page> = {
title: 'Posts',
slug: 'live-preview/posts',
slug: 'posts',
meta: {
title: 'Payload Website Template',
description: 'An open-source website built with Payload and Next.js.',
Expand Down
3 changes: 2 additions & 1 deletion test/live-preview/utilities/formatLivePreviewURL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const formatLivePreviewURL: LivePreviewConfig['url'] = async ({
collectionConfig,
payload,
}) => {
let baseURL = 'http://localhost:3000/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
Expand All @@ -25,6 +25,7 @@ export const formatLivePreviewURL: LivePreviewConfig['url'] = async ({
.then((res) => res?.docs?.[0])

if (fullTenant?.clientURL) {
// Note: appending a fully-qualified URL here won't work for preview deployments on Vercel
baseURL = `${fullTenant.clientURL}/live-preview`
}
} catch (e) {
Expand Down

0 comments on commit f12b4dc

Please sign in to comment.