diff --git a/packages/next/src/lib/metadata/default-metadata.tsx b/packages/next/src/lib/metadata/default-metadata.tsx index e0723aee07572..d70f1dc74df7f 100644 --- a/packages/next/src/lib/metadata/default-metadata.tsx +++ b/packages/next/src/lib/metadata/default-metadata.tsx @@ -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, diff --git a/packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts b/packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts index 10d60e0e34e8e..087d90dd21e24 100644 --- a/packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts +++ b/packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts @@ -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'], @@ -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) @@ -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) { diff --git a/packages/next/src/lib/metadata/resolvers/resolve-url.ts b/packages/next/src/lib/metadata/resolvers/resolve-url.ts index 7d59f5d553c59..269a3104d9a47 100644 --- a/packages/next/src/lib/metadata/resolvers/resolve-url.ts +++ b/packages/next/src/lib/metadata/resolvers/resolve-url.ts @@ -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( @@ -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 diff --git a/test/e2e/app-dir/metadata-dynamic-routes/app/layout.tsx b/test/e2e/app-dir/metadata-dynamic-routes/app/layout.tsx index 2e96e371aa4ff..9b1a363d19bf0 100644 --- a/test/e2e/app-dir/metadata-dynamic-routes/app/layout.tsx +++ b/test/e2e/app-dir/metadata-dynamic-routes/app/layout.tsx @@ -8,9 +8,7 @@ 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: { @@ -18,4 +16,7 @@ export const metadata = { title: 'Twitter - Next.js App', description: 'Twitter - This is a Next.js App', }, + alternates: { + canonical: './', + }, } diff --git a/test/e2e/app-dir/metadata-dynamic-routes/index.test.ts b/test/e2e/app-dir/metadata-dynamic-routes/index.test.ts index 5e9644b0c7968..0f30efe5a11e6 100644 --- a/test/e2e/app-dir/metadata-dynamic-routes/index.test.ts +++ b/test/e2e/app-dir/metadata-dynamic-routes/index.test.ts @@ -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"]') @@ -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) diff --git a/test/e2e/app-dir/metadata/metadata.test.ts b/test/e2e/app-dir/metadata/metadata.test.ts index ef759b0555b00..c6042927ce5a2 100644 --- a/test/e2e/app-dir/metadata/metadata.test.ts +++ b/test/e2e/app-dir/metadata/metadata.test.ts @@ -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', })