The Node.js® Website
1'use strict';
2
3import { existsSync } from 'node:fs';
4import { readFile } from 'node:fs/promises';
5import { join, normalize, sep } from 'node:path';
6
7import matter from 'gray-matter';
8import { cache } from 'react';
9import { VFile } from 'vfile';
10
11import { BASE_URL, BASE_PATH, IS_DEVELOPMENT } from './next.constants.mjs';
12import {
13 IGNORED_ROUTES,
14 DYNAMIC_ROUTES,
15 PAGE_METADATA,
16} from './next.dynamic.constants.mjs';
17import { getMarkdownFiles } from './next.helpers.mjs';
18import { siteConfig } from './next.json.mjs';
19import { availableLocaleCodes, defaultLocale } from './next.locales.mjs';
20import { compileMDX } from './next.mdx.compiler.mjs';
21
22// This is the combination of the Application Base URL and Base PATH
23const baseUrlAndPath = `${BASE_URL}${BASE_PATH}`;
24
25// This is a small utility that allows us to quickly separate locale from the remaining pathname
26const getPathname = (path = []) => path.join('/');
27
28// This maps a pathname into an actual route object that can be used
29// we use a platform-specific separator to split the pathname
30// since we're using filepaths here and not URL paths
31const mapPathToRoute = (locale = defaultLocale.code, path = '') => ({
32 locale,
33 path: path.split(sep),
34});
35
36// Provides an in-memory Map that lasts the whole build process
37// and disabled when on development mode (stubbed)
38const createCachedMarkdownCache = () => {
39 if (IS_DEVELOPMENT) {
40 return {
41 has: () => false,
42 set: () => {},
43 get: () => null,
44 };
45 }
46
47 return new Map();
48};
49
50const getDynamicRouter = async () => {
51 // Creates a Cache System that is disabled during development mode
52 const cachedMarkdownFiles = createCachedMarkdownCache();
53
54 // Keeps the map of pathnames to filenames
55 const pathnameToFilename = new Map();
56
57 const websitePages = await getMarkdownFiles(
58 process.cwd(),
59 `pages/${defaultLocale.code}`
60 );
61
62 websitePages.forEach(filename => {
63 // This Regular Expression is used to remove the `index.md(x)` suffix
64 // of a name and to remove the `.md(x)` extensions of a filename.
65 let pathname = filename.replace(/((\/)?(index))?\.mdx?$/i, '');
66
67 if (pathname.length > 1 && pathname.endsWith(sep)) {
68 pathname = pathname.substring(0, pathname.length - 1);
69 }
70
71 pathname = normalize(pathname).replace('.', '');
72
73 // We map the pathname to the filename to be able to quickly
74 // resolve the filename for a given pathname
75 pathnameToFilename.set(pathname, filename);
76 });
77
78 /**
79 * This method returns a list of all routes that exist for a given locale
80 *
81 * @param {string} locale
82 * @returns {Array<string>}
83 */
84 const getRoutesByLanguage = async (locale = defaultLocale.code) => {
85 const shouldIgnoreStaticRoute = pathname =>
86 IGNORED_ROUTES.every(e => !e({ pathname, locale }));
87
88 return [...pathnameToFilename.keys()]
89 .filter(shouldIgnoreStaticRoute)
90 .concat([...DYNAMIC_ROUTES.keys()]);
91 };
92
93 /**
94 * This method attempts to retrieve either a localized Markdown file
95 * or the English version of the Markdown file if no localized version exists
96 * and then returns the contents of the file and the name of the file (not the path)
97 *
98 * @param {string} locale
99 * @param {string} pathname
100 * @returns {Promise<{ source: string; filename: string }>}
101 */
102 const _getMarkdownFile = async (locale = '', pathname = '') => {
103 const normalizedPathname = normalize(pathname).replace('.', '');
104
105 // This verifies if the given pathname actually exists on our Map
106 // meaning that the route exists on the website and can be rendered
107 if (pathnameToFilename.has(normalizedPathname)) {
108 const filename = pathnameToFilename.get(normalizedPathname);
109
110 let filePath = join(process.cwd(), 'pages');
111
112 // We verify if our Markdown cache already has a cache entry for a localized
113 // version of this file, because if not, it means that either
114 // we did not cache this file yet or there is no localized version of this file
115 if (cachedMarkdownFiles.has(`${locale}${normalizedPathname}`)) {
116 const fileContent = cachedMarkdownFiles.get(
117 `${locale}${normalizedPathname}`
118 );
119
120 return { source: fileContent, filename };
121 }
122
123 // No cache hit exists, so we check if the localized file actually
124 // exists within our file system and if it does we set it on the cache
125 // and return the current fetched result; If the file does not exist
126 // we fallback to the English source
127 if (existsSync(join(filePath, locale, filename))) {
128 filePath = join(filePath, locale, filename);
129
130 const fileContent = await readFile(filePath, 'utf8');
131
132 cachedMarkdownFiles.set(`${locale}${normalizedPathname}`, fileContent);
133
134 return { source: fileContent, filename };
135 }
136
137 // We then attempt to retrieve the source version of the file as there is no localised version
138 // of the file and we set it on the cache to prevent future checks of the same locale for this file
139 const { source: fileContent } = await _getMarkdownFile(
140 defaultLocale.code,
141 pathname
142 );
143
144 // We set the source file on the localized cache to prevent future checks
145 // of the same locale for this file and improve read performance
146 cachedMarkdownFiles.set(`${locale}${normalizedPathname}`, fileContent);
147
148 return { source: fileContent, filename };
149 }
150
151 return { filename: '', source: '' };
152 };
153
154 // Creates a Cached Version of the Markdown File Resolver
155 const getMarkdownFile = cache(async (locale, pathname) => {
156 return await _getMarkdownFile(locale, pathname);
157 });
158
159 /**
160 * This method runs the MDX compiler on the server-side and returns the
161 * parsed JSX ready to be rendered on a page as a React Component
162 *
163 * @param {string} source
164 * @param {string} filename
165 */
166 const _getMDXContent = async (source = '', filename = '') => {
167 // We create a VFile (Virtual File) to be able to access some contextual
168 // data post serialization (compilation) of the source Markdown into MDX
169 const sourceAsVirtualFile = new VFile(source);
170
171 // Gets the file extension of the file, to determine which parser and plugins to use
172 const fileExtension = filename.endsWith('.mdx') ? 'mdx' : 'md';
173
174 // This compiles our MDX source (VFile) into a final MDX-parsed VFile
175 // that then is passed as a string to the MDXProvider which will run the MDX Code
176 return compileMDX(sourceAsVirtualFile, fileExtension);
177 };
178
179 // Creates a Cached Version of the MDX Compiler
180 const getMDXContent = cache(async (source, filename) => {
181 return await _getMDXContent(source, filename);
182 });
183
184 /**
185 * This method generates the Next.js App Router Metadata
186 * that can be used for each page to provide metadata
187 *
188 * @param {string} locale
189 * @param {string} path
190 * @returns {import('next').Metadata}
191 */
192 const _getPageMetadata = async (locale = defaultLocale.code, path = '') => {
193 const pageMetadata = { ...PAGE_METADATA };
194
195 const { source = '' } = await getMarkdownFile(locale, path);
196
197 const { data } = matter(source);
198
199 pageMetadata.title = data.title
200 ? `${siteConfig.title} — ${data.title}`
201 : siteConfig.title;
202
203 pageMetadata.twitter.title = pageMetadata.title;
204
205 const getUrlForPathname = (l, p) =>
206 `${baseUrlAndPath}/${l}${p ? `/${p}` : ''}`;
207
208 pageMetadata.alternates.canonical = getUrlForPathname(locale, path);
209
210 pageMetadata.alternates.languages['x-default'] = getUrlForPathname(
211 defaultLocale.code,
212 path
213 );
214
215 availableLocaleCodes.forEach(currentLocale => {
216 pageMetadata.alternates.languages[currentLocale] = getUrlForPathname(
217 currentLocale,
218 path
219 );
220 pageMetadata.openGraph.images = [
221 `${currentLocale}/next-data/og?title=${data.title}&type=${data.category ?? 'announcement'}`,
222 ];
223 });
224
225 return pageMetadata;
226 };
227
228 // Creates a Cached Version of the Page Metadata Context
229 const getPageMetadata = cache(async (locale, path) => {
230 return await _getPageMetadata(locale, path);
231 });
232
233 return {
234 mapPathToRoute,
235 getPathname,
236 getRoutesByLanguage,
237 getMDXContent,
238 getMarkdownFile,
239 getPageMetadata,
240 };
241};
242
243export const dynamicRouter = await getDynamicRouter();