diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index 4085e911712a6..0f1005e78a4ef 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -67,7 +67,10 @@ import { isInternalComponent, isNonRoutePagesPage, } from '../lib/is-internal-component' -import { isMetadataRoute } from '../lib/metadata/is-metadata-route' +import { + isMetadataRoute, + isMetadataRouteFile, +} from '../lib/metadata/is-metadata-route' import { RouteKind } from '../server/route-kind' import { encodeToBase64 } from './webpack/loaders/utils' import { normalizeCatchAllRoutes } from './normalize-catchall-routes' @@ -267,7 +270,11 @@ export async function createPagesMapping({ let route = pagesType === 'app' ? normalizeMetadataRoute(pageKey) : pageKey - if (isMetadataRoute(route) && pagesType === 'app') { + if ( + isMetadataRoute(route) && + pagesType === 'app' && + isMetadataRouteFile(pagePath, pageExtensions, true) + ) { const filePath = join(appDir!, pagePath) const staticInfo = await getPageStaticInfo({ nextConfig: {}, diff --git a/packages/next/src/server/dev/hot-reloader-turbopack.ts b/packages/next/src/server/dev/hot-reloader-turbopack.ts index 6f5944e680cf4..5eb5f61c0aef0 100644 --- a/packages/next/src/server/dev/hot-reloader-turbopack.ts +++ b/packages/next/src/server/dev/hot-reloader-turbopack.ts @@ -84,6 +84,7 @@ import { generateEncryptionKeyBase64 } from '../app-render/encryption-utils-serv import { isAppPageRouteDefinition } from '../route-definitions/app-page-route-definition' import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths' import { getNodeDebugType } from '../lib/utils' +import { isMetadataRouteFile } from '../../lib/metadata/is-metadata-route' // import { getSupportedBrowsers } from '../../build/utils' const wsServer = new ws.Server({ noServer: true }) @@ -936,10 +937,17 @@ export async function createHotReloaderTurbopack( } const isInsideAppDir = routeDef.bundlePath.startsWith('app/') - const normalizedAppPage = normalizedPageToTurbopackStructureRoute( - page, - extname(routeDef.filename) + const isEntryMetadataRouteFile = isMetadataRouteFile( + routeDef.filename.replace(opts.appDir || '', ''), + nextConfig.pageExtensions, + true ) + const normalizedAppPage = isEntryMetadataRouteFile + ? normalizedPageToTurbopackStructureRoute( + page, + extname(routeDef.filename) + ) + : page const route = isInsideAppDir ? currentEntrypoints.app.get(normalizedAppPage) diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index 77676f1406908..1c1eb0c68ec88 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -86,7 +86,10 @@ import { ModuleBuildError, TurbopackInternalError, } from '../../dev/turbopack-utils' -import { isMetadataRoute } from '../../../lib/metadata/is-metadata-route' +import { + isMetadataRoute, + isMetadataRouteFile, +} from '../../../lib/metadata/is-metadata-route' import { normalizeMetadataPageToRoute } from '../../../lib/metadata/get-metadata-route' import { createEnvDefinitions } from '../experimental/create-env-definitions' import { JsConfigPathsPlugin } from '../../../build/webpack/plugins/jsconfig-paths-plugin' @@ -429,7 +432,15 @@ async function startWatcher(opts: SetupOpts) { pagesType: isAppPath ? PAGE_TYPES.APP : PAGE_TYPES.PAGES, }) - if (isAppPath && isMetadataRoute(pageName)) { + if ( + isAppPath && + isMetadataRoute(pageName) && + isMetadataRouteFile( + fileName.replace(appDir!, ''), + nextConfig.pageExtensions, + true + ) + ) { const staticInfo = await getPageStaticInfo({ pageFilePath: fileName, nextConfig: {}, diff --git a/packages/next/src/server/route-matcher-providers/dev/dev-app-route-route-matcher-provider.ts b/packages/next/src/server/route-matcher-providers/dev/dev-app-route-route-matcher-provider.ts index 1a236aa8406a7..478d522a30d84 100644 --- a/packages/next/src/server/route-matcher-providers/dev/dev-app-route-route-matcher-provider.ts +++ b/packages/next/src/server/route-matcher-providers/dev/dev-app-route-route-matcher-provider.ts @@ -7,9 +7,11 @@ import { isAppRouteRoute } from '../../../lib/is-app-route-route' import { DevAppNormalizers } from '../../normalizers/built/app' import { isMetadataRoute, + isMetadataRouteFile, isStaticMetadataRoute, } from '../../../lib/metadata/is-metadata-route' import { normalizeMetadataPageToRoute } from '../../../lib/metadata/get-metadata-route' +import path from '../../../shared/lib/isomorphic/path' export class DevAppRouteRouteMatcherProvider extends FileCacheRouteMatcherProvider { private readonly normalizers: { @@ -17,6 +19,7 @@ export class DevAppRouteRouteMatcherProvider extends FileCacheRouteMatcherProvid pathname: Normalizer bundlePath: Normalizer } + private readonly appDir: string constructor( appDir: string, @@ -25,6 +28,7 @@ export class DevAppRouteRouteMatcherProvider extends FileCacheRouteMatcherProvid ) { super(appDir, reader) + this.appDir = appDir this.normalizers = new DevAppNormalizers(appDir, extensions) } @@ -43,8 +47,18 @@ export class DevAppRouteRouteMatcherProvider extends FileCacheRouteMatcherProvid const pathname = this.normalizers.pathname.normalize(filename) const bundlePath = this.normalizers.bundlePath.normalize(filename) + const ext = path.extname(filename).slice(1) + const isEntryMetadataRouteFile = isMetadataRouteFile( + filename.replace(this.appDir, ''), + [ext], + true + ) - if (isMetadataRoute(page) && !isStaticMetadataRoute(page)) { + if ( + isMetadataRoute(page) && + !isStaticMetadataRoute(page) && + isEntryMetadataRouteFile + ) { // Matching dynamic metadata routes. // Add 2 possibilities for both single and multiple routes: { @@ -59,7 +73,7 @@ export class DevAppRouteRouteMatcherProvider extends FileCacheRouteMatcherProvid const metadataBundlePath = normalizeMetadataPageToRoute( bundlePath, false - ) // this.normalizers.bundlePath.normalize(dummyFilename) + ) const matcher = new AppRouteRouteMatcher({ kind: RouteKind.APP_ROUTE, diff --git a/test/e2e/app-dir/metadata-non-standard-custom-routes/app/layout.tsx b/test/e2e/app-dir/metadata-non-standard-custom-routes/app/layout.tsx new file mode 100644 index 0000000000000..888614deda3ba --- /dev/null +++ b/test/e2e/app-dir/metadata-non-standard-custom-routes/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/metadata-non-standard-custom-routes/app/page.tsx b/test/e2e/app-dir/metadata-non-standard-custom-routes/app/page.tsx new file mode 100644 index 0000000000000..ff7159d9149fe --- /dev/null +++ b/test/e2e/app-dir/metadata-non-standard-custom-routes/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/e2e/app-dir/metadata-non-standard-custom-routes/app/sitemap/route.ts b/test/e2e/app-dir/metadata-non-standard-custom-routes/app/sitemap/route.ts new file mode 100644 index 0000000000000..4532d708c1968 --- /dev/null +++ b/test/e2e/app-dir/metadata-non-standard-custom-routes/app/sitemap/route.ts @@ -0,0 +1,20 @@ +// custom /sitemap route, with xml content +export function GET() { + return new Response( + ` + + + https://example.com + 2021-01-01 + weekly + 0.5 + + + `.trim(), + { + headers: { + 'Content-Type': 'application/xml', + }, + } + ) +} diff --git a/test/e2e/app-dir/metadata-non-standard-custom-routes/metadata-non-standard-custom-routes.test.ts b/test/e2e/app-dir/metadata-non-standard-custom-routes/metadata-non-standard-custom-routes.test.ts new file mode 100644 index 0000000000000..dabf46ab997bd --- /dev/null +++ b/test/e2e/app-dir/metadata-non-standard-custom-routes/metadata-non-standard-custom-routes.test.ts @@ -0,0 +1,14 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('app-dir - metadata-non-standard-custom-routes', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should work with custom sitemap route', async () => { + const res = await next.fetch('/sitemap') + expect(res.status).toBe(200) + expect(res.headers.get('content-type')).toBe('application/xml') + expect(await res.text()).toMatchInlineSnapshot(``) + }) +})