From c217ef0445fcedab6912eb72f487662b534f9fb0 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sun, 20 Jul 2025 16:54:00 +0200 Subject: [PATCH 1/7] meta: remove weird fetch/generator thingies, rely on next's internal revalidate api --- apps/site/app/[locale]/layout.tsx | 4 +- apps/site/app/[locale]/page.tsx | 2 +- .../Downloads/DownloadReleasesTable/index.tsx | 11 +++-- apps/site/components/withBlogCrossLinks.tsx | 4 +- apps/site/components/withDownloadSection.tsx | 17 ++++---- apps/site/components/withFooter.tsx | 4 +- apps/site/components/withNodeRelease.tsx | 6 +-- apps/site/layouts/Blog.tsx | 19 +++++---- apps/site/layouts/Download.tsx | 2 +- apps/site/next-data/downloadSnippets.ts | 42 ------------------- apps/site/next-data/providers/blogData.ts | 5 +-- .../next-data/providers/downloadSnippets.ts | 2 +- apps/site/next-data/releaseData.ts | 30 ------------- apps/site/next.constants.mjs | 12 ------ 14 files changed, 39 insertions(+), 121 deletions(-) delete mode 100644 apps/site/next-data/downloadSnippets.ts delete mode 100644 apps/site/next-data/releaseData.ts diff --git a/apps/site/app/[locale]/layout.tsx b/apps/site/app/[locale]/layout.tsx index ed0a60eb0057e..100326a9a3a63 100644 --- a/apps/site/app/[locale]/layout.tsx +++ b/apps/site/app/[locale]/layout.tsx @@ -14,7 +14,9 @@ import '#site/styles/index.css'; const fontClasses = classNames(IBM_PLEX_MONO.variable, OPEN_SANS.variable); -type RotLayoutProps = PropsWithChildren<{ params: { locale: string } }>; +type RotLayoutProps = PropsWithChildren<{ + params: Promise<{ locale: string }>; +}>; const RootLayout: FC = async ({ children, params }) => { const { locale } = await params; diff --git a/apps/site/app/[locale]/page.tsx b/apps/site/app/[locale]/page.tsx index 7e54ad9d66fc4..cf9d20c1de3b3 100644 --- a/apps/site/app/[locale]/page.tsx +++ b/apps/site/app/[locale]/page.tsx @@ -31,7 +31,7 @@ type DynamicParams = { params: Promise }; // This is the default Viewport Metadata // @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function -export const generateViewport = async () => ({ ...PAGE_VIEWPORT }); +export const generateViewport = () => ({ ...PAGE_VIEWPORT }); // This generates each page's HTML Metadata // @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata diff --git a/apps/site/components/Downloads/DownloadReleasesTable/index.tsx b/apps/site/components/Downloads/DownloadReleasesTable/index.tsx index e3d83f154edc3..557d6bd86e039 100644 --- a/apps/site/components/Downloads/DownloadReleasesTable/index.tsx +++ b/apps/site/components/Downloads/DownloadReleasesTable/index.tsx @@ -1,10 +1,10 @@ import Badge from '@node-core/ui-components/Common/Badge'; -import { getTranslations } from 'next-intl/server'; +import { useTranslations } from 'next-intl'; import type { FC } from 'react'; import FormattedTime from '#site/components/Common/FormattedTime'; import DetailsButton from '#site/components/Downloads/DownloadReleasesTable/DetailsButton'; -import getReleaseData from '#site/next-data/releaseData'; +import provideReleaseData from '#site/next-data/providers/releaseData'; const BADGE_KIND_MAP = { 'End-of-life': 'warning', @@ -14,10 +14,9 @@ const BADGE_KIND_MAP = { Pending: 'default', } as const; -const DownloadReleasesTable: FC = async () => { - const releaseData = await getReleaseData(); - - const t = await getTranslations(); +const DownloadReleasesTable: FC = () => { + const releaseData = provideReleaseData(); + const t = useTranslations(); return ( diff --git a/apps/site/components/withBlogCrossLinks.tsx b/apps/site/components/withBlogCrossLinks.tsx index f5d09ed136504..f57963eecaae7 100644 --- a/apps/site/components/withBlogCrossLinks.tsx +++ b/apps/site/components/withBlogCrossLinks.tsx @@ -5,13 +5,13 @@ import CrossLink from '#site/components/Common/CrossLink'; import getBlogData from '#site/next-data/blogData'; import type { BlogCategory } from '#site/types'; -const WithBlogCrossLinks: FC = async () => { +const WithBlogCrossLinks: FC = () => { const { pathname } = getClientContext(); // Extracts from the static URL the components used for the Blog Post slug const [, , category, postname] = pathname.split('/'); - const { posts } = await getBlogData(category as BlogCategory); + const { posts } = getBlogData(category as BlogCategory); const currentItem = posts.findIndex( ({ slug }) => slug === `/blog/${category}/${postname}` diff --git a/apps/site/components/withDownloadSection.tsx b/apps/site/components/withDownloadSection.tsx index 93678cc577d88..34d76d09d6991 100644 --- a/apps/site/components/withDownloadSection.tsx +++ b/apps/site/components/withDownloadSection.tsx @@ -1,10 +1,10 @@ -import { getLocale } from 'next-intl/server'; +import { useLocale } from 'next-intl'; import type { FC, PropsWithChildren } from 'react'; import { getClientContext } from '#site/client-context'; import WithNodeRelease from '#site/components/withNodeRelease'; -import getDownloadSnippets from '#site/next-data/downloadSnippets'; -import getReleaseData from '#site/next-data/releaseData'; +import provideDownloadSnippets from '#site/next-data/providers/downloadSnippets'; +import provideReleaseData from '#site/next-data/providers/releaseData'; import { defaultLocale } from '#site/next.locales.mjs'; import { ReleaseProvider, @@ -13,12 +13,13 @@ import { // By default the translated languages do not contain all the download snippets // Hence we always merge any translated snippet with the fallbacks for missing snippets -const fallbackSnippets = await getDownloadSnippets(defaultLocale.code); +const fallbackSnippets = provideDownloadSnippets(defaultLocale.code); -const WithDownloadSection: FC = async ({ children }) => { - const locale = await getLocale(); - const releases = await getReleaseData(); - const snippets = await getDownloadSnippets(locale); +const WithDownloadSection: FC = ({ children }) => { + const locale = useLocale(); + const releases = provideReleaseData(); + + const snippets = provideDownloadSnippets(locale); const { pathname } = getClientContext(); // Some available translations do not have download snippets translated or have them partially translated diff --git a/apps/site/components/withFooter.tsx b/apps/site/components/withFooter.tsx index 705a347cc36ee..5de6538c844d3 100644 --- a/apps/site/components/withFooter.tsx +++ b/apps/site/components/withFooter.tsx @@ -28,7 +28,7 @@ const WithFooter: FC = async () => { }; const primary = ( - <> +
{({ release }) => ( { )} - +
); return ( diff --git a/apps/site/components/withNodeRelease.tsx b/apps/site/components/withNodeRelease.tsx index bb73bfa8290b7..49b7a38bd0fc0 100644 --- a/apps/site/components/withNodeRelease.tsx +++ b/apps/site/components/withNodeRelease.tsx @@ -1,6 +1,6 @@ import type { FC } from 'react'; -import getReleaseData from '#site/next-data/releaseData'; +import provideReleaseData from '#site/next-data/providers/releaseData'; import type { NodeRelease, NodeReleaseStatus } from '#site/types'; type WithNodeReleaseProps = { @@ -11,11 +11,11 @@ type WithNodeReleaseProps = { // This is a React Async Server Component // Note that Hooks cannot be used in a RSC async component // Async Components do not get re-rendered at all. -const WithNodeRelease: FC = async ({ +const WithNodeRelease: FC = ({ status, children: Component, }) => { - const releaseData = await getReleaseData(); + const releaseData = provideReleaseData(); const matchingRelease = releaseData.find(release => [status].flat().includes(release.status) diff --git a/apps/site/layouts/Blog.tsx b/apps/site/layouts/Blog.tsx index 58d8c089ec921..d6958d8ae6e0a 100644 --- a/apps/site/layouts/Blog.tsx +++ b/apps/site/layouts/Blog.tsx @@ -1,4 +1,4 @@ -import { getTranslations } from 'next-intl/server'; +import { useTranslations } from 'next-intl'; import type { FC } from 'react'; import { getClientContext } from '#site/client-context'; @@ -17,12 +17,15 @@ const getBlogCategory = (pathname: string) => { // hence we attempt to interpolate the full /en/blog/{category}/page/{page} // and in case of course no page argument is provided we define it to 1 // note that malformed routes can't happen as they are all statically generated - const [, , category = 'all', , page = 1] = pathname.split('/'); + const [, , category = 'all', , page = 1] = pathname.split('/') as [ + unknown, + unknown, + BlogCategory, + unknown, + number, + ]; - const { posts, pagination } = getBlogData( - category as BlogCategory, - Number(page) - ); + const { posts, pagination } = getBlogData(category, Number(page)); return { category: category, @@ -32,9 +35,9 @@ const getBlogCategory = (pathname: string) => { }; }; -const BlogLayout: FC = async () => { +const BlogLayout: FC = () => { + const t = useTranslations(); const { pathname } = getClientContext(); - const t = await getTranslations(); const mapCategoriesToTabs = (categories: Array) => categories.map(category => ({ diff --git a/apps/site/layouts/Download.tsx b/apps/site/layouts/Download.tsx index eb0cfdcacdf94..79f589bbf04f7 100644 --- a/apps/site/layouts/Download.tsx +++ b/apps/site/layouts/Download.tsx @@ -7,7 +7,7 @@ import WithNavBar from '#site/components/withNavBar'; import styles from './layouts.module.css'; -const DownloadLayout: FC = async ({ children }) => { +const DownloadLayout: FC = ({ children }) => { const { frontmatter } = getClientContext(); return ( diff --git a/apps/site/next-data/downloadSnippets.ts b/apps/site/next-data/downloadSnippets.ts deleted file mode 100644 index 48dab66a9ac56..0000000000000 --- a/apps/site/next-data/downloadSnippets.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - ENABLE_STATIC_EXPORT, - IS_NOT_VERCEL_RUNTIME_ENV, - NEXT_DATA_URL, -} from '#site/next.constants.mjs'; -import { availableLocaleCodes } from '#site/next.locales.mjs'; -import type { DownloadSnippet } from '#site/types'; - -export default async function getDownloadSnippets(lang: string) { - // Prevents attempting to retrieve data for an unsupported language as both the generator - // and the API endpoint will simply return 404. And we want to prevent a 404 response. - if (!availableLocaleCodes.includes(lang)) { - return []; - } - - // When we're using Static Exports the Next.js Server is not running (during build-time) - // hence the self-ingestion APIs will not be available. In this case we want to load - // the data directly within the current thread, which will anyways be loaded only once - // We use lazy-imports to prevent `provideBlogData` from executing on import - if (ENABLE_STATIC_EXPORT || IS_NOT_VERCEL_RUNTIME_ENV) { - const { default: provideDownloadSnippets } = await import( - '#site/next-data/providers/downloadSnippets' - ); - return provideDownloadSnippets(lang)!; - } - - // Applies the language to the URL, since this content is actually localized - const LOCALIZED_NEXT_DATA = NEXT_DATA_URL.replace( - '/en/next-data/', - `/${lang}/next-data/` - ); - - const fetchURL = `${LOCALIZED_NEXT_DATA}download-snippets`; - - // This data cannot be cached because it is continuously updated. Caching it would lead to - // outdated information being shown to the user. - // Note: We do manual JSON.parse after response.text() to prevent React from throwing an Error - // that does not provide a clear stack trace of which request is failing and what the JSON.parse error is - const response = await fetch(fetchURL); - - return JSON.parse(await response.text()) as Array; -} diff --git a/apps/site/next-data/providers/blogData.ts b/apps/site/next-data/providers/blogData.ts index 27f00e3f4e322..a73c4ed2d0b2c 100644 --- a/apps/site/next-data/providers/blogData.ts +++ b/apps/site/next-data/providers/blogData.ts @@ -5,10 +5,7 @@ import { blogData } from '#site/next.json.mjs'; import type { BlogCategory, BlogPostsRSC } from '#site/types'; const blogPosts = cache(() => - blogData.posts.map(post => ({ - ...post, - date: new Date(post.date), - })) + blogData.posts.map(post => ({ ...post, date: new Date(post.date) })) ); export const provideBlogPosts = cache( diff --git a/apps/site/next-data/providers/downloadSnippets.ts b/apps/site/next-data/providers/downloadSnippets.ts index a7aa7ff7b3d9f..3467d02838275 100644 --- a/apps/site/next-data/providers/downloadSnippets.ts +++ b/apps/site/next-data/providers/downloadSnippets.ts @@ -9,7 +9,7 @@ const provideDownloadSnippets = cache((language: string) => { return downloadSnippets.get(language)!; } - return undefined; + return []; }); export default provideDownloadSnippets; diff --git a/apps/site/next-data/releaseData.ts b/apps/site/next-data/releaseData.ts deleted file mode 100644 index 382a99cd08682..0000000000000 --- a/apps/site/next-data/releaseData.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - ENABLE_STATIC_EXPORT, - NEXT_DATA_URL, - IS_NOT_VERCEL_RUNTIME_ENV, -} from '#site/next.constants.mjs'; -import type { NodeRelease } from '#site/types'; - -const getReleaseData = (): Promise> => { - // When we're using Static Exports the Next.js Server is not running (during build-time) - // hence the self-ingestion APIs will not be available. In this case we want to load - // the data directly within the current thread, which will anyways be loaded only once - // We use lazy-imports to prevent `provideBlogData` from executing on import - if (ENABLE_STATIC_EXPORT || IS_NOT_VERCEL_RUNTIME_ENV) { - return import('#site/next-data/providers/releaseData').then( - ({ default: provideReleaseData }) => provideReleaseData() - ); - } - - const fetchURL = `${NEXT_DATA_URL}release-data`; - - // This data cannot be cached because it is continuously updated. Caching it would lead to - // outdated information being shown to the user. - // Note: We do manual JSON.parse after response.text() to prevent React from throwing an Error - // that does not provide a clear stack trace of which request is failing and what the JSON.parse error is - return fetch(fetchURL) - .then(response => response.text()) - .then(JSON.parse); -}; - -export default getReleaseData; diff --git a/apps/site/next.constants.mjs b/apps/site/next.constants.mjs index 309093b7b5dcd..52439c1454bee 100644 --- a/apps/site/next.constants.mjs +++ b/apps/site/next.constants.mjs @@ -23,18 +23,6 @@ export const VERCEL_ENV = process.env.VERCEL_ENV || undefined; */ export const VERCEL_REGION = process.env.VERCEL_REGION || undefined; -/** - * This constant determines if the current environment is NOT a Vercel runtime environment. - * - * The logic is as follows: - * - If we are NOT in a development environment (`!IS_DEV_ENV`) AND: - * - Vercel environment variable (`VERCEL_ENV`) is defined but the Vercel region (`VERCEL_REGION`) is NOT defined, OR - * - Vercel environment variable (`VERCEL_ENV`) is NOT defined at all. - * This helps identify cases where the application is running outside of Vercel's runtime environment. - */ -export const IS_NOT_VERCEL_RUNTIME_ENV = - !IS_DEV_ENV && ((VERCEL_ENV && !VERCEL_REGION) || !VERCEL_ENV); - /** * This is used for telling Next.js to do a Static Export Build of the Website * From dc3fe4d31c1adce824119956b0e3d42ca861690d Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sun, 20 Jul 2025 17:02:55 +0200 Subject: [PATCH 2/7] chore: remove unused vars --- apps/site/next.constants.mjs | 25 ++----------------------- apps/site/turbo.json | 4 ---- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/apps/site/next.constants.mjs b/apps/site/next.constants.mjs index 52439c1454bee..313b682d54aa9 100644 --- a/apps/site/next.constants.mjs +++ b/apps/site/next.constants.mjs @@ -14,15 +14,6 @@ export const IS_DEV_ENV = process.env.NODE_ENV === 'development'; */ export const VERCEL_ENV = process.env.VERCEL_ENV || undefined; -/** - * This is used for telling Next.js if we are current during build time or in runtime environment - * - * Can be used for conditionally enabling features that we know are Vercel only - * - * @see https://vercel.com/docs/projects/environment-variables/system-environment-variables#VERCEL_REGION - */ -export const VERCEL_REGION = process.env.VERCEL_REGION || undefined; - /** * This is used for telling Next.js to do a Static Export Build of the Website * @@ -52,6 +43,8 @@ export const ENABLE_STATIC_EXPORT_LOCALE = * This variable can either come from the Vercel Deployment as `NEXT_PUBLIC_VERCEL_URL` or from the `NEXT_PUBLIC_BASE_URL` Environment Variable that is manually defined * by us if necessary. Otherwise it will fallback to the default Node.js Website URL. * + * @TODO: We should get rid of needing to rely on `VERCEL_URL` for deployment URL. + * * @see https://vercel.com/docs/concepts/projects/environment-variables/system-environment-variables#framework-environment-variables */ export const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL @@ -86,20 +79,6 @@ export const DOCS_URL = */ export const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH || ''; -/** - * This is used for fetching static next-data through the /en/next-data/ endpoint - * - * Note this is assumes that the Node.js Website is either running within Vercel Environment - * or running locally (either production or development) mode - * - * Note this variable can be overridden via a manual Environment Variable defined by us if necessary. - */ -export const NEXT_DATA_URL = process.env.NEXT_PUBLIC_DATA_URL - ? process.env.NEXT_PUBLIC_DATA_URL - : VERCEL_ENV - ? `${BASE_URL}${BASE_PATH}/en/next-data/` - : `http://localhost:${process.env.PORT ?? 3000}/en/next-data/`; - /** * This is the default type of blog post type that we use for OG Meta Tags */ diff --git a/apps/site/turbo.json b/apps/site/turbo.json index b4211a13e6be0..758d117192464 100644 --- a/apps/site/turbo.json +++ b/apps/site/turbo.json @@ -10,7 +10,6 @@ "env": [ "VERCEL_ENV", "VERCEL_URL", - "VERCEL_REGION", "NEXT_PUBLIC_STATIC_EXPORT", "NEXT_PUBLIC_STATIC_EXPORT_LOCALE", "NEXT_PUBLIC_BASE_URL", @@ -37,7 +36,6 @@ "env": [ "VERCEL_ENV", "VERCEL_URL", - "VERCEL_REGION", "NEXT_PUBLIC_STATIC_EXPORT", "NEXT_PUBLIC_STATIC_EXPORT_LOCALE", "NEXT_PUBLIC_BASE_URL", @@ -57,7 +55,6 @@ "env": [ "VERCEL_ENV", "VERCEL_URL", - "VERCEL_REGION", "NEXT_PUBLIC_STATIC_EXPORT", "NEXT_PUBLIC_STATIC_EXPORT_LOCALE", "NEXT_PUBLIC_BASE_URL", @@ -83,7 +80,6 @@ "env": [ "VERCEL_ENV", "VERCEL_URL", - "VERCEL_REGION", "NEXT_PUBLIC_STATIC_EXPORT", "NEXT_PUBLIC_STATIC_EXPORT_LOCALE", "NEXT_PUBLIC_BASE_URL", From 66a1dabc1c95cd1e8b7f4c93e6eb5b9c0bb3139f Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sun, 20 Jul 2025 17:05:17 +0200 Subject: [PATCH 3/7] chore: remove unecessary next-data endpoints --- .../next-data/download-snippets/route.tsx | 37 ------------------- .../[locale]/next-data/release-data/route.ts | 31 ---------------- 2 files changed, 68 deletions(-) delete mode 100644 apps/site/app/[locale]/next-data/download-snippets/route.tsx delete mode 100644 apps/site/app/[locale]/next-data/release-data/route.ts diff --git a/apps/site/app/[locale]/next-data/download-snippets/route.tsx b/apps/site/app/[locale]/next-data/download-snippets/route.tsx deleted file mode 100644 index 90232982a2d45..0000000000000 --- a/apps/site/app/[locale]/next-data/download-snippets/route.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import provideDownloadSnippets from '#site/next-data/providers/downloadSnippets'; -import { defaultLocale } from '#site/next.locales.mjs'; - -type DynamicStaticPaths = { locale: string }; -type StaticParams = { params: Promise }; - -// This is the Route Handler for the `GET` method which handles the request -// for generating JSON data for Download Snippets -// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers -export const GET = async (_: Request, props: StaticParams) => { - const params = await props.params; - - // Retrieve all available Download snippets for a given locale if available - const snippets = provideDownloadSnippets(params.locale); - - // We append always the default/fallback snippets when a result is found - return Response.json(snippets, { - status: snippets !== undefined ? 200 : 404, - }); -}; - -// This function generates the static paths that come from the dynamic segments -// `[locale]/next-data/download-snippets/` this will return a default value as we don't want to -// statically generate this route as it is compute-expensive. -// Hence we generate a fake route just to satisfy Next.js requirements. -export const generateStaticParams = async () => [ - { locale: defaultLocale.code }, -]; - -// Enforces that this route is cached and static as much as possible -// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic -export const dynamic = 'force-static'; - -// Ensures that this endpoint is invalidated and re-executed every X minutes -// so that when new deployments happen, the data is refreshed -// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#revalidate -export const revalidate = 300; diff --git a/apps/site/app/[locale]/next-data/release-data/route.ts b/apps/site/app/[locale]/next-data/release-data/route.ts deleted file mode 100644 index 8b4d229f81066..0000000000000 --- a/apps/site/app/[locale]/next-data/release-data/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -import provideReleaseData from '#site/next-data/providers/releaseData'; -import { defaultLocale } from '#site/next.locales.mjs'; - -// This is the Route Handler for the `GET` method which handles the request -// for generating static data related to the Node.js Release Data -// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers -export const GET = async () => { - const releaseData = provideReleaseData(); - - return Response.json(releaseData); -}; - -// This function generates the static paths that come from the dynamic segments -// `[locale]/next-data/release-data/` and returns an array of all available static paths -// This is used for ISR static validation and generation -export const generateStaticParams = async () => [ - { locale: defaultLocale.code }, -]; - -// Enforces that only the paths from `generateStaticParams` are allowed, giving 404 on the contrary -// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams -export const dynamicParams = false; - -// Enforces that this route is used as static rendering -// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic -export const dynamic = 'error'; - -// Ensures that this endpoint is invalidated and re-executed every X minutes -// so that when new deployments happen, the data is refreshed -// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#revalidate -export const revalidate = 300; From 13b83df90c00bae34896527bd28f56adaedf3b01 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sun, 20 Jul 2025 17:10:49 +0200 Subject: [PATCH 4/7] chore: tiny cleanup --- apps/site/components/withBlogCrossLinks.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/site/components/withBlogCrossLinks.tsx b/apps/site/components/withBlogCrossLinks.tsx index f57963eecaae7..c128c919dfb03 100644 --- a/apps/site/components/withBlogCrossLinks.tsx +++ b/apps/site/components/withBlogCrossLinks.tsx @@ -9,9 +9,14 @@ const WithBlogCrossLinks: FC = () => { const { pathname } = getClientContext(); // Extracts from the static URL the components used for the Blog Post slug - const [, , category, postname] = pathname.split('/'); + const [, , category, postname] = pathname.split('/') as [ + unknown, + unknown, + BlogCategory, + string, + ]; - const { posts } = getBlogData(category as BlogCategory); + const { posts } = getBlogData(category); const currentItem = posts.findIndex( ({ slug }) => slug === `/blog/${category}/${postname}` From d5b1aa138139cf2cc75852a41b07d50aa380f0e1 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sun, 20 Jul 2025 17:23:32 +0200 Subject: [PATCH 5/7] chore: minor failsafes and cleanup --- .../app/[locale]/next-data/api-data/route.ts | 59 ++++++++++------- .../app/[locale]/next-data/page-data/route.ts | 66 +++++++++++-------- 2 files changed, 71 insertions(+), 54 deletions(-) diff --git a/apps/site/app/[locale]/next-data/api-data/route.ts b/apps/site/app/[locale]/next-data/api-data/route.ts index 4ba97056079e4..2e469ec77f717 100644 --- a/apps/site/app/[locale]/next-data/api-data/route.ts +++ b/apps/site/app/[locale]/next-data/api-data/route.ts @@ -32,31 +32,40 @@ export const GET = async () => { authorizationHeaders ); - return gitHubApiResponse.json().then((apiDocsFiles: Array) => { - // maps over each api file and get the download_url, fetch the content and deflates it - const mappedApiFiles = apiDocsFiles.map( - async ({ name, path: filename, download_url }) => { - const apiFileResponse = await fetch(download_url); - - // Retrieves the content as a raw text string - const source = await apiFileResponse.text(); - - // Removes empty/blank lines or lines just with spaces and trims each line - // from leading and trailing paddings/spaces - const cleanedContent = parseRichTextIntoPlainText(source); - - const deflatedSource = deflateSync(cleanedContent).toString('base64'); - - return { - filename: filename, - pathname: getPathnameForApiFile(name, versionWithPrefix), - content: deflatedSource, - }; - } - ); - - return Promise.all(mappedApiFiles).then(Response.json); - }); + // transforms the response into an array of GitHubApiFile + const apiDocsFiles: Array = await gitHubApiResponse.json(); + + // prevent the route from crashing if the response is not an array of GitHubApiFile + // and return an empty array instead. This is a fallback for when the GitHub API is not available. + if (!Array.isArray(apiDocsFiles)) { + return Response.json([]); + } + + // maps over each api file and get the download_url, fetch the content and deflates it + const mappedApiFiles = apiDocsFiles.map( + async ({ name, path: filename, download_url }) => { + const apiFileResponse = await fetch(download_url); + + // Retrieves the content as a raw text string + const source = await apiFileResponse.text(); + + // Removes empty/blank lines or lines just with spaces and trims each line + // from leading and trailing paddings/spaces + const cleanedContent = parseRichTextIntoPlainText(source); + + const deflatedSource = deflateSync(cleanedContent).toString('base64'); + + return { + filename: filename, + pathname: getPathnameForApiFile(name, versionWithPrefix), + content: deflatedSource, + }; + } + ); + + const data = await Promise.all(mappedApiFiles); + + return Response.json(data); }; // This function generates the static paths that come from the dynamic segments diff --git a/apps/site/app/[locale]/next-data/page-data/route.ts b/apps/site/app/[locale]/next-data/page-data/route.ts index ff1f155bfdf2b..a9ad5a7e4416c 100644 --- a/apps/site/app/[locale]/next-data/page-data/route.ts +++ b/apps/site/app/[locale]/next-data/page-data/route.ts @@ -10,44 +10,52 @@ import { parseRichTextIntoPlainText } from '#site/util/string'; // for a digest and metadata of all existing pages on Node.js Website // @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers export const GET = async () => { + // Retrieves all available routes for the default locale const allAvailbleRoutes = await dynamicRouter.getRoutesByLanguage( defaultLocale.code ); - const availablePagesMetadata = allAvailbleRoutes - .filter(route => !route.startsWith('blog')) - .map(async pathname => { - const { source, filename } = await dynamicRouter.getMarkdownFile( - defaultLocale.code, - pathname - ); + // We exclude the blog routes from the available pages metadata + // as they are generated separately and are not part of the static pages + // and are not part of the static pages metadata + const routesExceptBlog = allAvailbleRoutes.filter( + route => !route.startsWith('blog') + ); + + const availablePagesMetadata = routesExceptBlog.map(async pathname => { + const { source, filename } = await dynamicRouter.getMarkdownFile( + defaultLocale.code, + pathname + ); + + // Gets the title and the Description from the Page Metadata + const { title, description } = await dynamicRouter.getPageMetadata( + defaultLocale.code, + pathname + ); - // Gets the title and the Description from the Page Metadata - const { title, description } = await dynamicRouter.getPageMetadata( - defaultLocale.code, - pathname - ); + // Parser the Markdown source with `gray-matter` and then only + // grabs the markdown content and cleanses it by removing HTML/JSX tags + // removing empty/blank lines or lines just with spaces and trims each line + // from leading and trailing paddings/spaces + const cleanedContent = parseRichTextIntoPlainText(matter(source).content); - // Parser the Markdown source with `gray-matter` and then only - // grabs the markdown content and cleanses it by removing HTML/JSX tags - // removing empty/blank lines or lines just with spaces and trims each line - // from leading and trailing paddings/spaces - const cleanedContent = parseRichTextIntoPlainText(matter(source).content); + // Deflates a String into a base64 string-encoded (zlib compressed) + const content = deflateSync(cleanedContent).toString('base64'); - // Deflates a String into a base64 string-encoded (zlib compressed) - const content = deflateSync(cleanedContent).toString('base64'); + // Returns metadata of each page available on the Website + return { + filename, + pathname, + title, + description, + content, + }; + }); - // Returns metadata of each page available on the Website - return { - filename, - pathname, - title, - description, - content, - }; - }); + const data = await Promise.all(availablePagesMetadata); - return Response.json(await Promise.all(availablePagesMetadata)); + return Response.json(data); }; // This function generates the static paths that come from the dynamic segments From 6fded377ea13d25a835ac7d55053ada391d51641 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sun, 20 Jul 2025 19:56:57 +0200 Subject: [PATCH 6/7] Update layout.tsx Signed-off-by: Claudio Wunder --- apps/site/app/[locale]/layout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/site/app/[locale]/layout.tsx b/apps/site/app/[locale]/layout.tsx index 100326a9a3a63..3faa14079216a 100644 --- a/apps/site/app/[locale]/layout.tsx +++ b/apps/site/app/[locale]/layout.tsx @@ -14,11 +14,11 @@ import '#site/styles/index.css'; const fontClasses = classNames(IBM_PLEX_MONO.variable, OPEN_SANS.variable); -type RotLayoutProps = PropsWithChildren<{ +type RootLayoutProps = PropsWithChildren<{ params: Promise<{ locale: string }>; }>; -const RootLayout: FC = async ({ children, params }) => { +const RootLayout: FC = async ({ children, params }) => { const { locale } = await params; const { langDir, hrefLang } = availableLocalesMap[locale] || defaultLocale; From 044454c302418cb559f7ae29e2396755866860a5 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sun, 20 Jul 2025 21:16:06 +0200 Subject: [PATCH 7/7] fix: fix current weirdness from `main` --- apps/site/app/[locale]/not-found.tsx | 2 ++ apps/site/components/withFooter.tsx | 15 ++++----------- apps/site/navigation.json | 8 ++++---- packages/i18n/src/locales/en.json | 1 + 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/apps/site/app/[locale]/not-found.tsx b/apps/site/app/[locale]/not-found.tsx index e97bdfef7032d..2caeb07a589f1 100644 --- a/apps/site/app/[locale]/not-found.tsx +++ b/apps/site/app/[locale]/not-found.tsx @@ -1,3 +1,5 @@ +'use client'; + import { ArrowRightIcon } from '@heroicons/react/24/solid'; import Image from 'next/image'; import { useTranslations } from 'next-intl'; diff --git a/apps/site/components/withFooter.tsx b/apps/site/components/withFooter.tsx index 5de6538c844d3..c5bc06e8a02e3 100644 --- a/apps/site/components/withFooter.tsx +++ b/apps/site/components/withFooter.tsx @@ -1,6 +1,6 @@ import BadgeGroup from '@node-core/ui-components/Common/BadgeGroup'; import Footer from '@node-core/ui-components/Containers/Footer'; -import { getTranslations } from 'next-intl/server'; +import { useTranslations } from 'next-intl'; import type { FC } from 'react'; import { getClientContext } from '#site/client-context'; @@ -9,22 +9,15 @@ import { siteNavigation } from '#site/next.json.mjs'; import WithNodeRelease from './withNodeRelease'; -const WithFooter: FC = async () => { - const t = await getTranslations(); +const WithFooter: FC = () => { + const t = useTranslations(); const { pathname } = getClientContext(); const { socialLinks, footerLinks } = siteNavigation; - const updatedFooterLinks = footerLinks - .slice(0, -1) - .map(link => ({ ...link, text: t(link.text) })); - - // Add OpenJS link - updatedFooterLinks.push(footerLinks.at(-1)!); - const navigation = { socialLinks: socialLinks, - footerLinks: updatedFooterLinks, + footerLinks: footerLinks.map(link => ({ ...link, text: t(link.text) })), }; const primary = ( diff --git a/apps/site/navigation.json b/apps/site/navigation.json index 598019b9c4d6e..c4ea25b978b7c 100644 --- a/apps/site/navigation.json +++ b/apps/site/navigation.json @@ -32,6 +32,10 @@ } }, "footerLinks": [ + { + "link": "https://openjsf.org/", + "text": "components.containers.footer.links.openJSFoundation" + }, { "link": "https://trademark-policy.openjsf.org/", "text": "components.containers.footer.links.trademarkPolicy" @@ -51,10 +55,6 @@ { "link": "https://github.com/nodejs/node/security/policy", "text": "components.containers.footer.links.security" - }, - { - "link": "https://openjsf.org/", - "text": "OpenJS Foundation" } ], "socialLinks": [ diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json index eb3ff873d6a72..cadcca42859f3 100644 --- a/packages/i18n/src/locales/en.json +++ b/packages/i18n/src/locales/en.json @@ -3,6 +3,7 @@ "containers": { "footer": { "links": { + "openJSFoundation": "OpenJS Foundation", "trademarkPolicy": "Trademark Policy", "privacyPolicy": "Privacy Policy", "versionSupport": "Version Support",