Static og and twitter image files as metadata (#45797)
## Feature Closes NEXT-265 Fixes NEXT-516 - [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [x] Related issues linked using `fixes #number` - [x] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md)
This commit is contained in:
parent
8fb2f59169
commit
a5fe64cd06
17 changed files with 213 additions and 101 deletions
|
@ -61,7 +61,7 @@ import { AppBuildManifestPlugin } from './webpack/plugins/app-build-manifest-plu
|
|||
import { SubresourceIntegrityPlugin } from './webpack/plugins/subresource-integrity-plugin'
|
||||
import { FontLoaderManifestPlugin } from './webpack/plugins/font-loader-manifest-plugin'
|
||||
import { getSupportedBrowsers } from './utils'
|
||||
import { METADATA_IMAGE_RESOURCE_QUERY } from './webpack/loaders/app-dir/metadata'
|
||||
import { METADATA_IMAGE_RESOURCE_QUERY } from './webpack/loaders/metadata/discover'
|
||||
|
||||
const EXTERNAL_PACKAGES = require('../lib/server-external-packages.json')
|
||||
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
// TODO-APP: check if this can be narrowed.
|
||||
export type ComponentModule = () => any
|
||||
export type ModuleReference = [
|
||||
componentModule: ComponentModule,
|
||||
filePath: string
|
||||
]
|
||||
|
||||
export type CollectedMetadata = {
|
||||
icon: ComponentModule[]
|
||||
apple: ComponentModule[]
|
||||
}
|
|
@ -1,8 +1,16 @@
|
|||
import type webpack from 'webpack'
|
||||
import type { AppLoaderOptions } from '../next-app-loader'
|
||||
import type { CollectingMetadata } from './types'
|
||||
import path from 'path'
|
||||
import { stringify } from 'querystring'
|
||||
|
||||
type PossibleImageFileNameConvention =
|
||||
| 'icon'
|
||||
| 'apple'
|
||||
| 'favicon'
|
||||
| 'twitter'
|
||||
| 'opengraph'
|
||||
|
||||
const METADATA_TYPE = 'metadata'
|
||||
|
||||
export const METADATA_IMAGE_RESOURCE_QUERY = '?__next_metadata'
|
||||
|
@ -14,12 +22,20 @@ const staticAssetIconsImage = {
|
|||
},
|
||||
apple: {
|
||||
filename: 'apple-icon',
|
||||
extensions: ['jpg', 'jpeg', 'png', 'svg'],
|
||||
extensions: ['jpg', 'jpeg', 'png'],
|
||||
},
|
||||
favicon: {
|
||||
filename: 'favicon',
|
||||
extensions: ['ico'],
|
||||
},
|
||||
opengraph: {
|
||||
filename: 'opengraph-image',
|
||||
extensions: ['jpg', 'jpeg', 'png', 'gif'],
|
||||
},
|
||||
twitter: {
|
||||
filename: 'twitter-image',
|
||||
extensions: ['jpg', 'jpeg', 'png', 'gif'],
|
||||
},
|
||||
}
|
||||
|
||||
// Produce all compositions with filename (icon, apple-icon, etc.) with extensions (png, jpg, etc.)
|
||||
|
@ -77,12 +93,11 @@ export async function discoverStaticMetadataFiles(
|
|||
}
|
||||
) {
|
||||
let hasStaticMetadataFiles = false
|
||||
const iconsMetadata: {
|
||||
icon: string[]
|
||||
apple: string[]
|
||||
} = {
|
||||
const staticImagesMetadata: CollectingMetadata = {
|
||||
icon: [],
|
||||
apple: [],
|
||||
twitter: [],
|
||||
opengraph: [],
|
||||
}
|
||||
|
||||
const opts = {
|
||||
|
@ -95,7 +110,9 @@ export async function discoverStaticMetadataFiles(
|
|||
assetPrefix: loaderOptions.assetPrefix,
|
||||
}
|
||||
|
||||
async function collectIconModuleIfExists(type: 'icon' | 'apple' | 'favicon') {
|
||||
async function collectIconModuleIfExists(
|
||||
type: PossibleImageFileNameConvention
|
||||
) {
|
||||
const resolvedMetadataFiles = await enumMetadataFiles(
|
||||
resolvedDir,
|
||||
staticAssetIconsImage[type].filename,
|
||||
|
@ -105,19 +122,21 @@ export async function discoverStaticMetadataFiles(
|
|||
resolvedMetadataFiles
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.forEach((filepath) => {
|
||||
const iconModule = `() => import(/* webpackMode: "eager" */ ${JSON.stringify(
|
||||
`next-metadata-image-loader?${stringify(
|
||||
metadataImageLoaderOptions
|
||||
)}!` +
|
||||
const imageModule = `() => import(/* webpackMode: "eager" */ ${JSON.stringify(
|
||||
`next-metadata-image-loader?${stringify({
|
||||
...metadataImageLoaderOptions,
|
||||
numericSizes:
|
||||
type === 'twitter' || type === 'opengraph' ? '1' : undefined,
|
||||
})}!` +
|
||||
filepath +
|
||||
METADATA_IMAGE_RESOURCE_QUERY
|
||||
)})`
|
||||
|
||||
hasStaticMetadataFiles = true
|
||||
if (type === 'favicon') {
|
||||
iconsMetadata.icon.unshift(iconModule)
|
||||
staticImagesMetadata.icon.unshift(imageModule)
|
||||
} else {
|
||||
iconsMetadata[type].push(iconModule)
|
||||
staticImagesMetadata[type].push(imageModule)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -125,10 +144,12 @@ export async function discoverStaticMetadataFiles(
|
|||
await Promise.all([
|
||||
collectIconModuleIfExists('icon'),
|
||||
collectIconModuleIfExists('apple'),
|
||||
collectIconModuleIfExists('opengraph'),
|
||||
collectIconModuleIfExists('twitter'),
|
||||
isRootLayer && collectIconModuleIfExists('favicon'),
|
||||
])
|
||||
|
||||
return hasStaticMetadataFiles ? iconsMetadata : null
|
||||
return hasStaticMetadataFiles ? staticImagesMetadata : null
|
||||
}
|
||||
|
||||
export function buildMetadata(
|
||||
|
@ -137,7 +158,9 @@ export function buildMetadata(
|
|||
return metadata
|
||||
? `${METADATA_TYPE}: {
|
||||
icon: [${metadata.icon.join(',')}],
|
||||
apple: [${metadata.apple.join(',')}]
|
||||
apple: [${metadata.apple.join(',')}],
|
||||
opengraph: [${metadata.opengraph.join(',')}],
|
||||
twitter: [${metadata.twitter.join(',')}],
|
||||
}`
|
||||
: ''
|
||||
}
|
33
packages/next/src/build/webpack/loaders/metadata/types.ts
Normal file
33
packages/next/src/build/webpack/loaders/metadata/types.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
// TODO-APP: check if this can be narrowed.
|
||||
export type ComponentModule = () => any
|
||||
export type ModuleReference = [
|
||||
componentModule: ComponentModule,
|
||||
filePath: string
|
||||
]
|
||||
|
||||
// Contain the collecting image module paths
|
||||
export type CollectingMetadata = {
|
||||
icon: string[]
|
||||
apple: string[]
|
||||
twitter: string[]
|
||||
opengraph: string[]
|
||||
}
|
||||
|
||||
// Contain the collecting evaluated image module
|
||||
export type CollectedMetadata = {
|
||||
icon: ComponentModule[]
|
||||
apple: ComponentModule[]
|
||||
twitter: ComponentModule[] | null
|
||||
opengraph: ComponentModule[] | null
|
||||
}
|
||||
|
||||
export type MetadataImageModule = {
|
||||
url: string
|
||||
type?: string
|
||||
} & (
|
||||
| { sizes?: string }
|
||||
| {
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
)
|
|
@ -1,6 +1,6 @@
|
|||
import type webpack from 'webpack'
|
||||
import type { ValueOf } from '../../../shared/lib/constants'
|
||||
import type { ModuleReference, CollectedMetadata } from './app-dir/types'
|
||||
import type { ModuleReference, CollectedMetadata } from './metadata/types'
|
||||
|
||||
import path from 'path'
|
||||
import chalk from 'next/dist/compiled/chalk'
|
||||
|
@ -9,7 +9,7 @@ import { getModuleBuildInfo } from './get-module-build-info'
|
|||
import { verifyRootLayout } from '../../../lib/verifyRootLayout'
|
||||
import * as Log from '../../../build/output/log'
|
||||
import { APP_DIR_ALIAS } from '../../../lib/constants'
|
||||
import { buildMetadata, discoverStaticMetadataFiles } from './app-dir/metadata'
|
||||
import { buildMetadata, discoverStaticMetadataFiles } from './metadata/discover'
|
||||
|
||||
const isNotResolvedError = (err: any) => err.message.includes("Can't resolve")
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import type { MetadataImageModule } from './metadata/types'
|
||||
import loaderUtils from 'next/dist/compiled/loader-utils3'
|
||||
import { getImageSize } from '../../../server/image-optimizer'
|
||||
|
||||
interface Options {
|
||||
isServer: boolean
|
||||
isDev: boolean
|
||||
assetPrefix: string
|
||||
numericSizes: boolean
|
||||
}
|
||||
|
||||
const mimeTypeMap = {
|
||||
|
@ -12,11 +13,13 @@ const mimeTypeMap = {
|
|||
png: 'image/png',
|
||||
ico: 'image/x-icon',
|
||||
svg: 'image/svg+xml',
|
||||
} as const
|
||||
avif: 'image/avif',
|
||||
webp: 'image/webp',
|
||||
}
|
||||
|
||||
async function nextMetadataImageLoader(this: any, content: Buffer) {
|
||||
const options: Options = this.getOptions()
|
||||
const { assetPrefix, isDev } = options
|
||||
const { assetPrefix, isDev, numericSizes } = options
|
||||
const context = this.rootContext
|
||||
|
||||
const opts = { context, content }
|
||||
|
@ -41,14 +44,22 @@ async function nextMetadataImageLoader(this: any, content: Buffer) {
|
|||
throw err
|
||||
}
|
||||
|
||||
const stringifiedData = JSON.stringify({
|
||||
const imageData: MetadataImageModule = {
|
||||
url: outputPath,
|
||||
...(extension in mimeTypeMap && {
|
||||
type: mimeTypeMap[extension as keyof typeof mimeTypeMap],
|
||||
}),
|
||||
sizes:
|
||||
extension === 'ico' ? 'any' : `${imageSize.width}x${imageSize.height}`,
|
||||
})
|
||||
...(numericSizes
|
||||
? { width: imageSize.width as number, height: imageSize.height as number }
|
||||
: {
|
||||
sizes:
|
||||
extension === 'ico'
|
||||
? 'any'
|
||||
: `${imageSize.width}x${imageSize.height}`,
|
||||
}),
|
||||
}
|
||||
|
||||
const stringifiedData = JSON.stringify(imageData)
|
||||
|
||||
this.emitFile(`../${isDev ? '' : '../'}${interpolatedName}`, content, null)
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
const MetadataContext = (React as any).createServerContext(null)
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
MetadataContext.displayName = 'MetadataContext'
|
||||
}
|
||||
|
||||
export function useMetadataBase() {
|
||||
return React.useContext(MetadataContext)
|
||||
}
|
||||
|
||||
export default MetadataContext
|
|
@ -4,6 +4,7 @@ import type {
|
|||
ResolvingMetadata,
|
||||
} from './types/metadata-interface'
|
||||
import type { AbsoluteTemplateString } from './types/metadata-types'
|
||||
import type { MetadataImageModule } from '../../build/webpack/loaders/metadata/types'
|
||||
import { createDefaultMetadata } from './default-metadata'
|
||||
import { resolveOpenGraph, resolveTwitter } from './resolvers/resolve-opengraph'
|
||||
import { mergeTitle } from './resolvers/resolve-title'
|
||||
|
@ -25,17 +26,63 @@ import {
|
|||
} from './resolvers/resolve-basics'
|
||||
import { resolveIcons } from './resolvers/resolve-icons'
|
||||
|
||||
type StaticMetadata = Awaited<ReturnType<typeof resolveStaticMetadata>>
|
||||
|
||||
type MetadataResolver = (
|
||||
_parent: ResolvingMetadata
|
||||
) => Metadata | Promise<Metadata>
|
||||
export type MetadataItems = [
|
||||
Metadata | MetadataResolver | null,
|
||||
StaticMetadata
|
||||
][]
|
||||
|
||||
function mergeStaticMetadata(
|
||||
metadata: ResolvedMetadata,
|
||||
staticFilesMetadata: StaticMetadata
|
||||
) {
|
||||
if (!staticFilesMetadata) return
|
||||
const { icon, apple, opengraph, twitter } = staticFilesMetadata
|
||||
if (icon || apple) {
|
||||
if (!metadata.icons) metadata.icons = { icon: [], apple: [] }
|
||||
if (icon) metadata.icons.icon.push(...icon)
|
||||
if (apple) metadata.icons.apple.push(...apple)
|
||||
}
|
||||
if (twitter) {
|
||||
const resolvedTwitter = resolveTwitter(
|
||||
{
|
||||
card: 'summary_large_image',
|
||||
images: twitter,
|
||||
},
|
||||
null
|
||||
)
|
||||
metadata.twitter = { ...metadata.twitter, ...resolvedTwitter! }
|
||||
}
|
||||
|
||||
if (opengraph) {
|
||||
const resolvedOg = resolveOpenGraph(
|
||||
{
|
||||
images: opengraph,
|
||||
},
|
||||
null
|
||||
)
|
||||
metadata.openGraph = { ...metadata.openGraph, ...resolvedOg! }
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
// Merge the source metadata into the resolved target metadata.
|
||||
function merge(
|
||||
target: ResolvedMetadata,
|
||||
source: Metadata,
|
||||
source: Metadata | null,
|
||||
staticFilesMetadata: StaticMetadata,
|
||||
templateStrings: {
|
||||
title: string | null
|
||||
openGraph: string | null
|
||||
twitter: string | null
|
||||
}
|
||||
) {
|
||||
const metadataBase = source.metadataBase || null
|
||||
const metadataBase = source?.metadataBase || null
|
||||
for (const key_ in source) {
|
||||
const key = key_ as keyof Metadata
|
||||
|
||||
|
@ -122,16 +169,9 @@ function merge(
|
|||
break
|
||||
}
|
||||
}
|
||||
mergeStaticMetadata(target, staticFilesMetadata)
|
||||
}
|
||||
|
||||
type MetadataResolver = (
|
||||
_parent: ResolvingMetadata
|
||||
) => Metadata | Promise<Metadata>
|
||||
export type MetadataItems = [
|
||||
Metadata | MetadataResolver | null,
|
||||
Metadata | null
|
||||
][]
|
||||
|
||||
async function getDefinedMetadata(
|
||||
mod: any,
|
||||
props: any
|
||||
|
@ -156,35 +196,39 @@ async function getDefinedMetadata(
|
|||
)
|
||||
}
|
||||
|
||||
async function collectStaticFsBasedIcons(
|
||||
async function collectStaticImagesFiles(
|
||||
metadata: ComponentsType['metadata'],
|
||||
type: 'icon' | 'apple'
|
||||
type: keyof NonNullable<ComponentsType['metadata']>
|
||||
) {
|
||||
if (!metadata?.[type]) return undefined
|
||||
const iconPromises = metadata[type].map(
|
||||
|
||||
const iconPromises = metadata[type as 'icon' | 'apple'].map(
|
||||
// TODO-APP: share the typing between next-metadata-image-loader and here
|
||||
async (iconResolver) =>
|
||||
interopDefault(await iconResolver()) as { url: string; sizes: string }
|
||||
async (iconResolver: any) =>
|
||||
interopDefault(await iconResolver()) as MetadataImageModule
|
||||
)
|
||||
return iconPromises?.length > 0 ? await Promise.all(iconPromises) : undefined
|
||||
}
|
||||
|
||||
async function resolveStaticMetadata(
|
||||
components: ComponentsType
|
||||
): Promise<Metadata | null> {
|
||||
async function resolveStaticMetadata(components: ComponentsType) {
|
||||
const { metadata } = components
|
||||
if (!metadata) return null
|
||||
|
||||
const [icon, apple] = await Promise.all([
|
||||
collectStaticFsBasedIcons(metadata, 'icon'),
|
||||
collectStaticFsBasedIcons(metadata, 'apple'),
|
||||
const [icon, apple, opengraph, twitter] = await Promise.all([
|
||||
collectStaticImagesFiles(metadata, 'icon'),
|
||||
collectStaticImagesFiles(metadata, 'apple'),
|
||||
collectStaticImagesFiles(metadata, 'opengraph'),
|
||||
collectStaticImagesFiles(metadata, 'twitter'),
|
||||
])
|
||||
|
||||
const icons: Metadata['icons'] = {}
|
||||
if (icon) icons.icon = icon
|
||||
if (apple) icons.apple = apple
|
||||
const staticMetadata = {
|
||||
icon,
|
||||
apple,
|
||||
opengraph,
|
||||
twitter,
|
||||
}
|
||||
|
||||
return { icons }
|
||||
return staticMetadata
|
||||
}
|
||||
|
||||
// [layout.metadata, static files metadata] -> ... -> [page.metadata, static files metadata]
|
||||
|
@ -208,28 +252,22 @@ export async function accumulateMetadata(
|
|||
|
||||
for (const item of metadataItems) {
|
||||
const [metadataExport, staticFilesMetadata] = item
|
||||
const layerMetadataPromise = Promise.resolve(
|
||||
const currentMetadata =
|
||||
typeof metadataExport === 'function'
|
||||
? metadataExport(parentPromise)
|
||||
: metadataExport
|
||||
)
|
||||
const layerMetadataPromise =
|
||||
currentMetadata instanceof Promise
|
||||
? currentMetadata
|
||||
: Promise.resolve(currentMetadata)
|
||||
|
||||
parentPromise = parentPromise.then((resolved) => {
|
||||
return layerMetadataPromise.then((exportedMetadata) => {
|
||||
const metadata = exportedMetadata || staticFilesMetadata
|
||||
|
||||
if (metadata) {
|
||||
// Overriding the metadata if static files metadata is present
|
||||
merge(
|
||||
resolved,
|
||||
{ ...metadata, ...staticFilesMetadata },
|
||||
{
|
||||
title: resolved.title?.template || null,
|
||||
openGraph: resolved.openGraph?.title?.template || null,
|
||||
twitter: resolved.twitter?.title?.template || null,
|
||||
}
|
||||
)
|
||||
}
|
||||
return layerMetadataPromise.then((metadata) => {
|
||||
merge(resolved, metadata, staticFilesMetadata, {
|
||||
title: resolved.title?.template || null,
|
||||
openGraph: resolved.openGraph?.title?.template || null,
|
||||
twitter: resolved.twitter?.title?.template || null,
|
||||
})
|
||||
|
||||
return resolved
|
||||
})
|
||||
|
|
|
@ -17,7 +17,10 @@ export const resolveIcons: FieldResolver<'icons'> = (icons) => {
|
|||
return null
|
||||
}
|
||||
|
||||
const resolved: ResolvedMetadata['icons'] = {}
|
||||
const resolved: ResolvedMetadata['icons'] = {
|
||||
icon: [],
|
||||
apple: [],
|
||||
}
|
||||
if (Array.isArray(icons)) {
|
||||
resolved.icon = icons.map(resolveIcon).filter(Boolean)
|
||||
} else if (isStringOrURL(icons)) {
|
||||
|
|
|
@ -100,11 +100,11 @@ export const resolveTwitter: FieldResolverWithMetadataBase<'twitter'> = (
|
|||
resolved.images = resolveAsArrayOrUndefined(twitter.images)?.map((item) => {
|
||||
if (isStringOrURL(item))
|
||||
return {
|
||||
url: resolveUrl(item, metadataBase),
|
||||
url: metadataBase ? resolveUrl(item, metadataBase) : item,
|
||||
}
|
||||
else {
|
||||
return {
|
||||
url: resolveUrl(item.url, metadataBase),
|
||||
url: metadataBase ? resolveUrl(item.url, metadataBase) : item.url,
|
||||
alt: item.alt,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -119,8 +119,8 @@ export type ResolvedVerification = {
|
|||
}
|
||||
|
||||
export type ResolvedIcons = {
|
||||
icon?: IconDescriptor[]
|
||||
icon: IconDescriptor[]
|
||||
apple: IconDescriptor[]
|
||||
shortcut?: IconDescriptor[]
|
||||
apple?: IconDescriptor[]
|
||||
other?: IconDescriptor[]
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ type Locale = string
|
|||
|
||||
type OpenGraphMetadata = {
|
||||
determiner?: 'a' | 'an' | 'the' | 'auto' | ''
|
||||
title?: TemplateString
|
||||
title?: string | TemplateString
|
||||
description?: string
|
||||
emails?: string | Array<string>
|
||||
phoneNumbers?: string | Array<string>
|
||||
|
@ -174,7 +174,7 @@ type ResolvedOpenGraphMetadata = {
|
|||
images?: Array<OGImage>
|
||||
audio?: Array<OGAudio>
|
||||
videos?: Array<OGVideo>
|
||||
url: null | URL
|
||||
url: null | URL | string
|
||||
countryName?: string
|
||||
ttl?: number
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ type TwitterPlayerDescriptor = {
|
|||
}
|
||||
|
||||
type ResolvedTwitterImage = {
|
||||
url: null | URL
|
||||
url: null | URL | string
|
||||
alt?: string
|
||||
}
|
||||
type ResolvedTwitterSummary = {
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
3
test/e2e/app-dir/metadata/app/opengraph/static/page.tsx
Normal file
3
test/e2e/app-dir/metadata/app/opengraph/static/page.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function Page() {
|
||||
return 'opengraph-static'
|
||||
}
|
BIN
test/e2e/app-dir/metadata/app/opengraph/static/twitter-image.png
Normal file
BIN
test/e2e/app-dir/metadata/app/opengraph/static/twitter-image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
|
@ -407,6 +407,31 @@ createNextDescribe(
|
|||
'author3',
|
||||
])
|
||||
})
|
||||
|
||||
it('should pick up opengraph-image and twitter-image as static metadata files', async () => {
|
||||
const $ = await next.render$('/opengraph/static')
|
||||
expect($('[property="og:image:url"]').attr('content')).toMatch(
|
||||
/_next\/static\/media\/metadata\/opengraph-image.\w+.png/
|
||||
)
|
||||
expect($('[property="og:image:type"]').attr('content')).toBe(
|
||||
'image/png'
|
||||
)
|
||||
expect($('[property="og:image:width"]').attr('content')).toBe('114')
|
||||
expect($('[property="og:image:height"]').attr('content')).toBe('114')
|
||||
|
||||
expect($('[name="twitter:image"]').attr('content')).toMatch(
|
||||
/_next\/static\/media\/metadata\/twitter-image.\w+.png/
|
||||
)
|
||||
expect($('[name="twitter:card"]').attr('content')).toBe(
|
||||
'summary_large_image'
|
||||
)
|
||||
|
||||
// favicon shouldn't be overridden
|
||||
const $icon = $('link[rel="icon"]')
|
||||
expect($icon.attr('href')).toMatch(
|
||||
/_next\/static\/media\/metadata\/favicon.\w+.ico/
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('icons', () => {
|
||||
|
@ -477,7 +502,7 @@ createNextDescribe(
|
|||
it('should render icon and apple touch icon meta if their images are specified', async () => {
|
||||
const $ = await next.render$('/icons/static/nested')
|
||||
|
||||
const $icon = $('head > link[rel="icon"]')
|
||||
const $icon = $('head > link[rel="icon"][type!="image/x-icon"]')
|
||||
const $appleIcon = $('head > link[rel="apple-touch-icon"]')
|
||||
|
||||
expect($icon.attr('href')).toMatch(
|
||||
|
@ -495,7 +520,7 @@ createNextDescribe(
|
|||
it('should not render if image file is not specified', async () => {
|
||||
const $ = await next.render$('/icons/static')
|
||||
|
||||
const $icon = $('head > link[rel="icon"]')
|
||||
const $icon = $('head > link[rel="icon"][type!="image/x-icon"]')
|
||||
const $appleIcon = $('head > link[rel="apple-touch-icon"]')
|
||||
|
||||
expect($icon.attr('href')).toMatch(
|
||||
|
@ -515,7 +540,7 @@ createNextDescribe(
|
|||
|
||||
await check(async () => {
|
||||
const $ = await next.render$('/icons/static')
|
||||
const $icon = $('head > link[rel="icon"]')
|
||||
const $icon = $('head > link[rel="icon"][type!="image/x-icon"]')
|
||||
return $icon.attr('href')
|
||||
}, /\/_next\/static\/media\/metadata\/icon2\.\w+\.png/)
|
||||
|
||||
|
|
Loading…
Reference in a new issue