Skip to content

Commit

Permalink
Error for missing dynamic generated id and refactor metadata image url (
Browse files Browse the repository at this point in the history
#48953)

### What?

In #48928 we decided to error for the missing `id` from `generateImageMetadata` and `generateSitemaps` for better dev DX. This PR also refactors the metadata image urls generation that assumbling the utils together
  • Loading branch information
huozhi committed Apr 28, 2023
1 parent cc2bcec commit a141366
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 138 deletions.
2 changes: 0 additions & 2 deletions packages/next/src/build/webpack/loaders/metadata/discover.ts
Expand Up @@ -6,7 +6,6 @@ import type {
import path from 'path'
import { stringify } from 'querystring'
import { STATIC_METADATA_IMAGES } from '../../../../lib/metadata/is-metadata-route'
import { normalizeAppPath } from '../../../../shared/lib/router/utils/app-paths'

const METADATA_TYPE = 'metadata'

Expand Down Expand Up @@ -125,7 +124,6 @@ export async function createStaticMetadataFromRoute(
{
type,
segment,
route: normalizeAppPath(segment),
pageExtensions,
}
)}!${filepath}${METADATA_RESOURCE_QUERY}`
Expand Down
Expand Up @@ -14,15 +14,14 @@ import { imageExtMimeTypeMap } from '../../../lib/mime-type'
import { fileExists } from '../../../lib/file-exists'

interface Options {
route: string
segment: string
type: PossibleImageFileNameConvention
pageExtensions: string[]
}

async function nextMetadataImageLoader(this: any, content: Buffer) {
const options: Options = this.getOptions()
const { type, route, segment, pageExtensions } = options
const { type, segment, pageExtensions } = options
const numericSizes = type === 'twitter' || type === 'openGraph'
const { resourcePath, rootContext: context } = this
const { name: fileNameBase, ext } = path.parse(resourcePath)
Expand All @@ -47,36 +46,31 @@ async function nextMetadataImageLoader(this: any, content: Buffer) {
)

const isDynamicResource = pageExtensions.includes(extension)
const pageRoute = isDynamicResource ? fileNameBase : interpolatedName
const pageSegment = isDynamicResource ? fileNameBase : interpolatedName
const hashQuery = contentHash ? '?' + contentHash : ''

if (isDynamicResource) {
// re-export and spread as `exportedImageData` to avoid non-exported error
return `\
import path from 'next/dist/shared/lib/isomorphic/path'
import * as exported from ${JSON.stringify(resourcePath)}
import { interpolateDynamicPath } from 'next/dist/server/server-utils'
import { getNamedRouteRegex } from 'next/dist/shared/lib/router/utils/route-regex'
import { getMetadataRouteSuffix } from 'next/dist/lib/metadata/get-metadata-route'
import { fillMetadataSegment } from 'next/dist/lib/metadata/get-metadata-route'
const imageModule = { ...exported }
export default async function (props) {
const pathname = ${JSON.stringify(route)}
const routeRegex = getNamedRouteRegex(pathname, false)
const segment = ${JSON.stringify(segment)}
const { __metadata_id__: _, ...params } = props.params
const route = interpolateDynamicPath(pathname, params, routeRegex)
const suffix = getMetadataRouteSuffix(segment)
const routeSuffix = suffix ? \`-\${suffix}\` : ''
const imageRoute = ${JSON.stringify(pageRoute)} + routeSuffix
const imageUrl = fillMetadataSegment(${JSON.stringify(
segment
)}, params, ${JSON.stringify(pageSegment)})
const { generateImageMetadata } = imageModule
function getImageMetadata(imageMetadata, segment) {
function getImageMetadata(imageMetadata, idParam) {
const data = {
alt: imageMetadata.alt,
type: imageMetadata.contentType || 'image/png',
url: path.join(route, segment + ${JSON.stringify(hashQuery)}),
url: imageUrl + (idParam ? ('/' + idParam) : '') + ${JSON.stringify(
hashQuery
)},
}
const { size } = imageMetadata
if (size) {
Expand All @@ -92,11 +86,11 @@ async function nextMetadataImageLoader(this: any, content: Buffer) {
if (generateImageMetadata) {
const imageMetadataArray = await generateImageMetadata({ params })
return imageMetadataArray.map((imageMetadata, index) => {
const segment = path.join(imageRoute, (imageMetadata.id || index) + '')
return getImageMetadata(imageMetadata, segment)
const idParam = (imageMetadata.id || index) + ''
return getImageMetadata(imageMetadata, idParam)
})
} else {
return [getImageMetadata(imageModule, imageRoute)]
return [getImageMetadata(imageModule, '')]
}
}`
}
Expand Down Expand Up @@ -137,27 +131,17 @@ async function nextMetadataImageLoader(this: any, content: Buffer) {
}

return `\
import path from 'next/dist/shared/lib/isomorphic/path'
import { interpolateDynamicPath } from 'next/dist/server/server-utils'
import { getNamedRouteRegex } from 'next/dist/shared/lib/router/utils/route-regex'
import { getMetadataRouteSuffix } from 'next/dist/lib/metadata/get-metadata-route'
import { fillMetadataSegment } from 'next/dist/lib/metadata/get-metadata-route'
export default (props) => {
const pathname = ${JSON.stringify(route)}
const routeRegex = getNamedRouteRegex(pathname, false)
const segment = ${JSON.stringify(segment)}
const route = interpolateDynamicPath(pathname, props.params, routeRegex)
const suffix = getMetadataRouteSuffix(segment)
const routeSuffix = suffix ? \`-\${suffix}\` : ''
const { name, ext } = path.parse(${JSON.stringify(pageRoute)})
const imageData = ${JSON.stringify(imageData)};
const imageData = ${JSON.stringify(imageData)}
const imageUrl = fillMetadataSegment(${JSON.stringify(
segment
)}, props.params, ${JSON.stringify(pageSegment)})
return [{
...imageData,
url: path.join(route, name + routeSuffix + ext + ${JSON.stringify(
type === 'favicon' ? '' : hashQuery
)}),
url: imageUrl + ${JSON.stringify(type === 'favicon' ? '' : hashQuery)},
}]
}`
}
Expand Down
Expand Up @@ -103,11 +103,20 @@ const generateImageMetadata = imageModule.generateImageMetadata
export async function GET(_, ctx) {
const { __metadata_id__ = [], ...params } = ctx.params
const id = __metadata_id__[0]
const targetId = __metadata_id__[0]
let id = undefined
const imageMetadata = generateImageMetadata ? await generateImageMetadata({ params }) : null
if (imageMetadata) {
const hasId = imageMetadata.some((item) => item.id.toString() === id)
if (!hasId) {
id = imageMetadata.find((item) => {
if (process.env.NODE_ENV !== 'production') {
if (item?.id == null) {
throw new Error('id is required for every item returned from generateImageMetadata')
}
}
return item.id.toString() === targetId
})?.id
if (id == null) {
return new NextResponse('Not Found', {
status: 404,
})
Expand All @@ -132,13 +141,20 @@ const contentType = ${JSON.stringify(getContentType(resourcePath))}
const fileType = ${JSON.stringify(getFilenameAndExtension(resourcePath).name)}
export async function GET(_, ctx) {
const sitemaps = generateSitemaps ? await generateSitemaps() : null
const { __metadata_id__ = [], ...params } = ctx.params
const targetId = __metadata_id__[0]
let id = undefined
const sitemaps = generateSitemaps ? await generateSitemaps() : null
if (sitemaps) {
const { __metadata_id__ = [] } = ctx.params
const targetId = __metadata_id__[0]
id = sitemaps.find((item) => item.id.toString() === targetId)?.id
id = sitemaps.find((item) => {
if (process.env.NODE_ENV !== 'production') {
if (item?.id == null) {
throw new Error('id property is required for every item returned from generateSitemaps')
}
}
return item.id.toString() === targetId
})?.id
if (id == null) {
return new NextResponse('Not Found', {
status: 404,
Expand Down
28 changes: 27 additions & 1 deletion packages/next/src/lib/metadata/get-metadata-route.ts
@@ -1,6 +1,9 @@
import { isMetadataRoute, isMetadataRouteFile } from './is-metadata-route'
import path from '../../shared/lib/isomorphic/path'
import { interpolateDynamicPath } from '../../server/server-utils'
import { getNamedRouteRegex } from '../../shared/lib/router/utils/route-regex'
import { djb2Hash } from '../../shared/lib/hash'
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'

/*
* If there's special convention like (...) or @ in the page path,
Expand All @@ -10,7 +13,7 @@ import { djb2Hash } from '../../shared/lib/hash'
* /app/open-graph.tsx -> /open-graph/route
* /app/(post)/open-graph.tsx -> /open-graph/route-[0-9a-z]{6}
*/
export function getMetadataRouteSuffix(page: string) {
function getMetadataRouteSuffix(page: string) {
let suffix = ''

if ((page.includes('(') && page.includes(')')) || page.includes('@')) {
Expand All @@ -19,6 +22,29 @@ export function getMetadataRouteSuffix(page: string) {
return suffix
}

/**
* Fill the dynamic segment in the metadata route
*
* Example:
* fillMetadataSegment('/a/[slug]', { params: { slug: 'b' } }, 'open-graph') -> '/a/b/open-graph'
*
*/
export function fillMetadataSegment(
segment: string,
params: any,
imageSegment: string
) {
const pathname = normalizeAppPath(segment)
const routeRegex = getNamedRouteRegex(pathname, false)
const route = interpolateDynamicPath(pathname, params, routeRegex)
const suffix = getMetadataRouteSuffix(segment)
const routeSuffix = suffix ? `-${suffix}` : ''

const { name, ext } = path.parse(imageSegment)

return path.join(route, `${name}${routeSuffix}${ext}`)
}

/**
* Map metadata page key to the corresponding route
*
Expand Down

0 comments on commit a141366

Please sign in to comment.