Skip to content

Commit

Permalink
Static og and twitter image files as metadata (#45797)
Browse files Browse the repository at this point in the history
## 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)
  • Loading branch information
huozhi authored Feb 13, 2023
1 parent 8fb2f59 commit a5fe64c
Show file tree
Hide file tree
Showing 17 changed files with 213 additions and 101 deletions.
2 changes: 1 addition & 1 deletion packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
11 changes: 0 additions & 11 deletions packages/next/src/build/webpack/loaders/app-dir/types.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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.)
Expand Down Expand Up @@ -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 = {
Expand All @@ -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,
Expand All @@ -105,30 +122,34 @@ 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)
}
})
}

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(
Expand All @@ -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 changes: 33 additions & 0 deletions packages/next/src/build/webpack/loaders/metadata/types.ts
Original file line number Diff line number Diff line change
@@ -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
}
)
4 changes: 2 additions & 2 deletions packages/next/src/build/webpack/loaders/next-app-loader.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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")

Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
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 = {
jpeg: 'image/jpeg',
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 }
Expand All @@ -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)

Expand Down
13 changes: 0 additions & 13 deletions packages/next/src/lib/metadata/metadata-context.ts

This file was deleted.

Loading

0 comments on commit a5fe64c

Please sign in to comment.