diff --git a/packages/next/src/build/webpack/loaders/metadata/discover.ts b/packages/next/src/build/webpack/loaders/metadata/discover.ts index e3e6058a4a5b..b89e93c32ade 100644 --- a/packages/next/src/build/webpack/loaders/metadata/discover.ts +++ b/packages/next/src/build/webpack/loaders/metadata/discover.ts @@ -1,7 +1,7 @@ import type webpack from 'webpack' import type { CollectingMetadata, - PossibleImageFileNameConvention, + PossibleStaticMetadataFileNameConvention, } from './types' import path from 'path' import { stringify } from 'querystring' @@ -20,17 +20,22 @@ async function enumMetadataFiles( { resolvePath, loaderContext, + // When set to true, possible filename without extension could: icon, icon0, ..., icon9 + numericSuffix, }: { resolvePath: (pathname: string) => Promise loaderContext: webpack.LoaderContext + numericSuffix: boolean } ) { const collectedFiles: string[] = [] - // Possible filename without extension could: icon, icon0, ..., icon9 + const possibleFileNames = [filename].concat( - Array(10) - .fill(0) - .map((_, index) => filename + index) + numericSuffix + ? Array(10) + .fill(0) + .map((_, index) => filename + index) + : [] ) for (const name of possibleFileNames) { for (const ext of extensions) { @@ -74,6 +79,7 @@ export async function createStaticMetadataFromRoute( apple: [], twitter: [], openGraph: [], + manifest: undefined, } const opts = { @@ -82,8 +88,27 @@ export async function createStaticMetadataFromRoute( } async function collectIconModuleIfExists( - type: PossibleImageFileNameConvention + type: PossibleStaticMetadataFileNameConvention ) { + if (type === 'manifest') { + const staticManifestExtension = ['webmanifest', 'json'] + const manifestFile = await enumMetadataFiles( + resolvedDir, + 'manifest', + staticManifestExtension.concat(pageExtensions), + { ...opts, numericSuffix: false } + ) + if (manifestFile.length > 0) { + hasStaticMetadataFiles = true + const { name, ext } = path.parse(manifestFile[0]) + const extension = staticManifestExtension.includes(ext.slice(1)) + ? ext.slice(1) + : 'webmanifest' + staticImagesMetadata.manifest = JSON.stringify(`/${name}.${extension}`) + } + return + } + const resolvedMetadataFiles = await enumMetadataFiles( resolvedDir, STATIC_METADATA_IMAGES[type].filename, @@ -91,7 +116,7 @@ export async function createStaticMetadataFromRoute( ...STATIC_METADATA_IMAGES[type].extensions, ...(type === 'favicon' ? [] : pageExtensions), ], - opts + { ...opts, numericSuffix: true } ) resolvedMetadataFiles .sort((a, b) => a.localeCompare(b)) @@ -123,6 +148,7 @@ export async function createStaticMetadataFromRoute( collectIconModuleIfExists('openGraph'), collectIconModuleIfExists('twitter'), isRootLayoutOrRootPage && collectIconModuleIfExists('favicon'), + isRootLayoutOrRootPage && collectIconModuleIfExists('manifest'), ]) return hasStaticMetadataFiles ? staticImagesMetadata : null @@ -137,6 +163,7 @@ export function createMetadataExportsCode( apple: [${metadata.apple.join(',')}], openGraph: [${metadata.openGraph.join(',')}], twitter: [${metadata.twitter.join(',')}], + manifest: ${metadata.manifest ? metadata.manifest : 'undefined'} }` : '' } diff --git a/packages/next/src/build/webpack/loaders/metadata/types.ts b/packages/next/src/build/webpack/loaders/metadata/types.ts index 89b2bfd48057..fddcedb94197 100644 --- a/packages/next/src/build/webpack/loaders/metadata/types.ts +++ b/packages/next/src/build/webpack/loaders/metadata/types.ts @@ -11,6 +11,7 @@ export type CollectingMetadata = { apple: string[] twitter: string[] openGraph: string[] + manifest?: string } // Contain the collecting evaluated image module @@ -19,6 +20,7 @@ export type CollectedMetadata = { apple: ComponentModule[] twitter: ComponentModule[] | null openGraph: ComponentModule[] | null + manifest?: string } export type MetadataImageModule = { @@ -39,3 +41,7 @@ export type PossibleImageFileNameConvention = | 'favicon' | 'twitter' | 'openGraph' + +export type PossibleStaticMetadataFileNameConvention = + | PossibleImageFileNameConvention + | 'manifest' diff --git a/packages/next/src/lib/metadata/resolve-metadata.ts b/packages/next/src/lib/metadata/resolve-metadata.ts index 398483c4f395..959d52a89e6a 100644 --- a/packages/next/src/lib/metadata/resolve-metadata.ts +++ b/packages/next/src/lib/metadata/resolve-metadata.ts @@ -45,7 +45,7 @@ function mergeStaticMetadata( staticFilesMetadata: StaticMetadata ) { if (!staticFilesMetadata) return - const { icon, apple, openGraph, twitter } = staticFilesMetadata + const { icon, apple, openGraph, twitter, manifest } = staticFilesMetadata if (icon || apple) { metadata.icons = { icon: icon || [], @@ -67,6 +67,9 @@ function mergeStaticMetadata( ) metadata.openGraph = resolvedOpenGraph } + if (manifest) { + metadata.manifest = manifest + } return metadata } @@ -249,6 +252,7 @@ async function resolveStaticMetadata(components: ComponentsType, props: any) { apple, openGraph, twitter, + manifest: metadata.manifest, } return staticMetadata 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 28a0e98344cd..20c1e4aaf266 100644 --- a/test/e2e/app-dir/metadata-dynamic-routes/index.test.ts +++ b/test/e2e/app-dir/metadata-dynamic-routes/index.test.ts @@ -201,6 +201,11 @@ createNextDescribe( expect($('link[rel="favicon"]')).toHaveLength(0) + // manifest + expect($('link[rel="manifest"]').attr('href')).toBe( + '/manifest.webmanifest' + ) + // non absolute urls expect($icon.attr('href')).toContain('/icon') expect($icon.attr('href')).toMatch(hashRegex)