diff --git a/app/[locale]/feed/[feed]/route.ts b/app/[locale]/feed/[feed]/route.ts
index 6e331a9e3c30e..4007bb97e210e 100644
--- a/app/[locale]/feed/[feed]/route.ts
+++ b/app/[locale]/feed/[feed]/route.ts
@@ -1,21 +1,24 @@
import { NextResponse } from 'next/server';
-import { websiteFeeds } from '@/next.data.mjs';
+import provideWebsiteFeeds from '@/next-data/providers/websiteFeeds';
+import { siteConfig } from '@/next.json.mjs';
import { defaultLocale } from '@/next.locales.mjs';
-type StaticParams = { params: { feed: string } };
+// We only support fetching these pages from the /en/ locale code
+const locale = defaultLocale.code;
+
+type StaticParams = { params: { feed: string; locale: string } };
// This is the Route Handler for the `GET` method which handles the request
// for Blog Feeds within the Node.js Website
// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers
-export const GET = (_: Request, { params }: StaticParams) => {
- if (params.feed.includes('.xml') && websiteFeeds.has(params.feed)) {
- return new NextResponse(websiteFeeds.get(params.feed)?.rss2(), {
- headers: { 'Content-Type': 'application/xml' },
- });
- }
+export const GET = async (_: Request, { params }: StaticParams) => {
+ const websiteFeed = await provideWebsiteFeeds(params.feed);
- return new NextResponse(null, { status: 404 });
+ return new NextResponse(websiteFeed, {
+ headers: { 'Content-Type': 'application/xml' },
+ status: websiteFeed ? 200 : 404,
+ });
};
// This function generates the static paths that come from the dynamic segments
@@ -23,8 +26,8 @@ export const GET = (_: Request, { params }: StaticParams) => {
// this is useful for static exports, for example.
// Note that differently from the App Router these don't get built at the build time
// only if the export is already set for static export
-export const generateStaticParams = () =>
- [...websiteFeeds.keys()].map(feed => ({ feed, locale: defaultLocale.code }));
+export const generateStaticParams = async () =>
+ siteConfig.rssFeeds.map(feed => ({ feed: feed.file, locale }));
// Enforces that this route is used as static rendering
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
diff --git a/app/[locale]/next-data/blog-data/[category]/route.ts b/app/[locale]/next-data/blog-data/[category]/route.ts
new file mode 100644
index 0000000000000..f7893f76d3082
--- /dev/null
+++ b/app/[locale]/next-data/blog-data/[category]/route.ts
@@ -0,0 +1,39 @@
+import provideBlogData from '@/next-data/providers/blogData';
+import { defaultLocale } from '@/next.locales.mjs';
+
+// We only support fetching these pages from the /en/ locale code
+const locale = defaultLocale.code;
+
+type StaticParams = { params: { category: string; locale: string } };
+
+// This is the Route Handler for the `GET` method which handles the request
+// for generating our static data for the Node.js Website
+// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers
+export const GET = async (_: Request, { params }: StaticParams) => {
+ const { posts, pagination } = await provideBlogData(params.category);
+
+ return Response.json(
+ { posts, pagination },
+ { status: posts.length ? 200 : 404 }
+ );
+};
+
+// This function generates the static paths that come from the dynamic segments
+// `en/next-data/[type]` and returns an array of all available static paths
+// this is useful for static exports, for example.
+// Note that differently from the App Router these don't get built at the build time
+// only if the export is already set for static export
+export const generateStaticParams = async () => {
+ const {
+ meta: { categories, pagination },
+ } = await provideBlogData();
+
+ return [
+ ...categories.map(category => ({ category, locale })),
+ ...pagination.map(year => ({ category: `year-${year}`, locale })),
+ ];
+};
+
+// 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';
diff --git a/app/[locale]/next-data/release-data/route.ts b/app/[locale]/next-data/release-data/route.ts
new file mode 100644
index 0000000000000..dce567a5ab71b
--- /dev/null
+++ b/app/[locale]/next-data/release-data/route.ts
@@ -0,0 +1,25 @@
+import provideReleaseData from '@/next-data/providers/releaseData';
+import { defaultLocale } from '@/next.locales.mjs';
+
+// We only support fetching these pages from the /en/ locale code
+const locale = defaultLocale.code;
+
+// This is the Route Handler for the `GET` method which handles the request
+// for generating our static data for the Node.js Website
+// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers
+export const GET = async () => {
+ const releaseData = await provideReleaseData();
+
+ return Response.json(releaseData);
+};
+
+// This function generates the static paths that come from the dynamic segments
+// `en/next-data/[type]` and returns an array of all available static paths
+// this is useful for static exports, for example.
+// Note that differently from the App Router these don't get built at the build time
+// only if the export is already set for static export
+export const generateStaticParams = async () => [{ locale }];
+
+// 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';
diff --git a/components/Docs/NodeApiVersionLinks.tsx b/components/Docs/NodeApiVersionLinks.tsx
index f5bcf42b83882..a9039a496e7b2 100644
--- a/components/Docs/NodeApiVersionLinks.tsx
+++ b/components/Docs/NodeApiVersionLinks.tsx
@@ -1,7 +1,9 @@
+import getReleaseData from '@/next-data/releaseData';
import { DOCS_URL } from '@/next.constants.mjs';
-import { releaseData } from '@/next.data.mjs';
-const NodeApiVersionLinks = () => {
+const NodeApiVersionLinks = async () => {
+ const releaseData = await getReleaseData();
+
// Gets all major releases without the 0x release as those are divided on 0.12x and 0.10x
const mappedReleases = releaseData.slice(0, -1).map(({ major }) => (
diff --git a/components/Downloads/DownloadReleasesTable.tsx b/components/Downloads/DownloadReleasesTable.tsx
index bcb1a1f507357..45e5ed64c0d84 100644
--- a/components/Downloads/DownloadReleasesTable.tsx
+++ b/components/Downloads/DownloadReleasesTable.tsx
@@ -1,12 +1,14 @@
-import { useTranslations } from 'next-intl';
+import { getTranslations } from 'next-intl/server';
import type { FC } from 'react';
-import { releaseData } from '@/next.data.mjs';
+import getReleaseData from '@/next-data/releaseData';
import { getNodeApiLink } from '@/util/getNodeApiLink';
import { getNodejsChangelog } from '@/util/getNodeJsChangelog';
-const DownloadReleasesTable: FC = () => {
- const t = useTranslations();
+const DownloadReleasesTable: FC = async () => {
+ const releaseData = await getReleaseData();
+
+ const t = await getTranslations();
return (
diff --git a/components/Pagination.tsx b/components/Pagination.tsx
index fd99fa1926a6d..55f935cf7614f 100644
--- a/components/Pagination.tsx
+++ b/components/Pagination.tsx
@@ -3,7 +3,7 @@ import type { FC } from 'react';
import Link from '@/components/Link';
-type PaginationProps = { prev?: number; next?: number };
+type PaginationProps = { prev?: number | null; next?: number | null };
const Pagination: FC = ({ next, prev }) => {
const t = useTranslations();
diff --git a/components/withNodeRelease.tsx b/components/withNodeRelease.tsx
index 82527ad8e68e5..26d945b11c31a 100644
--- a/components/withNodeRelease.tsx
+++ b/components/withNodeRelease.tsx
@@ -1,6 +1,6 @@
import type { FC } from 'react';
-import { releaseData } from '@/next.data.mjs';
+import getReleaseData from '@/next-data/releaseData';
import type { NodeRelease, NodeReleaseStatus } from '@/types';
type WithNodeReleaseProps = {
@@ -8,10 +8,12 @@ type WithNodeReleaseProps = {
children: FC<{ release: NodeRelease }>;
};
-export const WithNodeRelease: FC = ({
+export const WithNodeRelease: FC = async ({
status,
children: Component,
}) => {
+ const releaseData = await getReleaseData();
+
const matchingRelease = releaseData.find(release =>
[status].flat().includes(release.status)
);
diff --git a/hooks/react-client/index.ts b/hooks/react-client/index.ts
index c9e4bd771af87..d1f35f867e99f 100644
--- a/hooks/react-client/index.ts
+++ b/hooks/react-client/index.ts
@@ -3,5 +3,4 @@ export { default as useDetectOS } from './useDetectOS';
export { default as useMediaQuery } from './useMediaQuery';
export { default as useNotification } from './useNotification';
export { default as useClientContext } from './useClientContext';
-export { default as useBlogData } from './useBlogData';
export { default as useSiteNavigation } from './useSiteNavigation';
diff --git a/hooks/react-client/useBlogData.ts b/hooks/react-client/useBlogData.ts
deleted file mode 100644
index 69066a0932d55..0000000000000
--- a/hooks/react-client/useBlogData.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-'use client';
-
-const useBlogData = () => {
- throw new Error('Attempted to call useBlogData from RCC');
-};
-
-export default useBlogData;
diff --git a/hooks/react-server/index.ts b/hooks/react-server/index.ts
index c9e4bd771af87..d1f35f867e99f 100644
--- a/hooks/react-server/index.ts
+++ b/hooks/react-server/index.ts
@@ -3,5 +3,4 @@ export { default as useDetectOS } from './useDetectOS';
export { default as useMediaQuery } from './useMediaQuery';
export { default as useNotification } from './useNotification';
export { default as useClientContext } from './useClientContext';
-export { default as useBlogData } from './useBlogData';
export { default as useSiteNavigation } from './useSiteNavigation';
diff --git a/hooks/react-server/useBlogData.ts b/hooks/react-server/useBlogData.ts
deleted file mode 100644
index f10b5fc7c3f5a..0000000000000
--- a/hooks/react-server/useBlogData.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { useMemo } from 'react';
-
-import useClientContext from '@/hooks/react-server/useClientContext';
-import { blogData } from '@/next.data.mjs';
-
-const useBlogData = () => {
- const { pathname } = useClientContext();
-
- const getPostsByCategory = (category: string) =>
- blogData.posts.filter(post => post.category === category);
-
- const getPostsByYear = (year: string) =>
- blogData.posts.filter(
- post => new Date(post.date).getFullYear() === Number(year)
- );
-
- const getPagination = (currentYear: number | string) => {
- const _currentYear = Number(currentYear);
-
- return {
- next: blogData.pagination.includes(_currentYear + 1)
- ? _currentYear + 1
- : undefined,
- prev: blogData.pagination.includes(_currentYear - 1)
- ? _currentYear - 1
- : undefined,
- };
- };
-
- const currentCategory = useMemo(() => {
- // We split the pathname to retrieve the blog category from it since the
- // URL is usually blog/{category} the second path piece is usually the
- // category name
- const [_pathname, category] = pathname.split('/');
-
- if (_pathname === 'blog' && category && category.length) {
- return category;
- }
-
- // if either the pathname does not match to a blog page
- // which should not happen (as this hook should only be used in blog pages)
- // or if there is no category in the URL we return the current year as category name
- // which is always the default category (for example, the blog index)
- return `year-${new Date().getFullYear()}`;
- }, [pathname]);
-
- return {
- posts: blogData.posts,
- categories: blogData.categories,
- currentCategory,
- getPostsByCategory,
- getPostsByYear,
- getPagination,
- };
-};
-
-export default useBlogData;
diff --git a/layouts/BlogCategoryLayout.tsx b/layouts/BlogCategoryLayout.tsx
index a08b9d49d7e2f..e340620e1cec8 100644
--- a/layouts/BlogCategoryLayout.tsx
+++ b/layouts/BlogCategoryLayout.tsx
@@ -1,53 +1,50 @@
-import { useTranslations } from 'next-intl';
-import { useMemo } from 'react';
+import { getTranslations } from 'next-intl/server';
import type { FC } from 'react';
+import { getClientContext } from '@/client-context';
import { Time } from '@/components/Common/Time';
import Link from '@/components/Link';
import Pagination from '@/components/Pagination';
-import { useClientContext, useBlogData } from '@/hooks/server';
-import type { BlogPost } from '@/types';
+import getBlogData from '@/next-data/blogData';
-const BlogCategoryLayout: FC = () => {
- const t = useTranslations();
- const { getPagination, getPostsByYear, getPostsByCategory, currentCategory } =
- useBlogData();
+const getCurrentCategory = (pathname: string) => {
+ // We split the pathname to retrieve the blog category from it since the
+ // URL is usually blog/{category} the second path piece is usually the
+ // category name
+ const [_pathname, category] = pathname.split('/');
- const { frontmatter } = useClientContext();
+ if (_pathname === 'blog' && category && category.length) {
+ return category;
+ }
- const { posts, pagination, title } = useMemo(() => {
- if (currentCategory.startsWith('year-')) {
- const categoryWithoutPrefix = currentCategory.replace('year-', '');
+ // if either the pathname does not match to a blog page
+ // which should not happen (as this hook should only be used in blog pages)
+ // or if there is no category in the URL we return the current year as category name
+ // which is always the default category (for example, the blog index)
+ return `year-${new Date().getFullYear()}`;
+};
+
+const BlogCategoryLayout: FC = async () => {
+ const { frontmatter, pathname } = getClientContext();
+ const category = getCurrentCategory(pathname);
+
+ const t = await getTranslations();
+
+ const { posts, pagination } = await getBlogData(category);
- return {
- posts: getPostsByYear(categoryWithoutPrefix),
- pagination: getPagination(categoryWithoutPrefix),
- title: t('layouts.blogIndex.currentYear', {
- year: categoryWithoutPrefix,
- }),
- };
- }
+ // this only applies if current category is a year category
+ const year = category.replace('year-', '');
- return {
- posts: getPostsByCategory(currentCategory),
- pagination: undefined,
- title: frontmatter.title,
- };
- }, [
- currentCategory,
- frontmatter.title,
- getPagination,
- getPostsByCategory,
- getPostsByYear,
- t,
- ]);
+ const title = category.startsWith('year-')
+ ? t('layouts.blogIndex.currentYear', { year })
+ : frontmatter.title;
return (