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

Support generate dynamic sitemaps for dynamic routes #48867

Merged
merged 6 commits into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const imageModule = { ..._imageModule }
const handler = imageModule.default
const generateImageMetadata = imageModule.generateImageMetadata

export async function GET(req, ctx) {
export async function GET(_, ctx) {
const { __metadata_id__ = [], ...params } = ctx.params
const id = __metadata_id__[0]
const imageMetadata = generateImageMetadata ? await generateImageMetadata({ params }) : null
Expand All @@ -118,6 +118,46 @@ export async function GET(req, ctx) {
`
}

function getDynamicSiteMapRouteCode(resourcePath: string) {
// generateSitemaps
return `\
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return `\
return `

\ seems wrong here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's for connecting the new line, I want to start writting code in the new line. this doesn't create \n

import { NextResponse } from 'next/server'
import * as _sitemapModule from ${JSON.stringify(resourcePath)}
import { resolveRouteData } from 'next/dist/build/webpack/loaders/metadata/resolve-route-data'

const sitemapModule = { ..._sitemapModule }
const handler = sitemapModule.default
const generateSitemaps = sitemapModule.generateSitemaps
const contentType = ${JSON.stringify(getContentType(resourcePath))}
const fileType = ${JSON.stringify(getFilenameAndExtension(resourcePath).name)}

export async function GET(_, ctx) {
const sitemaps = generateSitemaps ? await generateSitemaps() : null
let id = undefined

if (sitemaps) {
const { __metadata_id__ = [] } = ctx.params
const targetId = __metadata_id__[0]
id = sitemaps.find((item) => item.id.toString() === targetId)?.id
if (id == null) {
return new NextResponse(null, {
status: 404,
})
}
}

const data = await handler({ id })
const content = resolveRouteData(data, fileType)

return new NextResponse(content, {
headers: {
'Content-Type': contentType,
'Cache-Control': ${JSON.stringify(cacheHeader.revalidate)},
},
})
}
`
}
// `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.
// TODO-METADATA: improve the cache control strategy
Expand All @@ -131,12 +171,10 @@ const nextMetadataRouterLoader: webpack.LoaderDefinitionFunction<MetadataRouteLo

let code = ''
if (isDynamic) {
if (
fileBaseName === 'sitemap' ||
fileBaseName === 'robots' ||
fileBaseName === 'manifest'
) {
if (fileBaseName === 'robots' || fileBaseName === 'manifest') {
code = getDynamicTextRouteCode(resourcePath)
} else if (fileBaseName === 'sitemap') {
code = getDynamicSiteMapRouteCode(resourcePath)
} else {
code = getDynamicImageRouteCode(resourcePath)
}
Expand Down
33 changes: 18 additions & 15 deletions packages/next/src/lib/metadata/get-metadata-route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isMetadataRoute, isMetadataRouteFile } from './is-metadata-route'
import path from '../../shared/lib/isomorphic/path'
import { djb2Hash } from '../../shared/lib/hash'
import { isDynamicRoute } from '../../shared/lib/router/utils'

/*
* If there's special convention like (...) or @ in the page path,
Expand Down Expand Up @@ -30,36 +31,38 @@ export function getMetadataRouteSuffix(page: string) {
*/
export function normalizeMetadataRoute(page: string) {
let route = page
let suffix = ''
if (isMetadataRoute(page)) {
// Remove the file extension, e.g. /route-path/robots.txt -> /route-path
const pathnamePrefix = page.slice(0, -(path.basename(page).length + 1))
const suffix = getMetadataRouteSuffix(pathnamePrefix)

if (route === '/sitemap') {
route += '.xml'
}
if (route === '/robots') {
route += '.txt'
}
if (route === '/manifest') {
} else if (route === '/manifest') {
route += '.webmanifest'
} else if (route.endsWith('/sitemap')) {
route += '.xml'
} else {
// Remove the file extension, e.g. /route-path/robots.txt -> /route-path
const pathnamePrefix = page.slice(0, -(path.basename(page).length + 1))
suffix = getMetadataRouteSuffix(pathnamePrefix)
}
// Support both /<metadata-route.ext> and custom routes /<metadata-route>/route.ts.
// If it's a metadata file route, we need to append /[id]/route to the page.
if (!route.endsWith('/route')) {
const isStaticMetadataFile = isMetadataRouteFile(route, [], true)
const { dir, name: baseName, ext } = path.parse(route)

const isSingleRoute =
page.startsWith('/sitemap') ||
page.startsWith('/robots') ||
page.startsWith('/manifest') ||
isStaticMetadataFile
// If it's dynamic routes, we need to append [[...__metadata_id__]] to the page;
// If it's static routes, we need to append nothing to the page.
// If its special routes like robots.txt and manifest.webmanifest, we leave them as static routes.
const isStaticRoute =
!isDynamicRoute(route) &&
(page.startsWith('/robots') ||
page.startsWith('/manifest') ||
isStaticMetadataFile)

route = path.posix.join(
dir,
`${baseName}${suffix ? `-${suffix}` : ''}${ext}`,
isSingleRoute ? '' : '[[...__metadata_id__]]',
isStaticRoute ? '' : '[[...__metadata_id__]]',
'route'
)
}
Expand Down
15 changes: 7 additions & 8 deletions packages/next/src/lib/metadata/is-metadata-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,6 @@ export function isMetadataRouteFile(
: ''
}`
),
new RegExp(
`^[\\\\/]sitemap${
withExtension
? `\\.${getExtensionRegexString(pageExtensions.concat('xml'))}`
: ''
}`
),
new RegExp(
`^[\\\\/]manifest${
withExtension
Expand All @@ -64,7 +57,13 @@ export function isMetadataRouteFile(
}`
),
new RegExp(`^[\\\\/]favicon\\.ico$`),
// TODO-METADATA: add dynamic routes for metadata images
new RegExp(
`[\\\\/]sitemap${
withExtension
? `\\.${getExtensionRegexString(pageExtensions.concat('xml'))}`
: ''
}`
),
new RegExp(
`[\\\\/]${STATIC_METADATA_IMAGES.icon.filename}${
withExtension
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MetadataRoute } from 'next'

export async function generateSitemaps() {
return [{ id: 0 }, { id: 1 }, { id: 2 }, { id: 3 }]
}

export default function sitemap({ id }): MetadataRoute.Sitemap {
return [
{
url: `https://example.com/dynamic/${id}`,
lastModified: '2021-01-01',
},
{
url: `https://example.com/dynamic/${id}/about`,
lastModified: '2021-01-01',
},
]
}
14 changes: 14 additions & 0 deletions test/e2e/app-dir/metadata-dynamic-routes/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,20 @@ createNextDescribe(
])
})

it('should support generate multi sitemaps with generateSitemaps', async () => {
const ids = [0, 1, 2, 3]
function fetchSitemap(id) {
return next
.fetch(`/dynamic/small/sitemap.xml/${id}`)
.then((res) => res.text())
}

for (const id of ids) {
const text = await fetchSitemap(id)
expect(text).toContain(`<loc>https://example.com/dynamic/${id}</loc>`)
}
})

it('should fill params into dynamic routes url of metadata images', async () => {
const $ = await next.render$('/dynamic/big')
const ogImageUrl = $('meta[property="og:image"]').attr('content')
Expand Down
24 changes: 0 additions & 24 deletions test/e2e/app-dir/metadata-dynamic-routes/tsconfig.json

This file was deleted.

4 changes: 2 additions & 2 deletions test/e2e/app-dir/metadata/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,7 @@ createNextDescribe(
expect(invalidRobotsResponse.status).toBe(404)
})

it('should support root dir sitemap.xml', async () => {
it('should support sitemap.xml under every routes', async () => {
const res = await next.fetch('/sitemap.xml')
expect(res.headers.get('content-type')).toBe('application/xml')
const sitemap = await res.text()
Expand All @@ -753,7 +753,7 @@ createNextDescribe(
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
)
const invalidSitemapResponse = await next.fetch('/title/sitemap.xml')
expect(invalidSitemapResponse.status).toBe(404)
expect(invalidSitemapResponse.status).toBe(200)
})

it('should support static manifest.webmanifest', async () => {
Expand Down
2 changes: 1 addition & 1 deletion test/unit/find-page-file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe('createPageFileMatcher', () => {
expect(fileMatcher.isMetadataFile('app/path/robots.txt')).toBe(false)

expect(fileMatcher.isMetadataFile('app/sitemap.xml')).toBe(true)
expect(fileMatcher.isMetadataFile('app/path/sitemap.xml')).toBe(false)
expect(fileMatcher.isMetadataFile('app/path/sitemap.xml')).toBe(true)
})
})
})