diff --git a/next.config.mjs b/next.config.mjs index a9ef7729adea4..ea3a751fdfcae 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,19 +1,10 @@ 'use strict'; -import * as nextConstants from './next.constants.mjs'; -import * as nextRewrites from './next.rewrites.mjs'; +import { BASE_PATH, ENABLE_STATIC_EXPORT } from './next.constants.mjs'; +import { redirects, rewrites } from './next.rewrites.mjs'; /** @type {import('next').NextConfig} */ const nextConfig = { - // This configures all the Next.js rewrites, which are used for rewriting internal URLs into other internal Endpoints - // This feature is not supported within static export builds, hence we pass an empty array if static exports are enabled - rewrites: !nextConstants.ENABLE_STATIC_EXPORT - ? nextRewrites.rewrites - : undefined, - // This configures all Next.js redirects - redirects: !nextConstants.ENABLE_STATIC_EXPORT - ? nextRewrites.redirects - : undefined, // We intentionally disable Next.js's built-in i18n support // as we dom have our own i18n and internationalisation engine i18n: null, @@ -24,13 +15,18 @@ const nextConfig = { trailingSlash: false, // We allow the BASE_PATH to be overridden in case that the Website // is being built on a subdirectory (e.g. /nodejs-website) - basePath: nextConstants.BASE_PATH, + basePath: BASE_PATH, // We disable image optimisation during static export builds - images: { unoptimized: nextConstants.ENABLE_STATIC_EXPORT }, + images: { unoptimized: ENABLE_STATIC_EXPORT }, // On static export builds we want the output directory to be "build" - distDir: nextConstants.ENABLE_STATIC_EXPORT ? 'build' : '.next', + distDir: ENABLE_STATIC_EXPORT ? 'build' : '.next', // On static export builds we want to enable the export feature - output: nextConstants.ENABLE_STATIC_EXPORT ? 'export' : undefined, + output: ENABLE_STATIC_EXPORT ? 'export' : undefined, + // This configures all the Next.js rewrites, which are used for rewriting internal URLs into other internal Endpoints + // This feature is not supported within static export builds, hence we pass an empty array if static exports are enabled + rewrites: !ENABLE_STATIC_EXPORT ? rewrites : undefined, + // This configures all Next.js redirects + redirects: !ENABLE_STATIC_EXPORT ? redirects : undefined, // We don't want to run Type Checking on Production Builds // as we already check it on the CI within each Pull Request typescript: { ignoreBuildErrors: true }, diff --git a/next.constants.mjs b/next.constants.mjs index e9d70871e2609..4d8041b92be8f 100644 --- a/next.constants.mjs +++ b/next.constants.mjs @@ -1,7 +1,7 @@ 'use strict'; -import * as nextJson from './next.json.mjs'; -import * as nextLocales from './next.locales.mjs'; +import { blogData } from './next.json.mjs'; +import { defaultLocale } from './next.locales.mjs'; /** * This is used for telling Next.js if the Website is deployed on Vercel @@ -80,7 +80,7 @@ export const MD_EXTENSION_REGEX = /((\/)?(index))?\.mdx?$/i; * This should only be used outside of the Next.js Application itself * as within React context the `useLocale` hook should be used instead. */ -export const DEFAULT_LOCALE_CODE = nextLocales.defaultLocale.code; +export const DEFAULT_LOCALE_CODE = defaultLocale.code; /** * This indicates the path to the Legacy JavaScript File that is used @@ -137,5 +137,5 @@ export const DYNAMIC_ROUTES_REWRITES = [ * @returns {string[]} A list of all the Dynamic Routes that are generated by the Website */ export const DYNAMIC_GENERATED_ROUTES = () => [ - ...nextJson.blogData.pagination.map(year => `en/blog/year-${year}`), + ...blogData.pagination.map(year => `en/blog/year-${year}`), ]; diff --git a/next.dynamic.mjs b/next.dynamic.mjs index 030c6597cff30..1c64202530a62 100644 --- a/next.dynamic.mjs +++ b/next.dynamic.mjs @@ -8,15 +8,13 @@ import remarkHeadings from '@vcarl/remark-headings'; import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import rehypeSlug from 'rehype-slug'; import { serialize } from 'next-mdx-remote/serialize'; -import * as nextLocales from './next.locales.mjs'; -import * as nextConstants from './next.constants.mjs'; -import * as nextHelpers from './next.helpers.mjs'; +import { availableLocales } from './next.locales.mjs'; +import { getMarkdownFiles } from './next.helpers.mjs'; +import { DEFAULT_LOCALE_CODE, MD_EXTENSION_REGEX } from './next.constants.mjs'; // allows us to run a glob to get markdown files based on a language folder -const getPathsByLanguage = async ( - locale = nextConstants.DEFAULT_LOCALE_CODE, - ignored = [] -) => nextHelpers.getMarkdownFiles(process.cwd(), `pages/${locale}`, ignored); +const getPathsByLanguage = async (locale = DEFAULT_LOCALE_CODE, ignored = []) => + getMarkdownFiles(process.cwd(), `pages/${locale}`, ignored); /** * This method is responsible for generating a Collection of all available paths that @@ -31,9 +29,7 @@ const getPathsByLanguage = async ( const getAllPaths = async () => { // during full static build we don't want to cover blog posts // as otherwise they will all get built as static pages during build time - const sourcePages = await getPathsByLanguage( - nextConstants.DEFAULT_LOCALE_CODE - ); + const sourcePages = await getPathsByLanguage(DEFAULT_LOCALE_CODE); /** * This method is used to provide the list of pages that are provided by a given locale @@ -44,7 +40,7 @@ const getAllPaths = async () => { (files = []) => sourcePages.map(filename => { // remove the index.md(x) suffix from a pathname - let pathname = filename.replace(nextConstants.MD_EXTENSION_REGEX, ''); + let pathname = filename.replace(MD_EXTENSION_REGEX, ''); // remove trailing slash for correct Windows pathing of the index files if (pathname.length > 1 && pathname.endsWith(sep)) { pathname = pathname.substring(0, pathname.length - 1); @@ -65,11 +61,10 @@ const getAllPaths = async () => { * * @type {[string, import('./types').RouteSegment[]][]} */ - const allAvailableMarkdownPaths = nextLocales.availableLocales.map( - ({ code }) => - getPathsByLanguage(code) - .then(mergePathsWithFallback(code)) - .then(files => [code, files]) + const allAvailableMarkdownPaths = availableLocales.map(({ code }) => + getPathsByLanguage(code) + .then(mergePathsWithFallback(code)) + .then(files => [code, files]) ); return Promise.all(allAvailableMarkdownPaths); @@ -95,7 +90,7 @@ export const allPaths = new Map(await getAllPaths()); * @throws {Error} if the file does not exist, which should never happen */ export const getMarkdownFile = ( - locale = nextConstants.DEFAULT_LOCALE_CODE, + locale = DEFAULT_LOCALE_CODE, pathname = '' ) => { const metadata = { source: '', filename: '' }; @@ -111,9 +106,7 @@ export const getMarkdownFile = ( if (route && route.filename) { // this determines if we should be using the fallback rendering to the default locale // or if we can use the current locale - const localeToUse = !route.localised - ? nextConstants.DEFAULT_LOCALE_CODE - : locale; + const localeToUse = !route.localised ? DEFAULT_LOCALE_CODE : locale; // gets the full pathname for the file (absolute path) metadata.filename = join( diff --git a/next.json.mjs b/next.json.mjs index f57ee0fe70348..cbcbfdc188642 100644 --- a/next.json.mjs +++ b/next.json.mjs @@ -6,10 +6,13 @@ import siteConfig from './site.json' assert { type: 'json' }; // This is the static Site Navigation (legacy website) import siteNavigation from './navigation.json' assert { type: 'json' }; +// This is the static Site External and Internal Redirects Metadata +import siteRedirects from './redirects.json' assert { type: 'json' }; + // This is the Website i18n Configuration import localeConfig from './i18n/config.json' assert { type: 'json' }; // This is the generated blog data for the Node.js Website import blogData from './public/blog-posts-data.json' assert { type: 'json' }; -export { siteConfig, siteNavigation, localeConfig, blogData }; +export { siteConfig, siteNavigation, siteRedirects, localeConfig, blogData }; diff --git a/next.locales.mjs b/next.locales.mjs index e4eec239ad4e6..9b996061386a8 100644 --- a/next.locales.mjs +++ b/next.locales.mjs @@ -1,12 +1,12 @@ 'use strict'; -import * as nextJson from './next.json.mjs'; +import { localeConfig } from './next.json.mjs'; import translations from './i18n/locales/index.mjs'; // As set of available and enabled locales for the website // This is used for allowing us to redirect the user to any // of the available locales that we have enabled on the website -const availableLocales = nextJson.localeConfig.filter(locale => locale.enabled); +const availableLocales = localeConfig.filter(locale => locale.enabled); // This provides the default locale information for the Next.js Application // This is marked by the unique `locale.default` property on the `en` locale diff --git a/next.rewrites.mjs b/next.rewrites.mjs index b16db305cd2c5..daf05fd55b276 100644 --- a/next.rewrites.mjs +++ b/next.rewrites.mjs @@ -1,11 +1,12 @@ 'use strict'; -import * as nextLocales from './next.locales.mjs'; +import { siteRedirects } from './next.json.mjs'; +import { availableLocales } from './next.locales.mjs'; // We only need Locale Codes for our URL redirects and rewrties -const localeCodes = nextLocales.availableLocales.map(locale => locale.code); +const localeCodes = availableLocales.map(locale => locale.code); -// This allows us to prefix redirect with all available locale codes, so that redirects are not bound to a single locale +// This allows us to prefix redirects with all available locale codes so that redirects are not bound to a single locale // This also transforms the locale itself as a matching group that can be used for rewrites // This match group also has an empty string match for the lack of locales, for example // Example: /:locale(ar/|ca/|de/|en/|es/|fa/|fr/|)about/security @@ -16,211 +17,31 @@ const localesMatch = `/:locale(${localeCodes.join('|')}|)?/`; /** * These are external redirects that happen before we check dynamic routes and rewrites * These are sourced originally from https://github.com/nodejs/build/blob/main/ansible/www-standalone/resources/config/nodejs.org?plain=1 - * and were then converted to Next.js rewrites. Note that only relevant rewrites were added and some were modified to match Next.js's syntax - * - * @TODO: These values should be no-code; These should be imported from a JSON file or something similar + * and were then converted to Next.js rewrites. Note that only relevant rewrites were added, and some were modified to match Next.js's syntax * * @type {import('next').NextConfig['redirects']} */ -const redirects = async () => [ - { - source: '/index.html', - destination: '/', - permanent: true, - }, - { - source: '/api.html', - destination: '/api', - permanent: true, - }, - { - source: '/changelog.html', - destination: 'https://github.com/nodejs/node/blob/HEAD/CHANGELOG.md', - permanent: true, - }, - { - source: '/calendar', - destination: - 'https://calendar.google.com/calendar/embed?src=nodejs.org_nr77ama8p7d7f9ajrpnu506c98%40group.calendar.google.com', - permanent: true, - }, - { - source: '/calendar.ics', - destination: - 'https://calendar.google.com/calendar/ical/nodejs.org_nr77ama8p7d7f9ajrpnu506c98%40group.calendar.google.com/public/basic.ics', - permanent: true, - }, - { - source: `${localesMatch}security`, - destination: - 'https://github.com/nodejs/node/blob/HEAD/SECURITY.md#security', - permanent: true, - }, - { - source: `${localesMatch}contribute/accepting_contributions.html`, - destination: 'https://github.com/nodejs/dev-policy', - permanent: true, - }, - { - source: `${localesMatch}about/releases`, - destination: 'https://github.com/nodejs/release#release-schedule', - permanent: true, - }, - { - source: `${localesMatch}about/security`, - destination: - 'https://github.com/nodejs/node/blob/HEAD/SECURITY.md#security', - permanent: true, - }, - { - source: `${localesMatch}advisory-board`, - destination: 'https://github.com/nodejs/TSC', - permanent: true, - }, - { - source: `${localesMatch}about/advisory-board`, - destination: 'https://github.com/nodejs/TSC', - permanent: true, - }, - { - source: `${localesMatch}about/organization`, - destination: 'https://github.com/nodejs/TSC', +const redirects = async () => { + return siteRedirects.external.map(({ source, destination }) => ({ + source: source.replace('/:locale/', localesMatch), permanent: true, - }, - { - source: `${localesMatch}about/organization/tsc-meetings`, - destination: 'https://github.com/nodejs/TSC/tree/HEAD/meetings', - permanent: true, - }, - { - source: `${localesMatch}about/trademark`, - destination: 'https://trademark-policy.openjsf.org', - permanent: true, - }, - { - source: `${localesMatch}foundation`, - destination: 'https://openjsf.org', - permanent: true, - }, - { - source: `${localesMatch}foundation/case-studies`, - destination: 'https://openjsf.org/projects', - permanent: true, - }, - { - source: `${localesMatch}foundation/members`, - destination: 'https://openjsf.org/about/members', - permanent: true, - }, - { - source: `${localesMatch}foundation/board`, - destination: 'https://openjsf.org/about/governance', - permanent: true, - }, - { - source: `${localesMatch}foundation/tsc`, - destination: 'https://github.com/nodejs/TSC', - permanent: true, - }, - { - source: `${localesMatch}foundation/certification`, - destination: 'https://openjsf.org/certification', - permanent: true, - }, - { - source: `${localesMatch}foundation/in-the-news`, - destination: 'https://openjsf.org', - permanent: true, - }, - { - source: `${localesMatch}foundation/announcements`, - destination: 'https://openjsf.org/blog', - permanent: true, - }, - { - source: `${localesMatch}foundation/education`, - destination: 'https://openjsf.org/certification', - permanent: true, - }, -]; + destination, + })); +}; /** * These are rewrites that happen before we check dynamic routes and after we check regular redirects * These should be used either for internal or external rewrite rules (like NGINX, for example) * These are sourced originally from https://github.com/nodejs/build/blob/main/ansible/www-standalone/resources/config/nodejs.org?plain=1 - * and were then converted to Next.js rewrites. Note that only relevant rewrites were added and some were modified to match Next.js's syntax - * - * @TODO: These values should be no-code; These should be imported from a JSON file or something similar + * and were then converted to Next.js rewrites. Note that only relevant rewrites were added, and some were modified to match Next.js's syntax * * @type {import('next').NextConfig['rewrites']} */ const rewrites = async () => ({ - afterFiles: [ - { - source: '/about', - destination: '/en/about', - }, - { - source: '/community', - destination: '/en/get-involved', - }, - { - source: '/contribute/:path*', - destination: '/en/get-involved', - }, - { - source: '/documentation/:path*', - destination: '/en/docs/:path*', - }, - { - source: '/blog/:path*', - destination: '/en/blog/:path*', - }, - { - source: `${localesMatch}community`, - destination: '/:locale/get-involved', - }, - { - source: `${localesMatch}contribute/:path*`, - destination: '/:locale/get-involved', - }, - { - source: `${localesMatch}documentation/:path*`, - destination: '/:locale/docs/:path*', - }, - { - source: '/(atom|feed|rss).xml', - destination: '/en/feed/blog.xml', - }, - { - source: '/feed', - destination: '/en/feed/blog.xml', - }, - { - source: '/feed/release', - destination: '/en/feed/releases.xml', - }, - { - source: '/feed/vulnerability', - destination: '/en/feed/vulnerability.xml', - }, - { - source: '/(static/|)favicon.ico', - destination: '/static/images/favicons/favicon.png', - }, - { - source: '/(static/|)favicon.png', - destination: '/static/images/favicons/favicon.png', - }, - { - source: '/(static/|)apple-touch-icon(.*).png', - destination: '/static/images/favicons/favicon.png', - }, - { - source: '/logos/:path*', - destination: '/static/images/logos/:path*', - }, - ], + afterFiles: siteRedirects.internal.map(({ source, destination }) => ({ + source: source.replace('/:locale/', localesMatch), + destination, + })), }); export { rewrites, redirects }; diff --git a/redirects.json b/redirects.json new file mode 100644 index 0000000000000..d5d48c3f1a5e3 --- /dev/null +++ b/redirects.json @@ -0,0 +1,162 @@ +{ + "external": [ + { + "source": "/index.html", + "destination": "/" + }, + { + "source": "/api.html", + "destination": "/api" + }, + { + "source": "/changelog.html", + "destination": "https://github.com/nodejs/node/blob/HEAD/CHANGELOG.md" + }, + { + "source": "/calendar", + "destination": "https://calendar.google.com/calendar/embed?src=nodejs.org_nr77ama8p7d7f9ajrpnu506c98%40group.calendar.google.com" + }, + { + "source": "/calendar.ics", + "destination": "https://calendar.google.com/calendar/ical/nodejs.org_nr77ama8p7d7f9ajrpnu506c98%40group.calendar.google.com/public/basic.ics" + }, + { + "source": "/:locale/security", + "destination": "https://github.com/nodejs/node/blob/HEAD/SECURITY.md#security" + }, + { + "source": "/:locale/contribute/accepting_contributions.html", + "destination": "https://github.com/nodejs/dev-policy" + }, + { + "source": "/:locale/about/releases", + "destination": "https://github.com/nodejs/release#release-schedule" + }, + { + "source": "/:locale/about/security", + "destination": "https://github.com/nodejs/node/blob/HEAD/SECURITY.md#security" + }, + { + "source": "/:locale/advisory-board", + "destination": "https://github.com/nodejs/TSC" + }, + { + "source": "/:locale/about/advisory-board", + "destination": "https://github.com/nodejs/TSC" + }, + { + "source": "/:locale/about/organization", + "destination": "https://github.com/nodejs/TSC" + }, + { + "source": "/:locale/about/organization/tsc-meetings", + "destination": "https://github.com/nodejs/TSC/tree/HEAD/meetings" + }, + { + "source": "/:locale/about/trademark", + "destination": "https://trademark-policy.openjsf.org" + }, + { + "source": "/:locale/foundation", + "destination": "https://openjsf.org" + }, + { + "source": "/:locale/foundation/case-studies", + "destination": "https://openjsf.org/projects" + }, + { + "source": "/:locale/foundation/members", + "destination": "https://openjsf.org/about/members" + }, + { + "source": "/:locale/foundation/board", + "destination": "https://openjsf.org/about/governance" + }, + { + "source": "/:locale/foundation/tsc", + "destination": "https://github.com/nodejs/TSC" + }, + { + "source": "/:locale/foundation/certification", + "destination": "https://openjsf.org/certification" + }, + { + "source": "/:locale/foundation/in-the-news", + "destination": "https://openjsf.org" + }, + { + "source": "/:locale/foundation/announcements", + "destination": "https://openjsf.org/blog" + }, + { + "source": "/:locale/foundation/education", + "destination": "https://openjsf.org/certification" + } + ], + "internal": [ + { + "source": "/about", + "destination": "/en/about" + }, + { + "source": "/community", + "destination": "/en/get-involved" + }, + { + "source": "/contribute/:path*", + "destination": "/en/get-involved" + }, + { + "source": "/documentation/:path*", + "destination": "/en/docs/:path*" + }, + { + "source": "/blog/:path*", + "destination": "/en/blog/:path*" + }, + { + "source": "/:locale/community", + "destination": "/:locale/get-involved" + }, + { + "source": "/:locale/contribute/:path*", + "destination": "/:locale/get-involved" + }, + { + "source": "/:locale/documentation/:path*", + "destination": "/:locale/docs/:path*" + }, + { + "source": "/(atom|feed|rss).xml", + "destination": "/en/feed/blog.xml" + }, + { + "source": "/feed", + "destination": "/en/feed/blog.xml" + }, + { + "source": "/feed/release", + "destination": "/en/feed/releases.xml" + }, + { + "source": "/feed/vulnerability", + "destination": "/en/feed/vulnerability.xml" + }, + { + "source": "/(static/|)favicon.ico", + "destination": "/static/images/favicons/favicon.png" + }, + { + "source": "/(static/|)favicon.png", + "destination": "/static/images/favicons/favicon.png" + }, + { + "source": "/(static/|)apple-touch-icon(.*).png", + "destination": "/static/images/favicons/favicon.png" + }, + { + "source": "/logos/:path*", + "destination": "/static/images/logos/:path*" + } + ] +}