diff --git a/packages/next/src/lib/metadata/resolvers/resolve-url.test.ts b/packages/next/src/lib/metadata/resolvers/resolve-url.test.ts index b9e1caf796aa5..d70c7bc5dc81a 100644 --- a/packages/next/src/lib/metadata/resolvers/resolve-url.test.ts +++ b/packages/next/src/lib/metadata/resolvers/resolve-url.test.ts @@ -110,6 +110,17 @@ describe('resolveAbsoluteUrlWithPathname', () => { 'https://example.com/foo?bar' ) }) + + it('should not add trailing slash to relative url that matches file pattern', () => { + expect(resolver('/foo.html')).toBe('https://example.com/foo.html') + expect(resolver('/foo.html?q=v')).toBe('https://example.com/foo.html?q=v') + expect(resolver(new URL('/.well-known/bar.jpg', metadataBase))).toBe( + 'https://example.com/.well-known/bar.jpg/' + ) + expect(resolver(new URL('/foo.html', metadataBase))).toBe( + 'https://example.com/foo.html' + ) + }) }) }) diff --git a/packages/next/src/lib/metadata/resolvers/resolve-url.ts b/packages/next/src/lib/metadata/resolvers/resolve-url.ts index 89edce00fa6db..6b4a25bebf3e9 100644 --- a/packages/next/src/lib/metadata/resolvers/resolve-url.ts +++ b/packages/next/src/lib/metadata/resolvers/resolve-url.ts @@ -86,6 +86,13 @@ function resolveRelativeUrl(url: string | URL, pathname: string): string | URL { return url } +// The regex is matching logic from packages/next/src/lib/load-custom-routes.ts +const FILE_REGEX = + /^(?:\/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+))(\/?|$)/i +function isFilePattern(pathname: string): boolean { + return FILE_REGEX.test(pathname) +} + // Resolve `pathname` if `url` is a relative path the compose with `metadataBase`. function resolveAbsoluteUrlWithPathname( url: string | URL, @@ -110,18 +117,27 @@ function resolveAbsoluteUrlWithPathname( // - Doesn't have query if (trailingSlash && !resolvedUrl.endsWith('/')) { let isRelative = resolvedUrl.startsWith('/') - let isExternal = false let hasQuery = resolvedUrl.includes('?') + let isExternal = false + let isFileUrl = false + if (!isRelative) { try { const parsedUrl = new URL(resolvedUrl) isExternal = metadataBase != null && parsedUrl.origin !== metadataBase.origin + isFileUrl = isFilePattern(parsedUrl.pathname) } catch { // If it's not a valid URL, treat it as external isExternal = true } - if (!isExternal && !hasQuery) return `${resolvedUrl}/` + if ( + // Do not apply trailing slash for file like urls, aligning with the behavior with `trailingSlash` + !isFileUrl && + !isExternal && + !hasQuery + ) + return `${resolvedUrl}/` } }