Skip to content

Commit

Permalink
Prefer to use deployment url for metadata routes on production (#48556)
Browse files Browse the repository at this point in the history
Use should only need to configure one `metadataBase` as fixed domain or
url but metadata image routes can still work properly with `VERCEL_URL`
in preview deployments

If you configured `new URL('https://mydomain.com')`, it will work for
canonical url all the time since it's for SEO.
For preview deployments metadata image routes will work with deployment
url as they're always bound with deployment.
For production deployments metadata images routes can be alias to the
configured `metadataBase` as they could be the expected exposed domain

Follow up for #47910

link NEXT-887
  • Loading branch information
huozhi committed Apr 19, 2023
1 parent 56b32eb commit 6f30c91
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 28 deletions.
7 changes: 1 addition & 6 deletions packages/next/src/lib/metadata/default-metadata.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import type { ResolvedMetadata } from './types/metadata-interface'

export function createDefaultMetadata(): ResolvedMetadata {
const defaultMetadataBase =
process.env.NODE_ENV === 'production' && process.env.VERCEL_URL
? new URL(`https://${process.env.VERCEL_URL}`)
: null

return {
viewport: 'width=device-width, initial-scale=1',
metadataBase: defaultMetadataBase,
metadataBase: null,

// Other values are all null
title: null,
Expand Down
12 changes: 9 additions & 3 deletions packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import type {
import type { FieldResolverWithMetadataBase } from '../types/resolvers'
import type { ResolvedTwitterMetadata, Twitter } from '../types/twitter-types'
import { resolveAsArrayOrUndefined } from '../generate/utils'
import { isStringOrURL, resolveUrl } from './resolve-url'
import {
getFallbackMetadataBaseIfPresent,
isStringOrURL,
resolveUrl,
} from './resolve-url'

const OgTypeFields = {
article: ['authors', 'tags'],
Expand Down Expand Up @@ -97,7 +101,8 @@ export const resolveOpenGraph: FieldResolverWithMetadataBase<'openGraph'> = (
}
}

resolved.images = resolveImages(og.images, metadataBase)
const imageMetadataBase = getFallbackMetadataBaseIfPresent(metadataBase)
resolved.images = resolveImages(og.images, imageMetadataBase)
}

assignProps(openGraph)
Expand Down Expand Up @@ -127,7 +132,8 @@ export const resolveTwitter: FieldResolverWithMetadataBase<'twitter'> = (
for (const infoKey of TwitterBasicInfoKeys) {
resolved[infoKey] = twitter[infoKey] || null
}
resolved.images = resolveImages(twitter.images, metadataBase)
const imageMetadataBase = getFallbackMetadataBaseIfPresent(metadataBase)
resolved.images = resolveImages(twitter.images, imageMetadataBase)

if ('card' in resolved) {
switch (resolved.card) {
Expand Down
21 changes: 20 additions & 1 deletion packages/next/src/lib/metadata/resolvers/resolve-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ function isStringOrURL(icon: any): icon is string | URL {
return typeof icon === 'string' || icon instanceof URL
}

function createLocalMetadataBase() {
return new URL(`http://localhost:${process.env.PORT || 3000}`)
}

// For deployment url for metadata routes, prefer to use the deployment url if possible
// as these routes are unique to the deployments url.
export function getFallbackMetadataBaseIfPresent(
metadataBase: URL | null
): URL | null {
const defaultMetadataBase = createLocalMetadataBase()
return process.env.NODE_ENV === 'production' &&
process.env.VERCEL_URL &&
process.env.VERCEL_ENV === 'preview'
? new URL(`https://${process.env.VERCEL_URL}`)
: process.env.NODE_ENV === 'development'
? defaultMetadataBase
: metadataBase || defaultMetadataBase
}

function resolveUrl(url: null | undefined, metadataBase: URL | null): null
function resolveUrl(url: string | URL, metadataBase: URL | null): URL
function resolveUrl(
Expand All @@ -24,7 +43,7 @@ function resolveUrl(
} catch (_) {}

if (!metadataBase) {
metadataBase = new URL(`http://localhost:${process.env.PORT || 3000}`)
metadataBase = createLocalMetadataBase()
}

// Handle relative or absolute paths
Expand Down
7 changes: 4 additions & 3 deletions test/e2e/app-dir/metadata-dynamic-routes/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ export default function Layout({ children }) {
}

export const metadata = {
metadataBase: process.env.VERCEL_URL
? new URL(`https://${process.env.VERCEL_URL}`)
: new URL(`http://localhost:${process.env.PORT || 3000}`),
metadataBase: new URL('https://mydomain.com'),
title: 'Next.js App',
description: 'This is a Next.js App',
twitter: {
cardType: 'summary_large_image',
title: 'Twitter - Next.js App',
description: 'Twitter - This is a Next.js App',
},
alternates: {
canonical: './',
},
}
31 changes: 20 additions & 11 deletions test/e2e/app-dir/metadata-dynamic-routes/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ createNextDescribe(
expect(resTwitter.status).toBe(200)
})

it('should pick configured metadataBase instead of deployment url for canonical url', async () => {
const $ = await next.render$('/')
const canonicalUrl = $('link[rel="canonical"]').attr('href')
expect(canonicalUrl).toBe('https://mydomain.com/')
})

it('should inject dynamic metadata properly to head', async () => {
const $ = await next.render$('/')
const $icon = $('link[rel="icon"]')
Expand Down Expand Up @@ -269,21 +275,24 @@ createNextDescribe(
expect(twitterTitle).toBe('Twitter - Next.js App')
expect(twitterDescription).toBe('Twitter - This is a Next.js App')

// Should prefer to pick up deployment url for metadata routes
let ogImageUrlPattern
let twitterImageUrlPattern
if (isNextDeploy) {
// absolute urls
expect(ogImageUrl).toMatch(
/https:\/\/\w+.vercel.app\/opengraph-image\?/
)
expect(twitterImageUrl).toMatch(
/https:\/\/\w+.vercel.app\/twitter-image\?/
)
ogImageUrlPattern = /https:\/\/\w+.vercel.app\/opengraph-image\?/
twitterImageUrlPattern = /https:\/\/\w+.vercel.app\/twitter-image\?/
} else if (isNextStart) {
// configured metadataBase for next start
ogImageUrlPattern = /https:\/\/mydomain.com\/opengraph-image\?/
twitterImageUrlPattern = /https:\/\/mydomain.com\/twitter-image\?/
} else {
// absolute urls
expect(ogImageUrl).toMatch(/http:\/\/localhost:\d+\/opengraph-image\?/)
expect(twitterImageUrl).toMatch(
/http:\/\/localhost:\d+\/twitter-image\?/
)
// localhost for dev
ogImageUrlPattern = /http:\/\/localhost:\d+\/opengraph-image\?/
twitterImageUrlPattern = /http:\/\/localhost:\d+\/twitter-image\?/
}
expect(ogImageUrl).toMatch(ogImageUrlPattern)
expect(twitterImageUrl).toMatch(twitterImageUrlPattern)
expect(ogImageUrl).toMatch(hashRegex)
expect(twitterImageUrl).toMatch(hashRegex)

Expand Down
14 changes: 10 additions & 4 deletions test/e2e/app-dir/metadata/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,13 +487,19 @@ createNextDescribe(
'og:image:height': '114',
'og:image:type': 'image/png',
'og:image:alt': 'A alt txt for og',
'og:image':
'https://example.com/opengraph/static/opengraph-image.png?b76e8f0282c93c8e',
'og:image': isNextDev
? expect.stringMatching(
/http:\/\/localhost:\d+\/opengraph\/static\/opengraph-image.png\?b76e8f0282c93c8e/
)
: 'https://example.com/opengraph/static/opengraph-image.png?b76e8f0282c93c8e',
})

await match('meta', 'name', 'content', {
'twitter:image':
'https://example.com/opengraph/static/twitter-image.png?b76e8f0282c93c8e',
'twitter:image': isNextDev
? expect.stringMatching(
/http:\/\/localhost:\d+\/opengraph\/static\/twitter-image.png\?b76e8f0282c93c8e/
)
: 'https://example.com/opengraph/static/twitter-image.png?b76e8f0282c93c8e',
'twitter:image:alt': 'A alt txt for twitter',
'twitter:card': 'summary_large_image',
})
Expand Down

0 comments on commit 6f30c91

Please sign in to comment.