pageExtensions
Sets which file extensions Next.js treats as routable pages and convention files.
pageExtensions?: string[]Parameters
| Parameter | Type | Description |
|---|---|---|
$pageExtensionsoptional | string[]= ['tsx', 'ts', 'jsx', 'js'] | Extensions, written without a leading dot, that Next.js resolves as routes and file conventions. The array replaces the default rather than extending it, so any default you still need must be listed again. |
Two opposite jobs are done by this one array. Adding extensions makes new file types routable — md and mdx so app/about/page.mdx becomes a route. Narrowing it to something like ['page.tsx'] lets you colocate tests, stories and helpers beside a route without Next.js trying to route them.
What trips people up
The array replaces the default; it does not merge. Write pageExtensions: ['mdx'] and every .tsx route in the project stops existing, which surfaces as a project-wide 404 rather than an error. Always restate the defaults you still need.
The second surprise is reach: this governs all file conventions, not just page. Narrow the list to page.ts and your proxy.ts stops being picked up — it must be renamed proxy.page.ts, and the same rule applies to layout, route and the other conventions. Silent, because a proxy that is never loaded looks exactly like a proxy whose matcher does not match.
Example
import createMDX from '@next/mdx'
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
// defaults restated, then md and mdx added
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
}
const withMDX = createMDX()
export default withMDX(nextConfig)When to use it
- Author content routes as MDX alongside normal TSX pages
- Colocate route.test.tsx and helpers in a route folder without them becoming routes
- Adopt a .page.tsx suffix convention across a large codebase
Frequently asked questions
Does pageExtensions add to the defaults or replace them?
It replaces them. The default is ['tsx', 'ts', 'jsx', 'js'], so extending the list means restating those alongside your addition. Listing only 'mdx' makes every TSX route stop resolving, with a 404 rather than a build error.
Why did my proxy file stop running after I changed pageExtensions?
Because the setting governs convention files too. With a suffix scheme such as .page.ts, the root file must be named proxy.page.ts to be picked up. Under the old name it is simply not loaded, and nothing warns you.
How do I keep test files next to my routes?
Narrow pageExtensions to a distinct suffix such as ['page.tsx', 'page.ts']. Only files matching it are treated as routes, so page.test.tsx and helper modules can live in the same folder without being routed.
More examples
A suffix scheme, and the files it renames
Now only files ending in .page.tsx or .page.ts are resolved, so app/blog/page.page.tsx is the route and app/blog/page.test.tsx is ignored by the router. The catch is that every convention follows the same rule: layout.tsx must become layout.page.tsx, and the root proxy.ts must be renamed proxy.page.ts. A proxy that is no longer matched by pageExtensions is simply never loaded — no warning, no error.
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
pageExtensions: ['page.tsx', 'page.ts'],
}
export default nextConfigThe mistake that 404s the whole app
The first config leaves Next.js resolving only .mdx files, so a project of TSX routes builds successfully and then 404s on every path. Because nothing errors, the cause is easy to miss. The default list is ['tsx', 'ts', 'jsx', 'js'] and must be written out again whenever you extend it.
import type { NextConfig } from 'next'
// WRONG: replaces the defaults, so every .tsx route disappears
const broken: NextConfig = {
pageExtensions: ['mdx'],
}
// RIGHT: defaults restated alongside the addition
const nextConfig: NextConfig = {
pageExtensions: ['tsx', 'ts', 'jsx', 'js', 'mdx'],
}
export default nextConfig
Discussion