Skip to content

Conversation

@avivkeller
Copy link
Member

@avivkeller avivkeller commented Dec 30, 2025

This PR changes our custom MDX logic to the Next.js-recommended next-mdx-remote logic. While this isn't the first-party @next/mdx package, it's an officially recommended solution to reading MDX from files.

Before we can use @next/mdx directly, the following should be resolved:

  1. Layouts
    a. Support passing additional options to layouts vercel/next.js#87990 is resolved OR
    b. Given that @next/mdx would require moving to a 1:1 app/ -> route layout, we could define the layouts as layout.tsx files, rather than via a WithLayout provider
  2. Internationalization: Currently, our internationalization is done via a dynamic /[locale]/ slug. However, if we move all of our pages into the App Router, this /[locale]/ will no longer be dynamic, since all the possible locales will be hard-coded. How do we set up internationalization? Layouts across many locales?

@vercel
Copy link

vercel bot commented Dec 30, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
nodejs-org Ready Ready Preview Jan 2, 2026 10:23pm

@github-actions
Copy link
Contributor

👋 Codeowner Review Request

The following codeowners have been identified for the changed files:

Team reviewers: @nodejs/web-infra @nodejs/nodejs-website

Please review the changes when you have a chance. Thank you! 🙏

@avivkeller
Copy link
Member Author

I'm aware reviewing this many files is impossible. Once I get everything passing, I will make this much more manageable. Right now, the goal was just to be in a position where a feature branch could be opened

@codecov
Copy link

codecov bot commented Dec 30, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.16%. Comparing base (fcfc7e2) to head (d74c39e).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8490      +/-   ##
==========================================
- Coverage   74.21%   74.16%   -0.05%     
==========================================
  Files         106      105       -1     
  Lines        9102     9038      -64     
  Branches      308      303       -5     
==========================================
- Hits         6755     6703      -52     
+ Misses       2345     2333      -12     
  Partials        2        2              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@ovflowd
Copy link
Member

ovflowd commented Dec 30, 2025

casual tuesday 1am (cest): claudio about to chill with YouTube
aviv: Imma gonna destroy this man's plans muahahaha

@avivkeller
Copy link
Member Author

avivkeller commented Dec 30, 2025

I think you might wanna go back to that YouTube... this is no where near ready 😅. On the silver lining, through the process of building this, I have several bug reports to send to Next.js.

@github-actions
Copy link
Contributor

github-actions bot commented Dec 30, 2025

📦 Build Size Comparison

Summary

Metric Value
Old Total Size 3.75 MB
New Total Size 3.09 MB
Delta -679.82 KB (-17.70%)

Changes

➕ Added Assets (1)
Name Size
.next/static/chunks/3706bf043231ef56.js 205.09 KB
➖ Removed Assets (3)
Name Size
.next/static/chunks/9385d8b2e7a095eb.js 205.09 KB
.next/static/chunks/96f7a6f942e912ae.js 561.55 KB
.next/static/chunks/976ea1c2d37f9b6c.js 118.27 KB

@avivkeller
Copy link
Member Author

avivkeller commented Dec 30, 2025

-743.48 KB (-19.53%)

😲

I'm still figuring out where this is a good thing or a bad thing :/

@ovflowd
Copy link
Member

ovflowd commented Dec 30, 2025

I think you might wanna go back to that YouTube... this is no where near ready 😅. On the silver lining, through the process of building this, I have several bug reports to send to Next.js.

Imma gonna back to another lifetime 😆 -- Im out! HAHAHA

@ovflowd
Copy link
Member

ovflowd commented Dec 30, 2025

-743.48 KB (-19.53%)

😲

I'm still figuring out where this is a good thing or a bad thing :/

🌡️

@ovflowd
Copy link
Member

ovflowd commented Dec 30, 2025

image

@avivkeller
Copy link
Member Author

If I can get it to work, we should see a substantial size reduction on both the client and server build.... if I can get it to work

@avivkeller avivkeller changed the title feat(mdx): move to dedicated package, switch to @next/mdx feat(mdx): move to next-mdx-remote with a dedicated MDX content package Jan 2, 2026
@avivkeller avivkeller changed the title feat(mdx): move to next-mdx-remote with a dedicated MDX content package feat(mdx): move to next-mdx-remote Jan 2, 2026
@avivkeller avivkeller marked this pull request as ready for review January 2, 2026 22:20
@avivkeller avivkeller requested review from a team as code owners January 2, 2026 22:20
Copilot AI review requested due to automatic review settings January 2, 2026 22:20
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Next.js seems to really enjoy changing this file, should we .gitignore it?

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the Node.js website's MDX handling from a custom implementation to the Next.js-recommended next-mdx-remote library. The change simplifies the codebase by replacing custom MDX compilation logic with a well-supported third-party solution.

Key changes:

  • Migrated from custom @mdx-js/mdx compilation to next-mdx-remote/rsc
  • Removed dependencies on @vcarl/remark-headings, remark-reading-time, vfile, and vfile-matter (now handled by next-mdx-remote)
  • Reorganized MDX-related code into a new router/ directory structure with dedicated plugin files
  • Simplified page rendering logic across locale and blog routes

Reviewed changes

Copilot reviewed 30 out of 32 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
pnpm-lock.yaml Updated dependencies: added next-mdx-remote, moved @mdx-js/mdx to devDependencies, removed custom remark/vfile packages
apps/site/package.json Aligned package changes with lock file
apps/site/types/server.ts Updated type definitions to use inline heading type instead of @vcarl/remark-headings
apps/site/types/markdown.ts Added MarkdownFile type and canonical frontmatter field
apps/site/util/array.ts Added joinNested utility for path segment joining
apps/site/router/index.ts New centralized router logic using next-mdx-remote's compileMDX
apps/site/router/render.tsx Extracted page rendering logic into dedicated component
apps/site/router/page.ts Shared page metadata and viewport generation logic
apps/site/router/mdx/plugins/* Reorganized MDX plugins (table, headings, shiki) into dedicated files
apps/site/app/[locale]/page.tsx Simplified page component using new router infrastructure
apps/site/app/[locale]/blog/[cat]/page.tsx New blog category page using simplified routing
apps/site/app/[locale]/download/archive/[version]/page.tsx Refactored to use new router infrastructure
apps/site/next.helpers.mjs Simplified markdown file retrieval with new CONTENT_ROOT constant
apps/site/scripts/* Updated to use new helper functions and constants
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +58 to +60
markdown.pathname = `/download/archive/${version}`;

return renderPage(markdown);
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The markdown object returned from getMarkdownFile is being mutated directly. If the markdown file is cached (which it is via the cache wrapper in router/index.ts), this mutation could affect all consumers of the cached value.

Consider creating a new object with the updated pathname instead of mutating the original: return renderPage({ ...markdown, pathname: /download/archive/${version} });

Suggested change
markdown.pathname = `/download/archive/${version}`;
return renderPage(markdown);
return renderPage({
...markdown,
pathname: `/download/archive/${version}`,
});

Copilot uses AI. Check for mistakes.
context: { ...context, pathname: `/download/archive/${pathname}` },
});
if (isDynamicRoute) {
const markdown = (await getMarkdownFile(locale, 'download/archive'))!;
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The non-null assertion operator (!) is used here without proper null checking. If getMarkdownFile returns null (e.g., the file doesn't exist), this will throw a runtime error. The assertion should be replaced with proper error handling or null checking before the mutation occurs.

Suggested change
const markdown = (await getMarkdownFile(locale, 'download/archive'))!;
const markdown = await getMarkdownFile(locale, 'download/archive');
if (!markdown) {
return notFound();
}

Copilot uses AI. Check for mistakes.
Comment on lines 54 to 60
const getPage: FC<PageParams> = async props => {
const { path, locale: routeLocale } = await props.params;

// Gets the current full pathname for a given path
const [locale, pathname] = basePage.getLocaleAndPath(path, routeLocale);
const { path = [], locale } = await props.params;

// Gets the Markdown content and context
const [content, context] = await basePage.getMarkdownContext({
locale,
pathname,
});
const markdown = await getMarkdownFile(locale, joinNested(path));

// If we have a filename and layout then we have a page
if (context.filename && context.frontmatter.layout) {
return basePage.renderPage({
content,
layout: context.frontmatter.layout,
context,
});
}

return notFound();
return markdown ? renderPage(markdown) : notFound();
};
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing locale validation and redirect logic. The previous implementation in next.dynamic.page.mjs validated whether the locale was in availableLocaleCodes and redirected to the default locale if it wasn't. This validation and redirect logic is now absent, which could lead to pages being rendered with unsupported locales.

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +30
export const joinNested = (
...args: Array<string | Array<string> | undefined>
) => join(...(args.filter(Boolean) as Array<string>).flat());
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function joinNested has an unclear name that doesn't clearly convey its purpose. The name suggests it joins nested structures, but it actually joins path segments (which may be nested arrays) into a single path string. A more descriptive name would be joinPathSegments or flatJoinPath to better indicate that it flattens and joins path components.

Suggested change
export const joinNested = (
...args: Array<string | Array<string> | undefined>
) => join(...(args.filter(Boolean) as Array<string>).flat());
export const joinPathSegments = (
...args: Array<string | Array<string> | undefined>
) => join(...(args.filter(Boolean) as Array<string>).flat());
/**
* @deprecated Use {@link joinPathSegments} instead.
*/
export const joinNested = (
...args: Array<string | Array<string> | undefined>
) => joinPathSegments(...args);

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +30
// Join the arguments like a path
export const joinNested = (
...args: Array<string | Array<string> | undefined>
) => join(...(args.filter(Boolean) as Array<string>).flat());
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The joinNested utility function is placed in util/array.ts, but its functionality is path manipulation, not array manipulation. While it does use array flattening internally, its primary purpose is path joining. This function would be more appropriately located in a path utilities file or a dedicated routing utilities file.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +44
import { toString } from 'mdast-util-to-string';
import { visit } from 'unist-util-visit';

/**
* Remark plugin that adds data-label attributes to table cells (td)
* based on their corresponding table headers (th).
*/
export default () => tree => {
visit(tree, 'table', table => {
// Ensure table has at least a header row and one data row
if (table.children.length < 2) {
return;
}

const [headerRow, ...dataRows] = table.children;

if (headerRow.children.length <= 1) {
table.data ??= {};

table.data.hProperties = {
'data-cards': 'false',
};
}

// Extract header labels from the first row
const headerLabels = headerRow.children.map(headerCell =>
toString(headerCell.children)
);

// Assign data-label to each cell in data rows
dataRows.forEach(row => {
row.children.forEach((cell, idx) => {
if (idx > headerLabels.length - 1) {
return;
}
cell.data ??= {};

cell.data.hProperties = {
'data-label': headerLabels[idx],
};
});
});
});
};
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The table plugin was moved from util/table.ts to router/mdx/plugins/table.mjs, but its comprehensive test file (util/__tests__/table.test.mjs with 434 lines) was removed without being relocated. The moved plugin code should maintain the same test coverage to ensure the table labeling functionality continues to work correctly.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +20
import { toString } from 'mdast-util-to-string';
import { visit } from 'unist-util-visit';

export const headings = root => {
const list = [];

visit(root, 'heading', node => {
list.push({
depth: node.depth,
value: toString(node, { includeImageAlt: false }),
});
});

return list;
};

export default ({ output }) =>
tree => {
output.headings = headings(tree);
};
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The headings plugin lacks test coverage. The plugin extracts heading information from markdown, which is critical functionality for table of contents generation. It should have tests to ensure it correctly handles various heading depths, content types, and edge cases.

Copilot uses AI. Check for mistakes.
Comment on lines +2 to +7
* This file extends on the `page.tsx` file, which is the default file that is used to render
* the entry points for each locale and then also reused within the [...path] route to render the
* and contains all logic for rendering our dynamic and static routes within the Node.js Website.
*
* Note: that each `page.tsx` should have its own `generateStaticParams` to prevent clash of
* dynamic params, which will lead on static export errors and other sort of issues.
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment incorrectly states this file "extends on the page.tsx file", but this IS the page.tsx file itself. The comment should clarify that this is the main entry point page for locale routes and is reused by the [...path] route via exports.

Suggested change
* This file extends on the `page.tsx` file, which is the default file that is used to render
* the entry points for each locale and then also reused within the [...path] route to render the
* and contains all logic for rendering our dynamic and static routes within the Node.js Website.
*
* Note: that each `page.tsx` should have its own `generateStaticParams` to prevent clash of
* dynamic params, which will lead on static export errors and other sort of issues.
* This file is the main `page.tsx` entry point used to render the routes for each locale.
* It is also reused by the `[...path]` route via its exports to render both dynamic and
* static routes within the Node.js Website.
*
* Note: each `page.tsx` should have its own `generateStaticParams` to prevent clashes of
* dynamic params, which can lead to static export errors and other related issues.

Copilot uses AI. Check for mistakes.
Comment on lines +42 to 47
return locales.map((locale: string) =>
allRoutes.map(path => ({
locale,
path: path.split(sep),
}))
);
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generateStaticParams function returns a nested array structure (array of arrays) instead of a flat array. The map operation on line 42-46 returns an array of arrays where each locale maps to an array of path objects. This should be flattened before being returned.

The return value should be locales.flatMap(...) or locales.map(...).flat() to ensure a flat array of objects is returned.

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +46
file.pathname = `/blog/${cat}`;

return renderPage(file);
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The markdown object returned from getMarkdownFile is being mutated directly. If the markdown file is cached (which it is via the cache wrapper in router/index.ts), this mutation could affect all consumers of the cached value.

Consider creating a new object with the updated pathname instead of mutating the original: return renderPage({ ...markdown, pathname: '/blog/${cat}' });

Suggested change
file.pathname = `/blog/${cat}`;
return renderPage(file);
return renderPage({ ...file, pathname: `/blog/${cat}` });

Copilot uses AI. Check for mistakes.
@avivkeller avivkeller marked this pull request as draft January 2, 2026 22:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants