Skip to content

Font preload tags not emitted when using multi-dot pageExtensions (e.g. 'page.tsx') #95449

Description

@KinjiKawaguchi

Link to the code that reproduces this issue

https://github.com/KinjiKawaguchi/nextjs-font-preload-multi-dot-repro

To Reproduce

  1. Clone the reproduction repository and install (pnpm install).
  2. Build and start (pnpm build && pnpm start).
  3. curl -s http://localhost:3000/ | grep 'rel="preload".*font'
  4. Nothing prints — no <link rel="preload" as="font"> tag is present in the HTML.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Font (next/font)Related to Next.js Font Optimization.TurbopackRelated to Turbopack with Next.js.WebpackRelated to Webpack with Next.js.

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions