-4
.eslintignore
-4
.eslintignore
-4
.husky/pre-commit
-4
.husky/pre-commit
-2
.prettierignore
-2
.prettierignore
+11
-4
app/[locale]/[[...path]]/page.tsx
+11
-4
app/[locale]/[[...path]]/page.tsx
···
5
5
import { setClientContext } from '@/client-context';
6
6
import { MDXRenderer } from '@/components/mdxRenderer';
7
7
import { WithLayout } from '@/components/withLayout';
8
-
import { DEFAULT_VIEWPORT, ENABLE_STATIC_EXPORT } from '@/next.constants.mjs';
8
+
import { ENABLE_STATIC_EXPORT } from '@/next.constants.mjs';
9
+
import { DEFAULT_VIEWPORT } from '@/next.dynamic.constants.mjs';
9
10
import { dynamicRouter } from '@/next.dynamic.mjs';
10
11
import { availableLocaleCodes, defaultLocale } from '@/next.locales.mjs';
11
12
import { MatterProvider } from '@/providers/matterProvider';
···
14
15
type DynamicParams = { params: DynamicStaticPaths };
15
16
16
17
// This is the default Viewport Metadata
17
-
// @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport
18
-
export const viewport = DEFAULT_VIEWPORT;
18
+
// @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function
19
+
export const generateViewport = async () => ({ ...DEFAULT_VIEWPORT });
19
20
20
21
// This generates each page's HTML Metadata
21
22
// @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata
···
113
114
return notFound();
114
115
};
115
116
116
-
// Enforce that all these routes are compatible with SSR
117
+
// In this case we want to catch-all possible pages even to this page. This ensures that we use our 404
118
+
// and that all pages including existing ones are handled here and provide `next-intl` locale also
119
+
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams
120
+
export const dynamicParams = true;
121
+
122
+
// Enforces that this route is used as static rendering
123
+
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
117
124
export const dynamic = 'error';
118
125
119
126
export default getPage;
+22
-20
app/[locale]/feed/[feed]/route.ts
+22
-20
app/[locale]/feed/[feed]/route.ts
···
1
1
import { NextResponse } from 'next/server';
2
2
3
-
import { generateWebsiteFeeds } from '@/next.data.mjs';
4
-
import { blogData } from '@/next.json.mjs';
3
+
import provideWebsiteFeeds from '@/next-data/providers/websiteFeeds';
4
+
import { siteConfig } from '@/next.json.mjs';
5
5
import { defaultLocale } from '@/next.locales.mjs';
6
6
7
-
// loads all the data from the blog-posts-data.json file
8
-
const websiteFeeds = generateWebsiteFeeds(blogData);
7
+
// We only support fetching these pages from the /en/ locale code
8
+
const locale = defaultLocale.code;
9
9
10
-
type StaticParams = { params: { feed: string } };
10
+
type StaticParams = { params: { feed: string; locale: string } };
11
11
12
12
// This is the Route Handler for the `GET` method which handles the request
13
-
// for Blog Feeds within the Node.js Website
13
+
// for the Node.js Website Blog Feeds (RSS)
14
14
// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers
15
-
export const GET = (_: Request, { params }: StaticParams) => {
16
-
if (params.feed.includes('.xml') && websiteFeeds.has(params.feed)) {
17
-
return new NextResponse(websiteFeeds.get(params.feed)?.rss2(), {
18
-
headers: { 'Content-Type': 'application/xml' },
19
-
});
20
-
}
15
+
export const GET = async (_: Request, { params }: StaticParams) => {
16
+
// Generate the Feed for the given feed type (blog, releases, etc)
17
+
const websiteFeed = await provideWebsiteFeeds(params.feed);
21
18
22
-
return new NextResponse(null, { status: 404 });
19
+
return new NextResponse(websiteFeed, {
20
+
headers: { 'Content-Type': 'application/xml' },
21
+
status: websiteFeed ? 200 : 404,
22
+
});
23
23
};
24
24
25
25
// This function generates the static paths that come from the dynamic segments
26
-
// `en/feeds/[feed]` and returns an array of all available static paths
27
-
// this is useful for static exports, for example.
28
-
// Note that differently from the App Router these don't get built at the build time
29
-
// only if the export is already set for static export
30
-
export const generateStaticParams = () =>
31
-
[...websiteFeeds.keys()].map(feed => ({ feed, locale: defaultLocale.code }));
26
+
// `[locale]/feeds/[feed]` and returns an array of all available static paths
27
+
// This is used for ISR static validation and generation
28
+
export const generateStaticParams = async () =>
29
+
siteConfig.rssFeeds.map(feed => ({ feed: feed.file, locale }));
30
+
31
+
// Forces that only the paths from `generateStaticParams` are allowed, giving 404 on the contrary
32
+
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams
33
+
export const dynamicParams = false;
32
34
33
-
// Enforces that this route is used as static rendering
35
+
// Enforces that this route is cached and static as much as possible
34
36
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
35
37
export const dynamic = 'error';
+42
app/[locale]/next-data/blog-data/[category]/route.ts
+42
app/[locale]/next-data/blog-data/[category]/route.ts
···
1
+
import provideBlogData from '@/next-data/providers/blogData';
2
+
import { defaultLocale } from '@/next.locales.mjs';
3
+
4
+
// We only support fetching these pages from the /en/ locale code
5
+
const locale = defaultLocale.code;
6
+
7
+
type StaticParams = { params: { category: string; locale: string } };
8
+
9
+
// This is the Route Handler for the `GET` method which handles the request
10
+
// for providing Blog Posts, Pagination for every supported Blog Category
11
+
// this includes the `year-XXXX` categories for yearly archives (pagination)
12
+
// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers
13
+
export const GET = async (_: Request, { params }: StaticParams) => {
14
+
const { posts, pagination } = await provideBlogData(params.category);
15
+
16
+
return Response.json(
17
+
{ posts, pagination },
18
+
{ status: posts.length ? 200 : 404 }
19
+
);
20
+
};
21
+
22
+
// This function generates the static paths that come from the dynamic segments
23
+
// `[locale]/next-data/blog-data/[category]` and returns an array of all available static paths
24
+
// This is used for ISR static validation and generation
25
+
export const generateStaticParams = async () => {
26
+
// This metadata is the original list of all available categories and all available years
27
+
// within the Node.js Website Blog Posts (2011, 2012...)
28
+
const { meta } = await provideBlogData();
29
+
30
+
return [
31
+
...meta.categories.map(category => ({ category, locale })),
32
+
...meta.pagination.map(year => ({ category: `year-${year}`, locale })),
33
+
];
34
+
};
35
+
36
+
// Forces that only the paths from `generateStaticParams` are allowed, giving 404 on the contrary
37
+
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams
38
+
export const dynamicParams = false;
39
+
40
+
// Enforces that this route is cached and static as much as possible
41
+
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
42
+
export const dynamic = 'error';
+27
app/[locale]/next-data/release-data/route.ts
+27
app/[locale]/next-data/release-data/route.ts
···
1
+
import provideReleaseData from '@/next-data/providers/releaseData';
2
+
import { defaultLocale } from '@/next.locales.mjs';
3
+
4
+
// We only support fetching these pages from the /en/ locale code
5
+
const locale = defaultLocale.code;
6
+
7
+
// This is the Route Handler for the `GET` method which handles the request
8
+
// for generating static data related to the Node.js Release Data
9
+
// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers
10
+
export const GET = async () => {
11
+
const releaseData = await provideReleaseData();
12
+
13
+
return Response.json(releaseData);
14
+
};
15
+
16
+
// This function generates the static paths that come from the dynamic segments
17
+
// `[locale]/next-data/release-data/` and returns an array of all available static paths
18
+
// This is used for ISR static validation and generation
19
+
export const generateStaticParams = async () => [{ locale }];
20
+
21
+
// Forces that only the paths from `generateStaticParams` are allowed, giving 404 on the contrary
22
+
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams
23
+
export const dynamicParams = false;
24
+
25
+
// Enforces that this route is used as static rendering
26
+
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
27
+
export const dynamic = 'error';
+3
-4
app/sitemap.ts
+3
-4
app/sitemap.ts
···
18
18
const paths: string[] = [];
19
19
20
20
for (const locale of availableLocaleCodes) {
21
-
const routesForLanguage = await dynamicRouter.getRoutesByLanguage(locale);
22
-
paths.push(
23
-
...routesForLanguage.map(route => `${baseUrlAndPath}/${locale}/${route}`)
24
-
);
21
+
const routes = await dynamicRouter.getRoutesByLanguage(locale);
22
+
23
+
paths.push(...routes.map(route => `${baseUrlAndPath}/${locale}/${route}`));
25
24
}
26
25
27
26
const currentDate = new Date().toISOString();
+9
-2
components/Docs/NodeApiVersionLinks.tsx
+9
-2
components/Docs/NodeApiVersionLinks.tsx
···
1
+
import type { FC } from 'react';
2
+
3
+
import getReleaseData from '@/next-data/releaseData';
1
4
import { DOCS_URL } from '@/next.constants.mjs';
2
-
import { releaseData } from '@/next.json.mjs';
5
+
6
+
// This is a React Async Server Component
7
+
// Note that Hooks cannot be used in a RSC async component
8
+
// Async Components do not get re-rendered at all.
9
+
const NodeApiVersionLinks: FC = async () => {
10
+
const releaseData = await getReleaseData();
3
11
4
-
const NodeApiVersionLinks = () => {
5
12
// Gets all major releases without the 0x release as those are divided on 0.12x and 0.10x
6
13
const mappedReleases = releaseData.slice(0, -1).map(({ major }) => (
7
14
<li key={major}>
+9
-4
components/Downloads/DownloadReleasesTable.tsx
+9
-4
components/Downloads/DownloadReleasesTable.tsx
···
1
-
import { useTranslations } from 'next-intl';
1
+
import { getTranslations } from 'next-intl/server';
2
2
import type { FC } from 'react';
3
3
4
-
import { releaseData } from '@/next.json.mjs';
4
+
import getReleaseData from '@/next-data/releaseData';
5
5
import { getNodeApiLink } from '@/util/getNodeApiLink';
6
6
import { getNodejsChangelog } from '@/util/getNodeJsChangelog';
7
7
8
-
const DownloadReleasesTable: FC = () => {
9
-
const t = useTranslations();
8
+
// This is a React Async Server Component
9
+
// Note that Hooks cannot be used in a RSC async component
10
+
// Async Components do not get re-rendered at all.
11
+
const DownloadReleasesTable: FC = async () => {
12
+
const releaseData = await getReleaseData();
13
+
14
+
const t = await getTranslations();
10
15
11
16
return (
12
17
<table id="tbVersions" className="download-table full-width">
+3
-15
components/Header.tsx
+3
-15
components/Header.tsx
···
3
3
import Image from 'next/image';
4
4
import { useTranslations } from 'next-intl';
5
5
import { useTheme } from 'next-themes';
6
+
import type { FC, PropsWithChildren } from 'react';
6
7
import { useState } from 'react';
7
8
8
-
import ActiveLink from '@/components/Common/ActiveLink';
9
9
import Link from '@/components/Link';
10
-
import { useSiteNavigation } from '@/hooks';
11
10
import { usePathname } from '@/navigation.mjs';
12
11
import { BASE_PATH } from '@/next.constants.mjs';
13
12
import { availableLocales } from '@/next.locales.mjs';
14
13
15
-
const Header = () => {
16
-
const { navigationItems } = useSiteNavigation();
14
+
const Header: FC<PropsWithChildren> = ({ children }) => {
17
15
const [showLangPicker, setShowLangPicker] = useState(false);
18
16
const { resolvedTheme, setTheme } = useTheme();
19
17
···
36
34
/>
37
35
</Link>
38
36
39
-
<nav aria-label="primary">
40
-
<ul className="list-divider-pipe">
41
-
{navigationItems.map((item, key) => (
42
-
<li key={key}>
43
-
<ActiveLink href={item.link} allowSubPath>
44
-
{item.text}
45
-
</ActiveLink>
46
-
</li>
47
-
))}
48
-
</ul>
49
-
</nav>
37
+
{children}
50
38
51
39
<div className="switchers">
52
40
<button
+1
-1
components/Pagination.tsx
+1
-1
components/Pagination.tsx
+7
-2
components/withNodeRelease.tsx
+7
-2
components/withNodeRelease.tsx
···
1
1
import type { FC } from 'react';
2
2
3
-
import { releaseData } from '@/next.json.mjs';
3
+
import getReleaseData from '@/next-data/releaseData';
4
4
import type { NodeRelease, NodeReleaseStatus } from '@/types';
5
5
6
6
type WithNodeReleaseProps = {
···
8
8
children: FC<{ release: NodeRelease }>;
9
9
};
10
10
11
-
export const WithNodeRelease: FC<WithNodeReleaseProps> = ({
11
+
// This is a React Async Server Component
12
+
// Note that Hooks cannot be used in a RSC async component
13
+
// Async Components do not get re-rendered at all.
14
+
export const WithNodeRelease: FC<WithNodeReleaseProps> = async ({
12
15
status,
13
16
children: Component,
14
17
}) => {
18
+
const releaseData = await getReleaseData();
19
+
15
20
const matchingRelease = releaseData.find(release =>
16
21
[status].flat().includes(release.status)
17
22
);
-1
hooks/react-client/index.ts
-1
hooks/react-client/index.ts
···
3
3
export { default as useMediaQuery } from './useMediaQuery';
4
4
export { default as useNotification } from './useNotification';
5
5
export { default as useClientContext } from './useClientContext';
6
-
export { default as useBlogData } from './useBlogData';
7
6
export { default as useSiteNavigation } from './useSiteNavigation';
-13
hooks/react-client/useBlogData.ts
-13
hooks/react-client/useBlogData.ts
···
1
-
'use client';
2
-
3
-
import useClientContext from '@/hooks/react-client/useClientContext';
4
-
import { useBaseBlogData } from '@/hooks/useBaseBlogData';
5
-
6
-
const useBlogData = () => {
7
-
const { pathname } = useClientContext();
8
-
const data = useBaseBlogData(pathname);
9
-
10
-
return data;
11
-
};
12
-
13
-
export default useBlogData;
-1
hooks/react-server/index.ts
-1
hooks/react-server/index.ts
···
3
3
export { default as useMediaQuery } from './useMediaQuery';
4
4
export { default as useNotification } from './useNotification';
5
5
export { default as useClientContext } from './useClientContext';
6
-
export { default as useBlogData } from './useBlogData';
7
6
export { default as useSiteNavigation } from './useSiteNavigation';
-11
hooks/react-server/useBlogData.ts
-11
hooks/react-server/useBlogData.ts
···
1
-
import useClientContext from '@/hooks/react-server/useClientContext';
2
-
import { useBaseBlogData } from '@/hooks/useBaseBlogData';
3
-
4
-
const useBlogData = () => {
5
-
const { pathname } = useClientContext();
6
-
const data = useBaseBlogData(pathname);
7
-
8
-
return data;
9
-
};
10
-
11
-
export default useBlogData;
-52
hooks/useBaseBlogData.ts
-52
hooks/useBaseBlogData.ts
···
1
-
import { useMemo } from 'react';
2
-
3
-
import { blogData } from '@/next.json.mjs';
4
-
5
-
export const useBaseBlogData = (pathname: string) => {
6
-
const { posts, pagination, categories } = blogData;
7
-
8
-
const getPostsByCategory = (category: string) =>
9
-
posts.filter(post => post.category === category);
10
-
11
-
const getPostsByYear = (year: string) =>
12
-
posts.filter(post => new Date(post.date).getFullYear() === Number(year));
13
-
14
-
const getPagination = (currentYear: number | string) => {
15
-
const _currentYear = Number(currentYear);
16
-
17
-
return {
18
-
next: pagination.includes(_currentYear + 1)
19
-
? _currentYear + 1
20
-
: undefined,
21
-
prev: pagination.includes(_currentYear - 1)
22
-
? _currentYear - 1
23
-
: undefined,
24
-
};
25
-
};
26
-
27
-
const currentCategory = useMemo(() => {
28
-
// We split the pathname to retrieve the blog category from it since the
29
-
// URL is usually blog/{category} the second path piece is usually the
30
-
// category name
31
-
const [_pathname, category] = pathname.split('/');
32
-
33
-
if (_pathname === 'blog' && category && category.length) {
34
-
return category;
35
-
}
36
-
37
-
// if either the pathname does not match to a blog page
38
-
// which should not happen (as this hook should only be used in blog pages)
39
-
// or if there is no category in the URL we return the current year as category name
40
-
// which is always the default category (for example, the blog index)
41
-
return `year-${new Date().getFullYear()}`;
42
-
}, [pathname]);
43
-
44
-
return {
45
-
posts,
46
-
categories,
47
-
currentCategory,
48
-
getPostsByCategory,
49
-
getPostsByYear,
50
-
getPagination,
51
-
};
52
-
};
+4
-1
layouts/BaseLayout.tsx
+4
-1
layouts/BaseLayout.tsx
···
4
4
5
5
import Footer from '@/components/Footer';
6
6
import Header from '@/components/Header';
7
+
import TopNavigation from '@/components/TopNavigation';
7
8
8
9
const BaseLayout: FC<PropsWithChildren> = ({ children }) => (
9
10
<>
10
-
<Header />
11
+
<Header>
12
+
<TopNavigation />
13
+
</Header>
11
14
<main id="main">{children}</main>
12
15
<Footer />
13
16
</>
+37
-41
layouts/BlogCategoryLayout.tsx
+37
-41
layouts/BlogCategoryLayout.tsx
···
1
-
import { useTranslations } from 'next-intl';
2
-
import { useMemo } from 'react';
1
+
import { getTranslations } from 'next-intl/server';
3
2
import type { FC } from 'react';
4
3
4
+
import { getClientContext } from '@/client-context';
5
5
import { Time } from '@/components/Common/Time';
6
6
import Link from '@/components/Link';
7
7
import Pagination from '@/components/Pagination';
8
-
import { useClientContext, useBlogData } from '@/hooks/server';
9
-
import type { BlogPost } from '@/types';
8
+
import getBlogData from '@/next-data/blogData';
10
9
11
-
const BlogCategoryLayout: FC = () => {
12
-
const t = useTranslations();
13
-
const { getPagination, getPostsByYear, getPostsByCategory, currentCategory } =
14
-
useBlogData();
10
+
const getCurrentCategory = (pathname: string) => {
11
+
// We split the pathname to retrieve the blog category from it since the
12
+
// URL is usually blog/{category} the second path piece is usually the
13
+
// category name
14
+
const [_pathname, category] = pathname.split('/');
15
15
16
-
const { frontmatter } = useClientContext();
16
+
if (_pathname === 'blog' && category && category.length) {
17
+
return category;
18
+
}
17
19
18
-
const { posts, pagination, title } = useMemo(() => {
19
-
if (currentCategory.startsWith('year-')) {
20
-
const categoryWithoutPrefix = currentCategory.replace('year-', '');
20
+
// if either the pathname does not match to a blog page
21
+
// which should not happen (as this hook should only be used in blog pages)
22
+
// or if there is no category in the URL we return the current year as category name
23
+
// which is always the default category (for example, the blog index)
24
+
return `year-${new Date().getFullYear()}`;
25
+
};
21
26
22
-
return {
23
-
posts: getPostsByYear(categoryWithoutPrefix),
24
-
pagination: getPagination(categoryWithoutPrefix),
25
-
title: t('layouts.blogIndex.currentYear', {
26
-
year: categoryWithoutPrefix,
27
-
}),
28
-
};
29
-
}
27
+
// This is a React Async Server Component
28
+
// Note that Hooks cannot be used in a RSC async component
29
+
// Async Components do not get re-rendered at all.
30
+
const BlogCategoryLayout: FC = async () => {
31
+
const { frontmatter, pathname } = getClientContext();
32
+
const category = getCurrentCategory(pathname);
30
33
31
-
return {
32
-
posts: getPostsByCategory(currentCategory),
33
-
pagination: undefined,
34
-
title: frontmatter.title,
35
-
};
36
-
}, [
37
-
currentCategory,
38
-
frontmatter.title,
39
-
getPagination,
40
-
getPostsByCategory,
41
-
getPostsByYear,
42
-
t,
43
-
]);
34
+
const t = await getTranslations();
35
+
36
+
const { posts, pagination } = await getBlogData(category);
37
+
38
+
// this only applies if current category is a year category
39
+
const year = category.replace('year-', '');
40
+
41
+
const title = category.startsWith('year-')
42
+
? t('layouts.blogIndex.currentYear', { year })
43
+
: frontmatter.title;
44
44
45
45
return (
46
46
<div className="container" dir="auto">
47
47
<h2>{title}</h2>
48
48
49
49
<ul className="blog-index">
50
-
{posts.map((post: BlogPost) => (
51
-
<li key={post.slug}>
52
-
<Time
53
-
date={post.date}
54
-
format={{ month: 'short', day: '2-digit' }}
55
-
/>
56
-
57
-
<Link href={post.slug}>{post.title}</Link>
50
+
{posts.map(({ slug, date, title }) => (
51
+
<li key={slug}>
52
+
<Time date={date} format={{ month: 'short', day: '2-digit' }} />
53
+
<Link href={slug}>{title}</Link>
58
54
</li>
59
55
))}
60
56
</ul>
+7
-2
layouts/DocsLayout.tsx
+7
-2
layouts/DocsLayout.tsx
···
2
2
import type { FC, PropsWithChildren } from 'react';
3
3
4
4
import SideNavigation from '@/components/SideNavigation';
5
-
import { releaseData } from '@/next.json.mjs';
5
+
import getReleaseData from '@/next-data/releaseData';
6
+
7
+
// This is a React Async Server Component
8
+
// Note that Hooks cannot be used in a RSC async component
9
+
// Async Components do not get re-rendered at all.
10
+
const DocsLayout: FC<PropsWithChildren> = async ({ children }) => {
11
+
const releaseData = await getReleaseData();
6
12
7
-
const DocsLayout: FC<PropsWithChildren> = ({ children }) => {
8
13
const [lts, current] = [
9
14
releaseData.find(({ isLts }) => isLts),
10
15
releaseData.find(({ status }) => status === 'Current'),
+21
next-data/blogData.ts
+21
next-data/blogData.ts
···
1
+
import { ENABLE_STATIC_EXPORT, NEXT_DATA_URL } from '@/next.constants.mjs';
2
+
import type { BlogDataRSC } from '@/types';
3
+
4
+
const getBlogData = (category: string): Promise<BlogDataRSC> => {
5
+
// When we're using Static Exports the Next.js Server is not running (during build-time)
6
+
// hence the self-ingestion APIs will not be available. In this case we want to load
7
+
// the data directly within the current thread, which will anyways be loaded only once
8
+
// We use lazy-imports to prevent `provideBlogData` from executing on import
9
+
if (ENABLE_STATIC_EXPORT) {
10
+
return import('@/next-data/providers/blogData').then(
11
+
({ default: provideBlogData }) => provideBlogData(category)
12
+
);
13
+
}
14
+
15
+
// When we're on RSC with Server capabilities we prefer using Next.js Data Fetching
16
+
// as this will load cached data from the server instead of generating data on the fly
17
+
// this is extremely useful for ISR and SSG as it will not generate this data on every request
18
+
return fetch(`${NEXT_DATA_URL}blog-data/${category}`).then(r => r.json());
19
+
};
20
+
21
+
export default getBlogData;
+12
-21
next-data/generateBlogPostsData.mjs
next-data/generators/blogData.mjs
+12
-21
next-data/generateBlogPostsData.mjs
next-data/generators/blogData.mjs
···
1
1
'use strict';
2
2
3
3
import { createReadStream } from 'node:fs';
4
-
import { writeFile } from 'node:fs/promises';
5
4
import { basename, extname, join } from 'node:path';
6
5
import readline from 'node:readline';
7
6
8
7
import graymatter from 'gray-matter';
9
8
10
-
import * as nextHelpers from '../next.helpers.mjs';
9
+
import * as nextHelpers from '../../next.helpers.mjs';
11
10
12
11
// gets the current blog path based on local module path
13
12
const blogPath = join(process.cwd(), 'pages/en/blog');
14
-
15
-
// this is the destination path for where the JSON file will be written
16
-
const jsonFilePath = join(process.cwd(), 'public/blog-posts-data.json');
17
13
18
14
/**
19
15
* This contains the metadata of all available blog categories and
···
51
47
};
52
48
53
49
/**
54
-
* This method is used to generate the JSON file
50
+
* This method is used to generate the Node.js Website Blog Data
51
+
* for self-consumption during RSC and Static Builds
52
+
*
53
+
* @return {Promise<import('../../types').BlogData>}
55
54
*/
56
-
const generateBlogPostsData = async () => {
55
+
const generateBlogData = async () => {
57
56
// we retrieve all the filenames of all blog posts
58
57
const filenames = await nextHelpers.getMarkdownFiles(
59
58
process.cwd(),
···
61
60
['**/index.md', '**/pagination.md']
62
61
);
63
62
64
-
// Writes the Blog Posts to the JSON file
65
-
const writeResult = blogPosts => {
66
-
return writeFile(
67
-
jsonFilePath,
68
-
JSON.stringify({
69
-
pagination: [...blogMetadata.pagination].sort(),
70
-
categories: [...blogMetadata.categories].sort(),
71
-
posts: blogPosts.sort((a, b) => b.date - a.date),
72
-
})
73
-
);
74
-
};
75
-
76
63
return new Promise(resolve => {
77
64
const blogPosts = [];
78
65
···
110
97
111
98
// Once we finish reading all fles
112
99
if (blogPosts.length === filenames.length) {
113
-
resolve(writeResult(blogPosts));
100
+
resolve({
101
+
pagination: [...blogMetadata.pagination].sort(),
102
+
categories: [...blogMetadata.categories].sort(),
103
+
posts: blogPosts.sort((a, b) => b.date - a.date),
104
+
});
114
105
}
115
106
});
116
107
}
117
108
});
118
109
};
119
110
120
-
export default generateBlogPostsData;
111
+
export default generateBlogData;
+17
-20
next-data/generateNodeReleasesJson.mjs
next-data/generators/releaseData.mjs
+17
-20
next-data/generateNodeReleasesJson.mjs
next-data/generators/releaseData.mjs
···
1
-
import { writeFile } from 'node:fs/promises';
2
-
import { join } from 'node:path';
3
-
4
1
import nodevu from '@nodevu/core';
5
-
6
-
// this is the destination path for where the JSON file will be written
7
-
const jsonFilePath = join(process.cwd(), 'public/node-releases-data.json');
8
2
9
3
// Gets the appropriate release status for each major release
10
4
const getNodeReleaseStatus = (now, support) => {
···
29
23
return 'Pending';
30
24
};
31
25
32
-
const generateNodeReleasesJson = async () => {
26
+
/**
27
+
* This method is used to generate the Node.js Release Data
28
+
* for self-consumption during RSC and Static Builds
29
+
*
30
+
* @returns {Promise<import('../../types').NodeRelease[]>}
31
+
*/
32
+
const generateReleaseData = async () => {
33
33
const nodevuOutput = await nodevu({ fetch: fetch });
34
34
35
35
// Filter out those without documented support
···
63
63
};
64
64
});
65
65
66
-
return writeFile(
67
-
jsonFilePath,
68
-
JSON.stringify(
69
-
// nodevu returns duplicated v0.x versions (v0.12, v0.10, ...).
70
-
// This behavior seems intentional as the case is hardcoded in nodevu,
71
-
// see https://github.com/cutenode/nodevu/blob/0c8538c70195fb7181e0a4d1eeb6a28e8ed95698/core/index.js#L24.
72
-
// This line ignores those duplicated versions and takes the latest
73
-
// v0.x version (v0.12.18). It is also consistent with the legacy
74
-
// nodejs.org implementation.
75
-
nodeReleases.filter(
76
-
release => release.major !== 0 || release.version === '0.12.18'
77
-
)
66
+
return Promise.resolve(
67
+
// nodevu returns duplicated v0.x versions (v0.12, v0.10, ...).
68
+
// This behavior seems intentional as the case is hardcoded in nodevu,
69
+
// see https://github.com/cutenode/nodevu/blob/0c8538c70195fb7181e0a4d1eeb6a28e8ed95698/core/index.js#L24.
70
+
// This line ignores those duplicated versions and takes the latest
71
+
// v0.x version (v0.12.18). It is also consistent with the legacy
72
+
// nodejs.org implementation.
73
+
nodeReleases.filter(
74
+
release => release.major !== 0 || release.version === '0.12.18'
78
75
)
79
76
);
80
77
};
81
78
82
-
export default generateNodeReleasesJson;
79
+
export default generateReleaseData;
+8
-5
next-data/generateWebsiteFeeds.mjs
next-data/generators/websiteFeeds.mjs
+8
-5
next-data/generateWebsiteFeeds.mjs
next-data/generators/websiteFeeds.mjs
···
2
2
3
3
import { Feed } from 'feed';
4
4
5
-
import { BASE_URL, BASE_PATH } from '../next.constants.mjs';
6
-
import { siteConfig } from '../next.json.mjs';
5
+
import { BASE_URL, BASE_PATH } from '../../next.constants.mjs';
6
+
import { siteConfig } from '../../next.json.mjs';
7
7
8
8
/**
9
9
* This method generates RSS website feeds based on the current website configuration
10
10
* and the current blog data that is available
11
11
*
12
-
* @param {import('../types').BlogData} blogData
12
+
* @param {Promise<import('../../types').BlogDataRSC>} blogData
13
13
*/
14
-
const generateWebsiteFeeds = ({ posts }) => {
14
+
const generateWebsiteFeeds = async blogData => {
15
15
const canonicalUrl = `${BASE_URL}${BASE_PATH}/en`;
16
+
17
+
// Wait for the Blog Data for being generate
18
+
const { posts } = await blogData;
16
19
17
20
/**
18
21
* This generates all the Website RSS Feeds that are used for the website
···
26
29
title: title,
27
30
language: 'en',
28
31
link: `${canonicalUrl}/feed/${file}`,
29
-
description: description || siteConfig.description,
32
+
description: description || description,
30
33
});
31
34
32
35
const blogFeedEntries = posts
+46
next-data/providers/blogData.ts
+46
next-data/providers/blogData.ts
···
1
+
import { cache } from 'react';
2
+
3
+
import generateBlogData from '@/next-data/generators/blogData.mjs';
4
+
import type { BlogDataRSC } from '@/types';
5
+
6
+
const blogData = generateBlogData();
7
+
8
+
const provideBlogData = cache(
9
+
async (category?: string): Promise<BlogDataRSC> => {
10
+
return blogData.then(({ posts, categories, pagination }) => {
11
+
const meta = { categories, pagination };
12
+
13
+
if (category && categories.includes(category)) {
14
+
return {
15
+
posts: posts.filter(post => post.category === category),
16
+
pagination: { next: null, prev: null },
17
+
meta,
18
+
};
19
+
}
20
+
21
+
if (category && category.startsWith('year-')) {
22
+
const paramYear = Number(category.replace('year-', ''));
23
+
24
+
const isEqualYear = (date: string) =>
25
+
new Date(date).getFullYear() === paramYear;
26
+
27
+
return {
28
+
posts: posts.filter(({ date }) => isEqualYear(date)),
29
+
pagination: {
30
+
next: pagination.includes(paramYear + 1) ? paramYear + 1 : null,
31
+
prev: pagination.includes(paramYear - 1) ? paramYear - 1 : null,
32
+
},
33
+
meta,
34
+
};
35
+
}
36
+
37
+
if (category && !categories.includes(category)) {
38
+
return { posts: [], pagination: { next: null, prev: null }, meta };
39
+
}
40
+
41
+
return { posts, pagination: { next: null, prev: null }, meta };
42
+
});
43
+
}
44
+
);
45
+
46
+
export default provideBlogData;
+9
next-data/providers/releaseData.ts
+9
next-data/providers/releaseData.ts
+18
next-data/providers/websiteFeeds.ts
+18
next-data/providers/websiteFeeds.ts
···
1
+
import { cache } from 'react';
2
+
3
+
import generateWebsiteFeeds from '@/next-data/generators/websiteFeeds.mjs';
4
+
import provideBlogData from '@/next-data/providers/blogData';
5
+
6
+
const websiteFeeds = generateWebsiteFeeds(provideBlogData());
7
+
8
+
const provideWebsiteFeeds = cache(async (feed: string) => {
9
+
return websiteFeeds.then(feeds => {
10
+
if (feed.includes('.xml') && feeds.has(feed)) {
11
+
return feeds.get(feed)?.rss2();
12
+
}
13
+
14
+
return undefined;
15
+
});
16
+
});
17
+
18
+
export default provideWebsiteFeeds;
+21
next-data/releaseData.ts
+21
next-data/releaseData.ts
···
1
+
import { ENABLE_STATIC_EXPORT, NEXT_DATA_URL } from '@/next.constants.mjs';
2
+
import type { NodeRelease } from '@/types';
3
+
4
+
const getReleaseData = (): Promise<NodeRelease[]> => {
5
+
// When we're using Static Exports the Next.js Server is not running (during build-time)
6
+
// hence the self-ingestion APIs will not be available. In this case we want to load
7
+
// the data directly within the current thread, which will anyways be loaded only once
8
+
// We use lazy-imports to prevent `provideBlogData` from executing on import
9
+
if (ENABLE_STATIC_EXPORT) {
10
+
return import('@/next-data/providers/releaseData').then(
11
+
({ default: provideReleaseData }) => provideReleaseData()
12
+
);
13
+
}
14
+
15
+
// When we're on RSC with Server capabilities we prefer using Next.js Data Fetching
16
+
// as this will load cached data from the server instead of generating data on the fly
17
+
// this is extremely useful for ISR and SSG as it will not generate this data on every request
18
+
return fetch(`${NEXT_DATA_URL}release-data`).then(r => r.json());
19
+
};
20
+
21
+
export default getReleaseData;
+26
-97
next.constants.mjs
+26
-97
next.constants.mjs
···
1
1
'use strict';
2
2
3
-
import { blogData, siteConfig } from './next.json.mjs';
4
-
import { defaultLocale } from './next.locales.mjs';
3
+
/**
4
+
* This is used for the current Legacy Website Blog Pagination Generation
5
+
*
6
+
* @deperecated remove with website redesign
7
+
*/
8
+
export const CURRENT_YEAR = new Date().getFullYear();
9
+
10
+
/**
11
+
* This is used to verify if the current Website is running on a Development Environment
12
+
*/
13
+
export const IS_DEVELOPMENT = process.env.NODE_ENV === 'development';
5
14
6
15
/**
7
16
* This is used for telling Next.js if the Website is deployed on Vercel
···
65
74
export const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH || '';
66
75
67
76
/**
77
+
* This is used for fetching static next-data through the /en/next-data/ endpoint
78
+
*
79
+
* Note this is assumes that the Node.js Website is either running within Vercel Environment
80
+
* or running locally (either production or development) mode
81
+
*
82
+
* Note this variable can be overrided via a manual Environment Variable defined by us if necessary.
83
+
*/
84
+
export const NEXT_DATA_URL = process.env.NEXT_PUBLIC_DATA_URL
85
+
? process.env.NEXT_PUBLIC_DATA_URL
86
+
: VERCEL_ENV
87
+
? `${BASE_URL}${BASE_PATH}/en/next-data/`
88
+
: `http://localhost:3000/en/next-data/`;
89
+
90
+
/**
68
91
* This ReGeX is used to remove the `index.md(x)` suffix of a name and to remove
69
92
* the `.md(x)` extensions of a filename.
70
93
*
···
73
96
*/
74
97
export const MD_EXTENSION_REGEX = /((\/)?(index))?\.mdx?$/i;
75
98
76
-
/**
77
-
* This is a list of all static routes or pages from the Website that we do not
78
-
* want to allow to be statically built on our Static Export Build.
79
-
*
80
-
* @type {((route: import('./types').RouteSegment) => boolean)[]} A list of Ignored Routes by Regular Expressions
81
-
*/
82
-
export const STATIC_ROUTES_IGNORES = [
83
-
// Ignore the 404 route on Static Generation
84
-
({ pathname }) => pathname === '404',
85
-
// This is used to ignore is used to ignore all blog routes except for the English language
86
-
({ locale, pathname }) =>
87
-
locale !== defaultLocale.code && /^blog\//.test(pathname),
88
-
// This is used to ignore the blog/pagination meta route
89
-
({ pathname }) => /^blog\/pagination/.test(pathname),
90
-
];
91
-
92
-
/**
93
-
* This is a list of all dynamic routes or pages from the Website that we do not
94
-
* want to allow to be dynamically access by our Dynamic Route Engine
95
-
*
96
-
* @type {RegExp[]} A list of Ignored Routes by Regular Expressions
97
-
*/
98
-
export const DYNAMIC_ROUTES_IGNORES = [
99
-
// This is used to ignore the blog/pagination route
100
-
/^blog\/pagination/,
101
-
];
102
-
103
-
/**
104
-
* This is a list of all static routes that we want to rewrite their pathnames
105
-
* into something else. This is useful when you want to have the current pathname in the route
106
-
* but replace the actual Markdown file that is being loaded by the Dynamic Route to something else
107
-
*
108
-
* @type {[RegexExp, (pathname: string) => string][]}
109
-
*/
110
-
export const DYNAMIC_ROUTES_REWRITES = [
111
-
[/^blog\/year-/, () => 'blog/pagination'],
112
-
];
113
-
114
-
/**
115
-
* This is a constant that should be used during runtime by (`getStaticPaths`) on `pages/[...path].tsx`
116
-
*
117
-
* This function is used to provide an extra set of routes that are not provided by `next.dynamic.mjs`
118
-
* static route discovery. This can happen when we have dynamic routes that **must** be provided
119
-
* within the static export (static build) of the website. This constant usually would be used along
120
-
* with a matching pathname on `DYNAMIC_ROUTES_REWRITES`.
121
-
*
122
-
* @type {string[]} A list of all the Dynamic Routes that are generated by the Website
123
-
*/
124
-
export const DYNAMIC_GENERATED_ROUTES = [
125
-
...blogData.pagination.map(year => `blog/year-${year}`),
126
-
];
127
-
128
99
/***
129
100
* This is a list of all external links that are used on website sitemap.
130
101
* @see https://github.com/nodejs/nodejs.org/issues/5813 for more context
···
145
116
export const THEME_STORAGE_KEY = 'theme';
146
117
147
118
/**
148
-
* This is the default Next.js Page Metadata for all pages
149
-
*
150
-
* @type {import('next').Metadata}
151
-
*/
152
-
export const DEFAULT_METADATA = {
153
-
metadataBase: new URL(`${BASE_URL}${BASE_PATH}`),
154
-
title: siteConfig.title,
155
-
description: siteConfig.description,
156
-
robots: { index: true, follow: true },
157
-
twitter: {
158
-
card: siteConfig.twitter.card,
159
-
title: siteConfig.twitter.title,
160
-
creator: siteConfig.twitter.username,
161
-
images: {
162
-
url: siteConfig.twitter.img,
163
-
alt: siteConfig.twitter.imgAlt,
164
-
},
165
-
},
166
-
alternates: {
167
-
canonical: '',
168
-
languages: { 'x-default': '' },
169
-
types: {
170
-
'application/rss+xml': 'https://nodejs.org/en/feed/blog.xml',
171
-
},
172
-
},
173
-
icons: { icon: siteConfig.favicon },
174
-
openGraph: { images: siteConfig.twitter.img },
175
-
};
176
-
177
-
/**
178
-
* This is the default Next.js Viewport Metadata for all pages
179
-
*
180
-
* @type {import('next').Viewport}
181
-
*/
182
-
export const DEFAULT_VIEWPORT = {
183
-
themeColor: siteConfig.accentColor,
184
-
width: 'device-width',
185
-
initialScale: 1,
186
-
};
187
-
188
-
/**
189
119
* This is the Sentry DSN for the Node.js Website Project
190
120
*/
191
121
export const SENTRY_DSN =
···
194
124
/**
195
125
* This states if Sentry should be enabled and bundled within our App
196
126
*/
197
-
export const SENTRY_ENABLE =
198
-
process.env.NODE_ENV === 'development' || !!VERCEL_ENV;
127
+
export const SENTRY_ENABLE = IS_DEVELOPMENT || !!VERCEL_ENV;
199
128
200
129
/**
201
130
* This configures the sampling rate for Sentry
-11
next.data.mjs
-11
next.data.mjs
···
1
-
'use strict';
2
-
3
-
import generateBlogPostsData from './next-data/generateBlogPostsData.mjs';
4
-
import generateNodeReleasesJson from './next-data/generateNodeReleasesJson.mjs';
5
-
import generateWebsiteFeeds from './next-data/generateWebsiteFeeds.mjs';
6
-
7
-
export {
8
-
generateWebsiteFeeds,
9
-
generateBlogPostsData,
10
-
generateNodeReleasesJson,
11
-
};
+107
next.dynamic.constants.mjs
+107
next.dynamic.constants.mjs
···
1
+
'use strict';
2
+
3
+
import { BASE_PATH, BASE_URL, CURRENT_YEAR } from './next.constants.mjs';
4
+
import { siteConfig } from './next.json.mjs';
5
+
import { defaultLocale } from './next.locales.mjs';
6
+
7
+
/**
8
+
* This is a list of all static routes or pages from the Website that we do not
9
+
* want to allow to be statically built on our Static Export Build.
10
+
*
11
+
* @type {((route: import('./types').RouteSegment) => boolean)[]} A list of Ignored Routes by Regular Expressions
12
+
*/
13
+
export const STATIC_ROUTES_IGNORES = [
14
+
// Ignore the 404 route on Static Generation
15
+
({ pathname }) => pathname === '404',
16
+
// This is used to ignore is used to ignore all blog routes except for the English language
17
+
({ locale, pathname }) =>
18
+
locale !== defaultLocale.code && /^blog\//.test(pathname),
19
+
// This is used to ignore the blog/pagination meta route
20
+
// @deprecated remove with website redesign
21
+
({ pathname }) => /^blog\/pagination/.test(pathname),
22
+
];
23
+
24
+
/**
25
+
* This is a list of all dynamic routes or pages from the Website that we do not
26
+
* want to allow to be dynamically access by our Dynamic Route Engine
27
+
*
28
+
* @type {RegExp[]} A list of Ignored Routes by Regular Expressions
29
+
* @deprecated remove with website redesign
30
+
*/
31
+
export const DYNAMIC_ROUTES_IGNORES = [
32
+
// This is used to ignore the blog/pagination route
33
+
/^blog\/pagination/,
34
+
];
35
+
36
+
/**
37
+
* This is a list of all static routes that we want to rewrite their pathnames
38
+
* into something else. This is useful when you want to have the current pathname in the route
39
+
* but replace the actual Markdown file that is being loaded by the Dynamic Route to something else
40
+
*
41
+
* @type {[RegexExp, (pathname: string) => string][]}
42
+
* @deprecated remove with website redesign
43
+
*/
44
+
export const DYNAMIC_ROUTES_REWRITES = [
45
+
[/^blog\/year-/, () => 'blog/pagination'],
46
+
];
47
+
48
+
/**
49
+
* This is a constant that should be used during runtime by (`getStaticPaths`) on `pages/[...path].tsx`
50
+
*
51
+
* This function is used to provide an extra set of routes that are not provided by `next.dynamic.mjs`
52
+
* static route discovery. This can happen when we have dynamic routes that **must** be provided
53
+
* within the static export (static build) of the website. This constant usually would be used along
54
+
* with a matching pathname on `DYNAMIC_ROUTES_REWRITES`.
55
+
*
56
+
* @type {string[]} A list of all the Dynamic Routes that are generated by the Website
57
+
* @deprecated remove with website redesign
58
+
*/
59
+
export const DYNAMIC_GENERATED_ROUTES = [
60
+
...Array.from(
61
+
// Statically generate a List of Years from Current Year
62
+
// til 2011 which is the oldest year with blog posts
63
+
{ length: CURRENT_YEAR - 2011 },
64
+
(_, i) => CURRENT_YEAR - i
65
+
).map(year => `blog/year-${year}`),
66
+
];
67
+
68
+
/**
69
+
* This is the default Next.js Page Metadata for all pages
70
+
*
71
+
* @type {import('next').Metadata}
72
+
*/
73
+
export const DEFAULT_METADATA = {
74
+
metadataBase: new URL(`${BASE_URL}${BASE_PATH}`),
75
+
title: siteConfig.title,
76
+
description: siteConfig.description,
77
+
robots: { index: true, follow: true },
78
+
twitter: {
79
+
card: siteConfig.twitter.card,
80
+
title: siteConfig.twitter.title,
81
+
creator: siteConfig.twitter.username,
82
+
images: {
83
+
url: siteConfig.twitter.img,
84
+
alt: siteConfig.twitter.imgAlt,
85
+
},
86
+
},
87
+
alternates: {
88
+
canonical: '',
89
+
languages: { 'x-default': '' },
90
+
types: {
91
+
'application/rss+xml': `${BASE_URL}${BASE_PATH}/en/feed/blog.xml`,
92
+
},
93
+
},
94
+
icons: { icon: siteConfig.favicon },
95
+
openGraph: { images: siteConfig.twitter.img },
96
+
};
97
+
98
+
/**
99
+
* This is the default Next.js Viewport Metadata for all pages
100
+
*
101
+
* @return {import('next').Viewport}
102
+
*/
103
+
export const DEFAULT_VIEWPORT = {
104
+
themeColor: siteConfig.accentColor,
105
+
width: 'device-width',
106
+
initialScale: 1,
107
+
};
+3
-5
next.dynamic.mjs
+3
-5
next.dynamic.mjs
···
8
8
import { cache } from 'react';
9
9
import { VFile } from 'vfile';
10
10
11
+
import { MD_EXTENSION_REGEX, BASE_URL, BASE_PATH } from './next.constants.mjs';
11
12
import {
12
-
DYNAMIC_GENERATED_ROUTES,
13
13
DYNAMIC_ROUTES_IGNORES,
14
14
DYNAMIC_ROUTES_REWRITES,
15
-
MD_EXTENSION_REGEX,
16
15
STATIC_ROUTES_IGNORES,
16
+
DYNAMIC_GENERATED_ROUTES,
17
17
DEFAULT_METADATA,
18
-
BASE_URL,
19
-
BASE_PATH,
20
-
} from './next.constants.mjs';
18
+
} from './next.dynamic.constants.mjs';
21
19
import { getMarkdownFiles } from './next.helpers.mjs';
22
20
import { siteConfig } from './next.json.mjs';
23
21
import { availableLocaleCodes, defaultLocale } from './next.locales.mjs';
-12
next.json.mjs
-12
next.json.mjs
···
1
1
'use strict';
2
2
3
-
import _localeConfig from './i18n/config.json' assert { type: 'json' };
4
3
import _siteNavigation from './navigation.json' assert { type: 'json' };
5
-
import _blogData from './public/blog-posts-data.json' assert { type: 'json' };
6
-
import _releaseData from './public/node-releases-data.json' assert { type: 'json' };
7
4
import _siteRedirects from './redirects.json' assert { type: 'json' };
8
5
import _siteConfig from './site.json' assert { type: 'json' };
9
-
10
-
/** @type {import('./types').LocaleConfig[]} */
11
-
export const localeConfig = _localeConfig;
12
6
13
7
/** @type {Record<string, import('./types').NavigationEntry>} */
14
8
export const siteNavigation = _siteNavigation;
15
-
16
-
/** @type {import('./types').BlogData} */
17
-
export const blogData = _blogData;
18
-
19
-
/** @type {import('./types').NodeRelease[]} */
20
-
export const releaseData = _releaseData;
21
9
22
10
/** @type {Record<string, import('./types').Redirect[]>} */
23
11
export const siteRedirects = _siteRedirects;
+1
-1
next.locales.mjs
+1
-1
next.locales.mjs
+10
-8
next.rewrites.mjs
+10
-8
next.rewrites.mjs
···
16
16
* These are sourced originally from https://github.com/nodejs/build/blob/main/ansible/www-standalone/resources/config/nodejs.org?plain=1
17
17
* 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
18
18
*
19
-
* @type {import('next').NextConfig['redirects']}
19
+
* @return {Promise<import('next').NextConfig['redirects']>}
20
20
*/
21
21
const redirects = async () => {
22
22
return siteRedirects.external.map(({ source, destination }) => ({
···
32
32
* These are sourced originally from https://github.com/nodejs/build/blob/main/ansible/www-standalone/resources/config/nodejs.org?plain=1
33
33
* 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
34
34
*
35
-
* @type {import('next').NextConfig['rewrites']}
35
+
* @return {Promise<import('next').NextConfig['rewrites']>}
36
36
*/
37
-
const rewrites = async () => ({
38
-
afterFiles: siteRedirects.internal.map(({ source, destination }) => ({
39
-
source: source.replace('/:locale/', localesMatch),
40
-
destination,
41
-
})),
42
-
});
37
+
const rewrites = async () => {
38
+
return {
39
+
afterFiles: siteRedirects.internal.map(({ source, destination }) => ({
40
+
source: source.replace('/:locale/', localesMatch),
41
+
destination,
42
+
})),
43
+
};
44
+
};
43
45
44
46
export { rewrites, redirects };
+12
-12
package-lock.json
+12
-12
package-lock.json
···
31
31
"husky": "8.0.3",
32
32
"lint-staged": "15.0.2",
33
33
"next": "~14.0.3",
34
-
"next-intl": "^3.1.0",
34
+
"next-intl": "^3.1.3",
35
35
"next-themes": "~0.2.1",
36
36
"postcss": "~8.4.30",
37
37
"postcss-calc": "~9.0.1",
···
45
45
"remark-gfm": "~4.0.0",
46
46
"semver": "~7.5.4",
47
47
"sharp": "0.32.6",
48
-
"shikiji": "~0.6.13",
48
+
"shikiji": "~0.7.3",
49
49
"tailwindcss": "^3.3.5",
50
50
"turbo": "1.10.16",
51
51
"typescript": "~5.2.2",
···
26776
26776
}
26777
26777
},
26778
26778
"node_modules/next-intl": {
26779
-
"version": "3.1.0",
26780
-
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.1.0.tgz",
26781
-
"integrity": "sha512-ZbQeJO0RcORFljtNjQmbXRnyS4KwiI0DCMemdIl0OmwoI3PWSpJojE/0hmy6RRZJHEtrWQnpS+afCBpvqgii+A==",
26779
+
"version": "3.1.3",
26780
+
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.1.3.tgz",
26781
+
"integrity": "sha512-pjR6px7qj5i7gNgFMFlk6nX8Y0A2xeoYUYXEMrvg9dg3peRuZYGpcf6r46+paEd2JvvIZSb7WzlUH98nu5S5HQ==",
26782
26782
"dependencies": {
26783
26783
"@formatjs/intl-localematcher": "^0.2.32",
26784
26784
"negotiator": "^0.6.3",
26785
-
"use-intl": "^3.1.0"
26785
+
"use-intl": "^3.1.3"
26786
26786
},
26787
26787
"peerDependencies": {
26788
26788
"next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0",
···
32744
32744
}
32745
32745
},
32746
32746
"node_modules/shikiji": {
32747
-
"version": "0.6.13",
32748
-
"resolved": "https://registry.npmjs.org/shikiji/-/shikiji-0.6.13.tgz",
32749
-
"integrity": "sha512-4T7X39csvhT0p7GDnq9vysWddf2b6BeioiN3Ymhnt3xcy9tXmDcnsEFVxX18Z4YcQgEE/w48dLJ4pPPUcG9KkA==",
32747
+
"version": "0.7.3",
32748
+
"resolved": "https://registry.npmjs.org/shikiji/-/shikiji-0.7.3.tgz",
32749
+
"integrity": "sha512-kMb6bfZ+VrTcVn35RGnYjCfRnnPvg08UUXnpVW5bqT5S8UyrrUtUJjhzQHkMi5k8N1aS/FO5YDl+sHRiRdtcLQ==",
32750
32750
"dependencies": {
32751
32751
"hast-util-to-html": "^9.0.0"
32752
32752
}
···
35566
35566
}
35567
35567
},
35568
35568
"node_modules/use-intl": {
35569
-
"version": "3.1.0",
35570
-
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.1.0.tgz",
35571
-
"integrity": "sha512-PT6g2BISl93vzkUlbEPG6WN6ztOe75F+d3flxCzn4MpHEDdiDwlc6WpNcoOnlIfxEQvYKr66zDA0QrJAEB8yAQ==",
35569
+
"version": "3.1.3",
35570
+
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.1.3.tgz",
35571
+
"integrity": "sha512-rYP1O+RiVef8/iMdZxYp4KIuPszzFQQMzp1y0pMm1VBRdrwWw9YA5j2bEIbjxRk6DaVGW+cpW9mdPe5X2snNvg==",
35572
35572
"dependencies": {
35573
35573
"@formatjs/ecma402-abstract": "^1.11.4",
35574
35574
"intl-messageformat": "^9.3.18"
+2
-5
package.json
+2
-5
package.json
···
16
16
},
17
17
"scripts": {
18
18
"scripts:release-post": "cross-env NODE_NO_WARNINGS=1 node scripts/release-post/index.mjs",
19
-
"scripts:generate-next-data": "cross-env NODE_NO_WARNINGS=1 node scripts/generate-next-data/index.mjs",
20
-
"preserve": "npm run scripts:generate-next-data",
21
19
"serve": "cross-env NODE_NO_WARNINGS=1 next dev --turbo",
22
-
"prebuild": "npm run scripts:generate-next-data",
23
20
"build": "cross-env NODE_NO_WARNINGS=1 next build",
24
21
"start": "cross-env NODE_NO_WARNINGS=1 next start",
25
22
"deploy": "cross-env NEXT_STATIC_EXPORT=true npm run build",
···
63
60
"husky": "8.0.3",
64
61
"lint-staged": "15.0.2",
65
62
"next": "~14.0.3",
66
-
"next-intl": "^3.1.0",
63
+
"next-intl": "^3.1.3",
67
64
"next-themes": "~0.2.1",
68
65
"postcss": "~8.4.30",
69
66
"postcss-calc": "~9.0.1",
···
77
74
"remark-gfm": "~4.0.0",
78
75
"semver": "~7.5.4",
79
76
"sharp": "0.32.6",
80
-
"shikiji": "~0.6.13",
77
+
"shikiji": "~0.7.3",
81
78
"tailwindcss": "^3.3.5",
82
79
"turbo": "1.10.16",
83
80
"typescript": "~5.2.2",
-1
public/blog-posts-data.json
-1
public/blog-posts-data.json
···
1
-
{ "pagination": [], "categories": [], "posts": [] }
-1
public/node-releases-data.json
-1
public/node-releases-data.json
···
1
-
[]
-14
scripts/generate-next-data/index.mjs
-14
scripts/generate-next-data/index.mjs
···
1
-
'use strict';
2
-
3
-
const textToDisplay =
4
-
'(blog-posts-data.json) and (node-releases-data.json) got generated.';
5
-
6
-
console.log(`- \x1b[0;34minfo\x1b[0m \x1b[1m${textToDisplay}\x1b[0m`);
7
-
8
-
import * as nextData from '../../next.data.mjs';
9
-
10
-
// generate the node.js releases json file
11
-
await nextData.generateNodeReleasesJson();
12
-
13
-
// generate the data from blog posts
14
-
await nextData.generateBlogPostsData();
+12
types/blog.ts
+12
types/blog.ts