Skip to content

Epic: Move away from pages folder #8262

Description

@ovflowd

Back in 2023/2024 when we were releasing the redesigned Node.js Website, we had to create a custom dynamic router around our infrastructure (Next.js) because pages folder structure is what @nodejs/collaborators were used to work with. This was intentionally done to not make the move to Next.js feel as big as an atomic move as it could be. This was also done to keep DX for existing collaborators simple.

Fast forwarding to now, release blog post generation is getting automated and releasers and other collaborators that have worked with us are used and more familiar with our Next.js environment.

The time has probably come. It's time to move away from our custom next.dynamic.mjs router (including all the bells-and-whistles) and adopt Next.js's built-in MDX, which uses mdx-rs, a Rust-based MDX parser/compiler (https://github.com/web-infra-dev/mdx-rs) and that at the moment also supports custom JS-based Rehype and Remark Plugins.

This work effectively means that:

  • Our custom Frontmatter Engine for Layouts will be gone. Frontmatter would still exist, but Layouts are decided by the App router.
    • i.e. /app/blog/layout.tsx, etc.
    • each section (learn, download, etc) use their own layouts.
  • Each page (.mdx) file under /pages would be moved under the respective folder under /app/ router
  • The use of catch-all segments would effectively be diminished (except for some rare situations such as blog category pages and pagination and the index page (that is also a blog category page)
  • Adoption of Next.js's built-in MDX parser/compiler that is much faster and also enables support of some real cool stuff
  • Our custom glob/mdx parsing and caching engine will be gone

Some of the benefits:

  • Much faster compilation and dev runtime
  • 1:1 (or almost 1:1, because there are still some magic routes such as blog categories) file-system to route
  • More out of the box Next.js experience and easier to maintain and understand codebase
  • Fast-refresh / HMR (Hot-Module-Reload) support for MDX
  • Simplification of our OpenNext.js infrastructure. No more need of the hacky node:fs polyfills and whatnot

The cons?

  • Poor @avivkeller is going to work overtime on this LOL (/s)

cc @nodejs/web-infra @nodejs/nodejs-website

Activity

  1. added theissue type on Oct 23, 2025
  2. added
    contentIssues/pr concerning content
    website redesignIssue/PR part of the Node.js Website Redesign
    infrastructureIssues/PRs related to the Repository Infra
    metaMeta Issues for Administration of the Website Team
    on Oct 23, 2025
  3. AugustinMauroy commented on Oct 23, 2025

    @AugustinMauroy
    Member

    feel -0.5 to put MDX on app dir because I don't like to mix routing and content.

    feel +1 to have a content dir that store md/mdx and these are processed by the Next.js app router. And for layout instead of injecting them by frontMatter we can just define it on the app router.

  4. ovflowd commented on Oct 23, 2025

    @ovflowd
    MemberAuthor

    feel -0.5 to put MDX on app dir because I don't like to mix routing and content.

    feel +1 to have a content dir that store md/mdx and these are processed by the Next.js app router. And for layout instead of injecting them by frontMatter we can just define it on the app router.

    I get the feeling, but right now we have some pretty custom infrastructure that deviates from standard Next.js and App router was made for content too. This differentiation has been the root cause for many problems with our Next.js upgrades + the main piece of effort and issues with our Cloudflare and OpenNext integration. So yeah, I definitely would like to move away from there :)

  5. avivkeller commented on Oct 23, 2025

    @avivkeller
    Member

    My main concern here is that every blog post would need to be its own folder, correct? At least, that's my understanding of https://nextjs.org/docs/app/guides/mdx

  6. avivkeller commented on Oct 23, 2025

    @avivkeller
    Member

    As I mentioned above, and as @ovflowd agreed on Slack, the "one page per folder" structure is a major blocker for this change. It's infeasible for us to make a new folder for every new blog post and learn article we have.

  7. bmuenzenmeyer commented on Oct 29, 2025

    @bmuenzenmeyer
    Contributor

    is the reasoning beyond busy work? we can script/automate that away

  8. avivkeller commented on Dec 22, 2025

    @avivkeller
    Member

    is the reasoning beyond busy work? we can script/automate that away

    The reasoning is DX. The pages folder, IMO, is very neat. Making a new folder for every page is a lot less... 'neat'.

  9. avivkeller commented on Dec 29, 2025

    @avivkeller
    Member

    Regarding a script, however, I wrote:

    import fs from 'node:fs/promises';
    import path from 'node:path';
    
    const base = new URL(import.meta.resolve('./apps/site/'));
    const pagesBase = new URL('./pages/', base);
    
    const pages = await Array.fromAsync(
      fs.glob('**/*.{md,mdx}', { cwd: pagesBase })
    );
    
    for (const page of pages) {
      const src = new URL(page, pagesBase);
    
      const ext = path.extname(page);
      const parsed = path.parse(page);
    
      // If the file is named "index", don't create a subdirectory
      const destPath =
        parsed.name === 'index'
          ? path.join('app', parsed.dir, `page${ext}`)
          : path.join('app', parsed.dir, parsed.name, `page${ext}`);
    
      await fs.cp(src, new URL(destPath, base));
    }
    
    
  10. avivkeller commented on Jan 2, 2026

    @avivkeller
    Member

    After doing a bunch of testing, the only viable way for use to use @next/mdx is via the app router without dynamic imports. This means, as Claudio said in the issue description, a 1:1 app folder to page routes. (This is because we have too many pages for Turbopack to compile into a bundle in a reasonable time.1)

    However, this structure brings it's own set of hoops.

    First, with layouts. Currently, we specify the layout via MDX frontmatter. At the same time, we also attach headings and readingTime to the VFile containing our Markdown. Unfortunately, when using the /app/(path/to/my/route)/page.mdx way of writing pages, this data cannot be passed to the layouts. In order to pass data to the layouts, we'd need Next.js to resolve vercel/next.js#87990, OR write a custom Remark plugin to have each page export it's data to the layouts.

    Second, with internationalization. Currently, our current i18n setup relies on a dynamic /[locale]/ route segment. If we move all of our pages into the app folder, locales will be explicitly defined (like in our pages folder, meaning /[locale]/ can no longer remain fully dynamic. This change raises a question: How do we have internationalization work without a [locale], but still honor the locale prefixes in our URLs.

    Footnotes

    1. https://github.com/hashicorp/next-mdx-remote#background--theory

      Webpack is a JavaScript bundler, forcing it to load hundreds/thousands of pages of text content will blow out your memory requirements. Webpack stores each page as a distinct object with a large amount of metadata. One of our implementations with a couple hundred pages hit more than 8GB of memory required to compile the site. Builds took more than 25 minutes.

      This is behavior that I was able to reproduce. Now, I'm wondering if Turbopack compiles all routes beforehand. If so, then I doubt we can use @next/mdx at all, given that compiling all of our routes takes several minutes. ↩

  11. ovflowd commented on Jan 3, 2026

    @ovflowd
    MemberAuthor

    Also a note with next-mdx-remote is that it would load the MDX as getStaticProps which means, all that raw MDX would also be attached to the page's HTML, as Next.js "imprints/stamps" the static data on the HTML within a <script> tag. -- There are many reasons why we went away from that library, unironically our custom router right now is as performant as it gets.

    I also foresee that if Vercel addresses vercel/next.js#87990, we can simplify a few things on our custom router. Ideally, @avivkeller we can even make this whole custom MDX router a package and distribute to npm as an alternative to next-mdx-remote. We validated it as being something good. Then we just need to figure out which pieces we outsource and how to make it an easily importable thing that isn't stuck to just our way of structuring pages. That can be a good contribution to the community as right now there isn't really a good alternative to next-mdx-remote... I've seen this next-mdx-remote-client package but no idea how good it is, and if it covers our needs...

    Note that the package I mentioned above feels supercomplex and probably not the way we want to do things...

  12. self-assigned this
    on Jan 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

blockedcontentIssues/pr concerning contentinfrastructureIssues/PRs related to the Repository InframetaMeta Issues for Administration of the Website Teamwebsite redesignIssue/PR part of the Node.js Website Redesign

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions