Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate fixed route path for favicon.ico #46997

Merged
merged 4 commits into from
Mar 10, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions packages/next/src/build/webpack/loaders/metadata/discover.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import type webpack from 'webpack'
import type { AppLoaderOptions } from '../next-app-loader'
import type { CollectingMetadata } from './types'
import type {
CollectingMetadata,
PossibleImageFileNameConvention,
} 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 Down Expand Up @@ -127,6 +123,7 @@ export async function discoverStaticMetadataFiles(
...metadataImageLoaderOptions,
numericSizes:
type === 'twitter' || type === 'opengraph' ? '1' : undefined,
type,
})}!` +
filepath +
METADATA_IMAGE_RESOURCE_QUERY
Expand Down
7 changes: 7 additions & 0 deletions packages/next/src/build/webpack/loaders/metadata/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@ export type MetadataImageModule = {
height?: number
}
)

export type PossibleImageFileNameConvention =
| 'icon'
| 'apple'
| 'favicon'
| 'twitter'
| 'opengraph'
Original file line number Diff line number Diff line change
@@ -1,36 +1,42 @@
import type { MetadataImageModule } from './metadata/types'
import type {
MetadataImageModule,
PossibleImageFileNameConvention,
} from './metadata/types'
import loaderUtils from 'next/dist/compiled/loader-utils3'
import { getImageSize } from '../../../server/image-optimizer'

interface Options {
isDev: boolean
assetPrefix: string
numericSizes: boolean
type: PossibleImageFileNameConvention
}

const mimeTypeMap = {
jpeg: 'image/jpeg',
png: 'image/png',
ico: 'image/x-icon',
svg: 'image/svg+xml',
avif: 'image/avif',
webp: 'image/webp',
}

async function nextMetadataImageLoader(this: any, content: Buffer) {
const options: Options = this.getOptions()
const { assetPrefix, isDev, numericSizes } = options
const { assetPrefix, isDev, numericSizes, type } = options
const context = this.rootContext

const opts = { context, content }

// favicon is the special case, always generate '/favicon.ico'
const isFavIcon = type === 'favicon'
// e.g. icon.png -> server/static/media/metadata/icon.399de3b9.png
const interpolatedName = loaderUtils.interpolateName(
this,
'/static/media/metadata/[name].[hash:8].[ext]',
(isFavIcon ? '' : '/static/media/metadata/') + '[name].[ext]',
opts
)
const outputPath = assetPrefix + '/_next' + interpolatedName
const outputPath = isFavIcon
? '/' + interpolatedName
: assetPrefix + '/_next' + interpolatedName

let extension = loaderUtils.interpolateName(this, '[ext]', opts)
if (extension === 'jpg') {
extension = 'jpeg'
Expand Down Expand Up @@ -61,7 +67,10 @@ async function nextMetadataImageLoader(this: any, content: Buffer) {

const stringifiedData = JSON.stringify(imageData)

this.emitFile(`../${isDev ? '' : '../'}${interpolatedName}`, content, null)
// TODO-METADATA: Move image generation to static app routes
if (!isFavIcon) {
this.emitFile(`../${isDev ? '' : '../'}${interpolatedName}`, content, null)
}

return `export default ${stringifiedData};`
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
import type webpack from 'webpack'
import path from 'path'

const staticFileRegex = /[\\/](robots\.txt|sitemap\.xml)/

function isStaticRoute(resourcePath: string) {
return staticFileRegex.test(resourcePath)
}
import { isStaticMetadataRoute } from '../../../lib/is-app-route-route'

function getContentType(resourcePath: string) {
const filename = path.basename(resourcePath)
const [name] = filename.split('.')
const [name, ext] = filename.split('.')
if (name === 'favicon' && ext === 'ico') return 'image/x-icon'
if (name === 'sitemap') return 'application/xml'
if (name === 'robots') return 'text/plain'
return 'text/plain'
}

const nextMetadataRouterLoader: webpack.LoaderDefinitionFunction = function (
content: string
) {
// `import.meta.url` is the resource name of the current module.
// When it's static route, it could be favicon.ico, sitemap.xml, robots.txt etc.
const nextMetadataRouterLoader: webpack.LoaderDefinitionFunction = function () {
const { resourcePath } = this

const code = isStaticRoute(resourcePath)
const code = isStaticMetadataRoute(resourcePath)
? `import { NextResponse } from 'next/server'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'

const content = ${JSON.stringify(content)}
const buffer = fs.readFileSync(fileURLToPath(import.meta.url))
const contentType = ${JSON.stringify(getContentType(resourcePath))}

export function GET() {
return new NextResponse(content, {
return new NextResponse(buffer, {
status: 200,
headers: {
'Content-Type': contentType,
Expand Down
7 changes: 6 additions & 1 deletion packages/next/src/build/webpack/stringify-request.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export function stringifyRequest(loaderContext: any, request: any) {
import type webpack from 'webpack'

export function stringifyRequest(
loaderContext: webpack.LoaderContext<any>,
request: string
) {
return JSON.stringify(
loaderContext.utils.contextify(
loaderContext.context || loaderContext.rootContext,
Expand Down
14 changes: 12 additions & 2 deletions packages/next/src/lib/is-app-route-route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import path from '../shared/lib/isomorphic/path'

export function isAppRouteRoute(route: string): boolean {
return route.endsWith('/route')
}

// TODO: support more metadata routes
const staticMetadataRoutes = ['robots.txt', 'sitemap.xml']
// Match routes that are metadata routes, e.g. /sitemap.xml, /favicon.<ext>, /<icon>.<ext>, etc.
// TODO-METADATA: support more metadata routes with more extensions
const staticMetadataRoutes = ['robots.txt', 'sitemap.xml', 'favicon.ico']
export function isMetadataRoute(route: string): boolean {
// Remove the 'app/' or '/' prefix, only check the route name since they're only allowed in root app directory
const filename = route.replace(/^app\//, '').replace(/^\//, '')
return staticMetadataRoutes.includes(filename)
}

// Only match the static metadata files
// TODO-METADATA: support static metadata files under nested routes folders
export function isStaticMetadataRoute(resourcePath: string) {
const filename = path.basename(resourcePath)
return staticMetadataRoutes.includes(filename)
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class DevAppRouteRouteMatcherProvider extends FileCacheRouteMatcherProvid
.concat('txt')
.join('|')})?$|[/\\\\]sitemap\\.(?:${extensions
.concat('xml')
.join('|')})?$`
.join('|')})?$|[/\\\\]favicon\\.ico$`
)

const pageNormalizer = new AbsoluteFilenameNormalizer(appDir, extensions)
Expand Down
28 changes: 21 additions & 7 deletions packages/next/src/server/lib/find-page-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,19 +79,33 @@ export function createValidFileMatcher(
pageExtensions: string[],
appDirPath: string | undefined
) {
const getExtensionRegexString = (
extensions: string[],
...extraExtensions: string[]
) => `(?:${extensions.concat(extraExtensions).join('|')})`

const validExtensionFileRegex = new RegExp(
`\\.+(?:${pageExtensions.join('|')})$`
'\\.' + getExtensionRegexString(pageExtensions) + '$'
)
const leafOnlyPageFileRegex = new RegExp(
`(^(page|route)|[\\\\/](page|route))\\.(?:${pageExtensions.join('|')})$`
`(^(page|route)|[\\\\/](page|route))\\.${getExtensionRegexString(
pageExtensions
)}$`
)
// TODO: support other metadata routes
// regex for /robots.txt|((j|t)sx?)
// regex for /sitemap.xml|((j|t)sx?)
/** TODO-METADATA: support other metadata routes
* regex for:
*
* /robots.txt|((j|t)sx?)
* /sitemap.xml|((j|t)sx?)
* /favicon.ico
*
*/
const metadataRoutesRelativePathRegex = new RegExp(
`^[\\\\/](robots)\\.(?:${pageExtensions.concat('txt').join('|')})$` +
`^[\\\\/]((robots\\.${getExtensionRegexString(pageExtensions, 'txt')})` +
'|' +
`(sitemap\\.${getExtensionRegexString(pageExtensions, 'xml')})` +
'|' +
`^[\\\\/](sitemap)\\.(?:${pageExtensions.concat('xml').join('|')})$`
`(favicon\\.ico))$`
)

function isMetadataRouteFile(filePath: string) {
Expand Down
24 changes: 9 additions & 15 deletions test/e2e/app-dir/metadata/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ createNextDescribe(
it('should pick up opengraph-image and twitter-image as static metadata files', async () => {
const $ = await next.render$('/opengraph/static')
expect($('[property="og:image"]').attr('content')).toMatch(
/https:\/\/example.com\/_next\/static\/media\/metadata\/opengraph-image.\w+.png/
/https:\/\/example.com\/_next\/static\/media\/metadata\/opengraph-image.png/
huozhi marked this conversation as resolved.
Show resolved Hide resolved
)
expect($('[property="og:image:type"]').attr('content')).toBe(
'image/png'
Expand All @@ -437,17 +437,15 @@ createNextDescribe(
expect($('[property="og:image:height"]').attr('content')).toBe('114')

expect($('[name="twitter:image"]').attr('content')).toMatch(
/https:\/\/example.com\/_next\/static\/media\/metadata\/twitter-image.\w+.png/
/https:\/\/example.com\/_next\/static\/media\/metadata\/twitter-image.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/
)
expect($icon.attr('href')).toBe('/favicon.ico')
})
})

Expand Down Expand Up @@ -497,17 +495,13 @@ createNextDescribe(
it('should support root level of favicon.ico', async () => {
let $ = await next.render$('/')
let $icon = $('link[rel="icon"]')
expect($icon.attr('href')).toMatch(
/_next\/static\/media\/metadata\/favicon.\w+.ico/
)
expect($icon.attr('href')).toBe('/favicon.ico')
expect($icon.attr('type')).toBe('image/x-icon')
expect($icon.attr('sizes')).toBe('any')

$ = await next.render$('/basic')
$icon = $('link[rel="icon"]')
expect($icon.attr('href')).toMatch(
/_next\/static\/media\/metadata\/favicon.\w+.ico/
)
expect($icon.attr('href')).toBe('/favicon.ico')
expect($icon.attr('sizes')).toBe('any')
})
})
Expand All @@ -520,12 +514,12 @@ createNextDescribe(
const $appleIcon = $('head > link[rel="apple-touch-icon"]')

expect($icon.attr('href')).toMatch(
/\/_next\/static\/media\/metadata\/icon1\.\w+\.png/
/\/_next\/static\/media\/metadata\/icon1\.png/
)
expect($icon.attr('sizes')).toBe('32x32')
expect($icon.attr('type')).toBe('image/png')
expect($appleIcon.attr('href')).toMatch(
/\/_next\/static\/media\/metadata\/apple-icon\.\w+\.png/
/\/_next\/static\/media\/metadata\/apple-icon\.png/
)
expect($appleIcon.attr('type')).toBe('image/png')
expect($appleIcon.attr('sizes')).toMatch('114x114')
Expand All @@ -538,7 +532,7 @@ createNextDescribe(
const $appleIcon = $('head > link[rel="apple-touch-icon"]')

expect($icon.attr('href')).toMatch(
/\/_next\/static\/media\/metadata\/icon\.\w+\.png/
/\/_next\/static\/media\/metadata\/icon\.png/
)
expect($icon.attr('sizes')).toBe('114x114')

Expand All @@ -556,7 +550,7 @@ createNextDescribe(
const $ = await next.render$('/icons/static')
const $icon = $('head > link[rel="icon"][type!="image/x-icon"]')
return $icon.attr('href')
}, /\/_next\/static\/media\/metadata\/icon2\.\w+\.png/)
}, /\/_next\/static\/media\/metadata\/icon2\.png/)

await next.renameFile(
'app/icons/static/icon2.png',
Expand Down