Link to the code that reproduces this issue
https://github.com/KinjiKawaguchi/nextjs-font-preload-multi-dot-repro
To Reproduce
- Clone the reproduction repository and install (
pnpm install).
- Build and start (
pnpm build && pnpm start).
curl -s http://localhost:3000/ | grep 'rel="preload".*font'
- Nothing prints — no
<link rel="preload" as="font"> tag is present in the HTML.
- To verify the working case, check out the
control branch (git checkout control) and repeat steps 2 and 3. The control branch removes the pageExtensions config and renames the files back to layout.tsx / page.tsx. Font preload tags now appear in the HTML.
Current vs. Expected behavior
Current behavior
With pageExtensions: ['page.tsx'], <link rel="preload" as="font"> tags are not emitted for fonts imported via next/font on any statically prerendered route. CSS and JS preload tags for the same route are still emitted correctly, so the omission is specific to fonts. The browser only discovers the font URLs after parsing CSS @font-face rules, which delays LCP for any text using the font.
Expected behavior
Font preload tags should be emitted regardless of the pageExtensions value. pageExtensions is a single top-level next.config.js option that applies to both the app and pages directories, and the pageExtensions docs list this exact multi-dot pattern as supported:
Then, rename your pages to have a file extension that includes .page (e.g. rename MyPage.tsx to MyPage.page.tsx).
module.exports = {
pageExtensions: ['page.tsx', 'page.ts', 'page.jsx', 'page.js'],
}
The issue does not reproduce when
pageExtensions is left at its default (font preload tags are emitted normally).
pageExtensions is set to a single-extension value like ['tsx'].
- The same file uses a single-dot extension (
layout.tsx → font preload OK; layout.page.tsx → font preload missing).
CSS and JS preload continue to work in every case. The omission is specific to next/font.
Provide environment information
Operating System:
Platform: linux
Arch: x64
Version: #85-Ubuntu SMP PREEMPT_DYNAMIC Thu Sep 18 15:26:59 UTC 2025
Available memory (MB): 1547759
Available CPU cores: 32
Binaries:
Node: 24.15.0
npm: 11.12.1
Yarn: N/A
pnpm: 10.33.0
Relevant Packages:
next: 16.3.0-canary.76 // Latest available version is detected (16.3.0-canary.76).
eslint-config-next: N/A
react: 19.2.7
react-dom: 19.2.7
typescript: 5.9.3
Next.js Config:
output: N/A
Which area(s) are affected? (Select all that apply)
Font (next/font), Webpack, Turbopack
Which stage(s) are affected? (Select all that apply)
next build (local), next start (local), Vercel (Deployed)
Additional context
Digging into the source, the two internal manifests emit keys in different formats for the same source file. entryCSSFiles / entryJSFiles (from flight-manifest-plugin) produce keys like [project]/app/layout.page, where only the final .tsx has been stripped from layout.page.tsx. next-font-manifest.app (from next-font-manifest-plugin) produces keys like [project]/app/page, where all extensions have been stripped so .page is gone.
At runtime both the CSS/JS preload and font preload paths normalize the source path with the same regex.
filePath.replace(/\.[^.]+$/, '')
That strips exactly one extension (.tsx), producing layout.page / page.page. CSS/JS lookups succeed because the manifest key layout.page matches. Font lookups fail because the manifest key page does not match page.page, so getPreloadableFonts returns null and no preload tag is emitted.
The relevant files (paths from packages/next/src) are server/app-render/get-preloadable-fonts.tsx (font lookup regex), server/app-render/get-css-inlined-link-tags.tsx (CSS/JS lookup with the same regex), build/webpack/plugins/next-font-manifest-plugin.ts (font manifest key), and build/webpack/plugins/flight-manifest-plugin.ts (CSS/JS manifest key).
Aligning the font manifest key format with the CSS/JS manifest key format (i.e. retain the pre-.tsx stem) looks like the smallest diff. I have not verified this in a PR yet; happy to send one if maintainers agree with the direction.
Related issues
I searched existing issues for getPreloadableFonts, pageExtensions font preload, next-font-manifest, and multi-dot pageExtensions, and did not find a direct duplicate.
#62332 ("Font preload not working as expected") has the same symptom of font preload tags missing, but none of the sub-reports there involve pageExtensions. Reported conditions include Windows-specific behavior, a VERCEL_CLI_VERSION regression around 33.5.1, a next dev --turbo regression in 15.4.4, and next/font/local on default pageExtensions. The reproduction in this issue is independent of all of those. It happens on Linux, on next start without the Vercel CLI, without Turbopack in dev, using next/font/google, and cleanly toggles on and off with the pageExtensions value. If maintainers consider this a facet of #62332 rather than a distinct bug, feel free to close and I will move the analysis there as a comment.
#53473 / #91746 report the no-html-link-for-pages ESLint rule not respecting custom pageExtensions. Different feature area, but the same underlying pattern of a Next.js internal hardcoding single-dot extension assumptions. Flagging in case there is a shared fix opportunity.
Link to the code that reproduces this issue
https://github.com/KinjiKawaguchi/nextjs-font-preload-multi-dot-repro
To Reproduce
pnpm install).pnpm build && pnpm start).curl -s http://localhost:3000/ | grep 'rel="preload".*font'<link rel="preload" as="font">tag is present in the HTML.controlbranch (git checkout control) and repeat steps 2 and 3. Thecontrolbranch removes thepageExtensionsconfig and renames the files back tolayout.tsx/page.tsx. Font preload tags now appear in the HTML.Current vs. Expected behavior
Current behavior
With
pageExtensions: ['page.tsx'],<link rel="preload" as="font">tags are not emitted for fonts imported vianext/fonton any statically prerendered route. CSS and JS preload tags for the same route are still emitted correctly, so the omission is specific to fonts. The browser only discovers the font URLs after parsing CSS@font-facerules, which delays LCP for any text using the font.Expected behavior
Font preload tags should be emitted regardless of the
pageExtensionsvalue.pageExtensionsis a single top-levelnext.config.jsoption that applies to both theappandpagesdirectories, and the pageExtensions docs list this exact multi-dot pattern as supported:The issue does not reproduce when
pageExtensionsis left at its default (font preload tags are emitted normally).pageExtensionsis set to a single-extension value like['tsx'].layout.tsx→ font preload OK;layout.page.tsx→ font preload missing).CSS and JS preload continue to work in every case. The omission is specific to
next/font.Provide environment information
Operating System: Platform: linux Arch: x64 Version: #85-Ubuntu SMP PREEMPT_DYNAMIC Thu Sep 18 15:26:59 UTC 2025 Available memory (MB): 1547759 Available CPU cores: 32 Binaries: Node: 24.15.0 npm: 11.12.1 Yarn: N/A pnpm: 10.33.0 Relevant Packages: next: 16.3.0-canary.76 // Latest available version is detected (16.3.0-canary.76). eslint-config-next: N/A react: 19.2.7 react-dom: 19.2.7 typescript: 5.9.3 Next.js Config: output: N/AWhich area(s) are affected? (Select all that apply)
Font (next/font), Webpack, Turbopack
Which stage(s) are affected? (Select all that apply)
next build (local), next start (local), Vercel (Deployed)
Additional context
Digging into the source, the two internal manifests emit keys in different formats for the same source file.
entryCSSFiles/entryJSFiles(fromflight-manifest-plugin) produce keys like[project]/app/layout.page, where only the final.tsxhas been stripped fromlayout.page.tsx.next-font-manifest.app(fromnext-font-manifest-plugin) produces keys like[project]/app/page, where all extensions have been stripped so.pageis gone.At runtime both the CSS/JS preload and font preload paths normalize the source path with the same regex.
That strips exactly one extension (
.tsx), producinglayout.page/page.page. CSS/JS lookups succeed because the manifest keylayout.pagematches. Font lookups fail because the manifest keypagedoes not matchpage.page, sogetPreloadableFontsreturns null and no preload tag is emitted.The relevant files (paths from
packages/next/src) areserver/app-render/get-preloadable-fonts.tsx(font lookup regex),server/app-render/get-css-inlined-link-tags.tsx(CSS/JS lookup with the same regex),build/webpack/plugins/next-font-manifest-plugin.ts(font manifest key), andbuild/webpack/plugins/flight-manifest-plugin.ts(CSS/JS manifest key).Aligning the font manifest key format with the CSS/JS manifest key format (i.e. retain the pre-
.tsxstem) looks like the smallest diff. I have not verified this in a PR yet; happy to send one if maintainers agree with the direction.Related issues
I searched existing issues for
getPreloadableFonts,pageExtensions font preload,next-font-manifest, andmulti-dot pageExtensions, and did not find a direct duplicate.#62332 ("Font preload not working as expected") has the same symptom of font preload tags missing, but none of the sub-reports there involve
pageExtensions. Reported conditions include Windows-specific behavior, aVERCEL_CLI_VERSIONregression around 33.5.1, anext dev --turboregression in 15.4.4, andnext/font/localon defaultpageExtensions. The reproduction in this issue is independent of all of those. It happens on Linux, onnext startwithout the Vercel CLI, without Turbopack in dev, usingnext/font/google, and cleanly toggles on and off with thepageExtensionsvalue. If maintainers consider this a facet of #62332 rather than a distinct bug, feel free to close and I will move the analysis there as a comment.#53473 / #91746 report the
no-html-link-for-pagesESLint rule not respecting custompageExtensions. Different feature area, but the same underlying pattern of a Next.js internal hardcoding single-dot extension assumptions. Flagging in case there is a shared fix opportunity.