+2
-2
.github/workflows/build.yml
+2
-2
.github/workflows/build.yml
···
91
91
node-version-file: '.nvmrc'
92
92
cache: 'npm'
93
93
94
-
- name: Install NPM packages
95
-
# We want to avoid NPM from running the Audit Step and Funding messages on a CI environment
94
+
- name: Install npm packages
95
+
# We want to avoid npm from running the Audit Step and Funding messages on a CI environment
96
96
# We also use `npm i` instead of `npm ci` so that the node_modules/.cache folder doesn't get deleted
97
97
# We also use `--omit=dev` to avoid installing devDependencies as we don't need them during the build step
98
98
run: npm i --no-audit --no-fund --userconfig=/dev/null --omit=dev
+4
-4
.github/workflows/lint-and-tests.yml
+4
-4
.github/workflows/lint-and-tests.yml
···
111
111
node-version-file: '.nvmrc'
112
112
cache: 'npm'
113
113
114
-
- name: Install NPM packages
115
-
# We want to avoid NPM from running the Audit Step and Funding messages on a CI environment
114
+
- name: Install npm packages
115
+
# We want to avoid npm from running the Audit Step and Funding messages on a CI environment
116
116
# We also use `npm i` instead of `npm ci` so that the node_modules/.cache folder doesn't get deleted
117
117
run: npm i --no-audit --no-fund --ignore-scripts --userconfig=/dev/null
118
118
···
209
209
node-version-file: '.nvmrc'
210
210
cache: 'npm'
211
211
212
-
- name: Install NPM packages
213
-
# We want to avoid NPM from running the Audit Step and Funding messages on a CI environment
212
+
- name: Install npm packages
213
+
# We want to avoid npm from running the Audit Step and Funding messages on a CI environment
214
214
# We also use `npm i` instead of `npm ci` so that the node_modules/.cache folder doesn't get deleted
215
215
run: npm i --no-audit --no-fund --userconfig=/dev/null
216
216
+2
-2
.github/workflows/translations-pr.yml
+2
-2
.github/workflows/translations-pr.yml
···
100
100
node-version-file: '.nvmrc'
101
101
cache: 'npm'
102
102
103
-
- name: Install NPM packages
104
-
# We want to avoid NPM from running the Audit Step and Funding messages on a CI environment
103
+
- name: Install npm packages
104
+
# We want to avoid npm from running the Audit Step and Funding messages on a CI environment
105
105
# We also use `npm i` instead of `npm ci` so that the node_modules/.cache folder doesn't get deleted
106
106
run: npm i --no-audit --no-fund --ignore-scripts --userconfig=/dev/null
107
107
+1
-1
DEPENDENCY_PINNING.md
+1
-1
DEPENDENCY_PINNING.md
···
1
1
## Dependency Pinning
2
2
3
-
Based on the initial discussions from [this discussion thread](https://github.com/nodejs/nodejs.org/discussions/5491), we've decided to use a more strict strategy for handling NPM dependencies within the Node.js Website.
3
+
Based on the initial discussions from [this discussion thread](https://github.com/nodejs/nodejs.org/discussions/5491), we've decided to use a more strict strategy for handling npm dependencies within the Node.js Website.
4
4
5
5
The intent here is to prevent the build process, or the website itself, from breaking due to changes in dependencies. As some dependencies do not respect semantic versioning, this is a real concern. Pinning dependencies also ensures that we stay fixed on a specific dependency version. For security updates, Dependabot is still configured to give us security alerts when specific dependencies got security advisories.
6
6
+44
-25
app/[locale]/[[...path]]/page.tsx
+44
-25
app/[locale]/[[...path]]/page.tsx
···
7
7
import { MDXRenderer } from '@/components/mdxRenderer';
8
8
import WithLayout from '@/components/withLayout';
9
9
import { ENABLE_STATIC_EXPORT, VERCEL_REVALIDATE } from '@/next.constants.mjs';
10
-
import { DEFAULT_VIEWPORT } from '@/next.dynamic.constants.mjs';
10
+
import { PAGE_VIEWPORT, DYNAMIC_ROUTES } from '@/next.dynamic.constants.mjs';
11
11
import { dynamicRouter } from '@/next.dynamic.mjs';
12
12
import { availableLocaleCodes, defaultLocale } from '@/next.locales.mjs';
13
13
import { MatterProvider } from '@/providers/matterProvider';
···
17
17
18
18
// This is the default Viewport Metadata
19
19
// @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function
20
-
export const generateViewport = async () => ({ ...DEFAULT_VIEWPORT });
20
+
export const generateViewport = async () => ({ ...PAGE_VIEWPORT });
21
21
22
22
// This generates each page's HTML Metadata
23
23
// @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata
···
26
26
27
27
const pathname = dynamicRouter.getPathname(path);
28
28
29
-
// Retrieves and rewriting rule if the pathname matches any rule
30
-
const [, rewriteRule] = dynamicRouter.getRouteRewrite(pathname);
29
+
return dynamicRouter.getPageMetadata(locale, pathname);
30
+
};
31
31
32
-
return dynamicRouter.getPageMetadata(
33
-
locale,
34
-
rewriteRule ? rewriteRule(pathname) : pathname
32
+
// Gets all mapped routes to the Next.js Routing Engine by Locale
33
+
const mapRoutesForLocale = async (locale: string) => {
34
+
const routesForLanguage = await dynamicRouter.getRoutesByLanguage(locale);
35
+
36
+
return routesForLanguage.map(pathname =>
37
+
dynamicRouter.mapPathToRoute(locale, pathname)
35
38
);
36
39
};
37
40
···
40
43
export const generateStaticParams = async () => {
41
44
const paths: Array<DynamicStaticPaths> = [];
42
45
43
-
// We don't need to compute all possible paths on regular builds
44
-
// as we benefit from Next.js's ISR (Incremental Static Regeneration)
45
-
if (!ENABLE_STATIC_EXPORT) {
46
-
return [];
47
-
}
48
-
49
-
for (const locale of availableLocaleCodes) {
50
-
const routesForLanguage = await dynamicRouter.getRoutesByLanguage(locale);
51
-
52
-
const mappedRoutesWithLocale = routesForLanguage.map(pathname =>
53
-
dynamicRouter.mapPathToRoute(locale, pathname)
46
+
// If static exports are enabled we need to compute all available routes
47
+
// And then append them to Next.js's Route Engine
48
+
if (ENABLE_STATIC_EXPORT) {
49
+
const allAvailableRoutes = await Promise.all(
50
+
availableLocaleCodes.map(mapRoutesForLocale)
54
51
);
55
52
56
-
paths.push(...mappedRoutesWithLocale);
53
+
paths.push(...allAvailableRoutes.flat());
57
54
}
58
55
59
56
return paths.sort();
···
76
73
// Configures the current Locale to be the given Locale of the Request
77
74
unstable_setRequestLocale(locale);
78
75
76
+
// Gets the current full pathname for a given path
79
77
const pathname = dynamicRouter.getPathname(path);
80
78
81
-
if (dynamicRouter.shouldIgnoreRoute(pathname)) {
82
-
return notFound();
83
-
}
79
+
// @todo: once removed the legacy layouts remove the any casting
80
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
81
+
const staticGeneratedLayout = DYNAMIC_ROUTES.get(pathname) as any;
84
82
85
-
// Retrieves and rewriting rule if the pathname matches any rule
86
-
const [, rewriteRule] = dynamicRouter.getRouteRewrite(pathname);
83
+
// If the current patname is a statically generated route
84
+
// it means it does not have a Markdown file nor exists under the filesystem
85
+
// but it is a valid route with an assigned layout that should be rendered
86
+
if (staticGeneratedLayout !== undefined) {
87
+
// Decorate the Locale and current Pathname to Sentry
88
+
setTags({ pathname, locale });
89
+
90
+
// Metadata and shared Context to be available through the lifecycle of the page
91
+
const sharedContext = { pathname: `/${pathname}` };
92
+
93
+
// Defines a shared Server Context for the Client-Side
94
+
// That is shared for all pages under the dynamic router
95
+
setClientContext(sharedContext);
96
+
97
+
// The Matter Provider allows Client-Side injection of the data
98
+
// to a shared React Client Provider even though the page is rendered
99
+
// within a server-side context
100
+
return (
101
+
<MatterProvider {...sharedContext}>
102
+
<WithLayout layout={staticGeneratedLayout} />
103
+
</MatterProvider>
104
+
);
105
+
}
87
106
88
107
// We retrieve the source of the Markdown file by doing an educated guess
89
108
// of what possible files could be the source of the page, since the extension
90
109
// context is lost from `getStaticProps` as a limitation of Next.js itself
91
110
const { source, filename } = await dynamicRouter.getMarkdownFile(
92
111
locale,
93
-
rewriteRule ? rewriteRule(pathname) : pathname
112
+
pathname
94
113
);
95
114
96
115
// Decorate the Locale and current Pathname to Sentry
+1
-1
app/[locale]/feed/[feed]/route.ts
+1
-1
app/[locale]/feed/[feed]/route.ts
···
15
15
// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers
16
16
export const GET = async (_: Request, { params }: StaticParams) => {
17
17
// Generate the Feed for the given feed type (blog, releases, etc)
18
-
const websiteFeed = await provideWebsiteFeeds(params.feed);
18
+
const websiteFeed = provideWebsiteFeeds(params.feed);
19
19
20
20
return new NextResponse(websiteFeed, {
21
21
headers: { 'Content-Type': 'application/xml' },
+67
app/[locale]/next-data/blog-data/[category]/[page]/route.ts
+67
app/[locale]/next-data/blog-data/[category]/[page]/route.ts
···
1
+
import {
2
+
provideBlogCategories,
3
+
provideBlogPosts,
4
+
providePaginatedBlogPosts,
5
+
} from '@/next-data/providers/blogData';
6
+
import { VERCEL_REVALIDATE } from '@/next.constants.mjs';
7
+
import { defaultLocale } from '@/next.locales.mjs';
8
+
9
+
type StaticParams = {
10
+
params: { locale: string; category: string; page: string };
11
+
};
12
+
13
+
// This is the Route Handler for the `GET` method which handles the request
14
+
// for providing Blog Posts for Blog Categories and Pagination Metadata
15
+
// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers
16
+
export const GET = async (_: Request, { params }: StaticParams) => {
17
+
const requestedPage = Number(params.page);
18
+
19
+
const data =
20
+
requestedPage >= 1
21
+
? // This allows us to blindly get all blog posts from a given category
22
+
// if the page number is 0 or something smaller than 1
23
+
providePaginatedBlogPosts(params.category, requestedPage)
24
+
: provideBlogPosts(params.category);
25
+
26
+
return Response.json(data, { status: data.posts.length ? 200 : 404 });
27
+
};
28
+
29
+
// This function generates the static paths that come from the dynamic segments
30
+
// `[locale]/next-data/blog-data/[category]` and returns an array of all available static paths
31
+
// This is used for ISR static validation and generation
32
+
export const generateStaticParams = async () => {
33
+
// This metadata is the original list of all available categories and all available years
34
+
// within the Node.js Website Blog Posts (2011, 2012...)
35
+
const categories = provideBlogCategories();
36
+
37
+
const mappedCategories = categories.map(category => {
38
+
// gets the current pagination meta for a given category
39
+
const { pagination } = provideBlogPosts(category);
40
+
41
+
// creates a sequential array containing each page number
42
+
const pages = [...Array(pagination.pages).keys()].map((_, key) => key + 1);
43
+
44
+
// maps the data into valid Next.js Route Engine routes with all required params
45
+
// notice that we add an extra 0 in the beginning in case we want a non-paginated route
46
+
return [0, ...pages].map(page => ({
47
+
locale: defaultLocale.code,
48
+
page: String(page),
49
+
category,
50
+
}));
51
+
});
52
+
53
+
return mappedCategories.flat();
54
+
};
55
+
56
+
// Enforces that only the paths from `generateStaticParams` are allowed, giving 404 on the contrary
57
+
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams
58
+
export const dynamicParams = false;
59
+
60
+
// Enforces that this route is cached and static as much as possible
61
+
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
62
+
export const dynamic = 'force-static';
63
+
64
+
// Ensures that this endpoint is invalidated and re-executed every X minutes
65
+
// so that when new deployments happen, the data is refreshed
66
+
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#revalidate
67
+
export const revalidate = VERCEL_REVALIDATE;
-45
app/[locale]/next-data/blog-data/[category]/route.ts
-45
app/[locale]/next-data/blog-data/[category]/route.ts
···
1
-
import provideBlogData from '@/next-data/providers/blogData';
2
-
import { VERCEL_REVALIDATE } from '@/next.constants.mjs';
3
-
import { defaultLocale } from '@/next.locales.mjs';
4
-
5
-
// We only support fetching these pages from the /en/ locale code
6
-
const locale = defaultLocale.code;
7
-
8
-
type StaticParams = { params: { category: string; locale: string } };
9
-
10
-
// This is the Route Handler for the `GET` method which handles the request
11
-
// for providing Blog Posts, Pagination for every supported Blog Category
12
-
// this includes the `year-XXXX` categories for yearly archives (pagination)
13
-
// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers
14
-
export const GET = async (_: Request, { params }: StaticParams) => {
15
-
const data = await provideBlogData(params.category);
16
-
17
-
return Response.json(data, { status: data.posts.length ? 200 : 404 });
18
-
};
19
-
20
-
// This function generates the static paths that come from the dynamic segments
21
-
// `[locale]/next-data/blog-data/[category]` and returns an array of all available static paths
22
-
// This is used for ISR static validation and generation
23
-
export const generateStaticParams = async () => {
24
-
// This metadata is the original list of all available categories and all available years
25
-
// within the Node.js Website Blog Posts (2011, 2012...)
26
-
const { meta } = await provideBlogData();
27
-
28
-
return [
29
-
...meta.categories.map(category => ({ category, locale })),
30
-
...meta.pagination.map(year => ({ category: `year-${year}`, locale })),
31
-
];
32
-
};
33
-
34
-
// Forces that only the paths from `generateStaticParams` are allowed, giving 404 on the contrary
35
-
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams
36
-
export const dynamicParams = true;
37
-
38
-
// Enforces that this route is cached and static as much as possible
39
-
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
40
-
export const dynamic = 'force-static';
41
-
42
-
// Ensures that this endpoint is invalidated and re-executed every X minutes
43
-
// so that when new deployments happen, the data is refreshed
44
-
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#revalidate
45
-
export const revalidate = VERCEL_REVALIDATE;
+1
-1
app/[locale]/next-data/release-data/route.ts
+1
-1
app/[locale]/next-data/release-data/route.ts
···
9
9
// for generating static data related to the Node.js Release Data
10
10
// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers
11
11
export const GET = async () => {
12
-
const releaseData = await provideReleaseData();
12
+
const releaseData = provideReleaseData();
13
13
14
14
return Response.json(releaseData);
15
15
};
+11
-13
client-context.ts
+11
-13
client-context.ts
···
2
2
3
3
import type { ClientSharedServerContext } from '@/types';
4
4
5
+
import { assignClientContext } from './util/assignClientContext';
6
+
5
7
// This allows us to have Server-Side Context's of the shared "contextual" data
6
8
// which includes the frontmatter, the current pathname from the dynamic segments
7
9
// and the current headings of the current markdown context
8
10
export const getClientContext = cache(() => {
9
-
const serverSharedContext: ClientSharedServerContext = {
10
-
frontmatter: {},
11
-
pathname: '',
12
-
headings: [],
13
-
readingTime: { text: '', minutes: 0, time: 0, words: 0 },
14
-
filename: '',
15
-
};
11
+
const serverSharedContext = assignClientContext({});
16
12
17
13
return serverSharedContext;
18
14
});
19
15
20
16
// This is used by the dynamic router to define on the request
21
17
// the current set of information we use (shared)
22
-
export const setClientContext = (data: ClientSharedServerContext) => {
23
-
getClientContext().frontmatter = data.frontmatter;
24
-
getClientContext().pathname = data.pathname;
25
-
getClientContext().headings = data.headings;
26
-
getClientContext().readingTime = data.readingTime;
27
-
getClientContext().filename = data.filename;
18
+
export const setClientContext = (data: Partial<ClientSharedServerContext>) => {
19
+
const _data = assignClientContext(data);
20
+
21
+
getClientContext().frontmatter = _data.frontmatter;
22
+
getClientContext().pathname = _data.pathname;
23
+
getClientContext().headings = _data.headings;
24
+
getClientContext().readingTime = _data.readingTime;
25
+
getClientContext().filename = _data.filename;
28
26
};
+2
-4
components/Common/AvatarGroup/Avatar/index.module.css
+2
-4
components/Common/AvatarGroup/Avatar/index.module.css
+6
-1
components/Common/AvatarGroup/Avatar/index.tsx
+6
-1
components/Common/AvatarGroup/Avatar/index.tsx
···
10
10
11
11
const Avatar: FC<AvatarProps> = ({ src, alt }) => (
12
12
<RadixAvatar.Root className={styles.avatarRoot}>
13
-
<RadixAvatar.Image src={src} alt={alt} className={styles.avatar} />
13
+
<RadixAvatar.Image
14
+
loading="lazy"
15
+
src={src}
16
+
alt={alt}
17
+
className={styles.avatar}
18
+
/>
14
19
<RadixAvatar.Fallback delayMs={500} className={styles.avatar}>
15
20
{alt}
16
21
</RadixAvatar.Fallback>
+2
components/Common/AvatarGroup/index.tsx
+2
components/Common/AvatarGroup/index.tsx
+1
-2
components/Common/Badge/index.module.css
+1
-2
components/Common/Badge/index.module.css
+1
-2
components/Common/Banner/index.module.css
+1
-2
components/Common/Banner/index.module.css
+10
-11
components/Common/BlogPostCard/__tests__/index.test.mjs
+10
-11
components/Common/BlogPostCard/__tests__/index.test.mjs
···
8
8
description = 'Blog post description',
9
9
authors = [],
10
10
date = new Date(),
11
+
slug = '',
11
12
}) {
12
13
render(
13
14
<BlogPostCard
14
15
title={title}
15
-
type={type}
16
+
category={type}
16
17
description={description}
17
18
authors={authors}
18
19
date={date}
20
+
slug={slug}
19
21
/>
20
22
);
21
23
···
33
35
it('Renders the title prop correctly', () => {
34
36
const { title } = renderBlogPostCard({});
35
37
36
-
// Title from Preview component
37
-
expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(
38
-
title
39
-
);
38
+
expect(screen.getAllByText(title).length).toBe(2);
40
39
41
-
// The second title should be hidden for screen-readers
42
-
// to prevent them from reading it twice
43
-
expect(screen.getAllByText(title)[1]).toHaveAttribute(
40
+
// title from preview should be ignored as the one from Links
41
+
// and blog card/post are what matter
42
+
expect(screen.getAllByText(title)[0]).toHaveAttribute(
44
43
'aria-hidden',
45
44
'true'
46
45
);
···
53
52
});
54
53
55
54
it.each([
56
-
{ label: 'components.common.card.vulnerability', type: 'vulnerability' },
57
-
{ label: 'components.common.card.announcement', type: 'announcement' },
58
-
{ label: 'components.common.card.release', type: 'release' },
55
+
{ label: 'layouts.blog.categories.vulnerability', type: 'vulnerability' },
56
+
{ label: 'layouts.blog.categories.announcements', type: 'announcements' },
57
+
{ label: 'layouts.blog.categories.release', type: 'release' },
59
58
])(
60
59
'Renders "%label" text when passing it the type "%type"',
61
60
({ label, type }) => {
+4
-10
components/Common/BlogPostCard/index.module.css
+4
-10
components/Common/BlogPostCard/index.module.css
···
1
1
.container {
2
-
@apply max-w-full
3
-
bg-white
4
-
dark:bg-neutral-950;
5
-
}
6
-
7
-
.preview {
8
-
@apply mb-6
9
-
max-w-full
10
-
rounded
11
-
p-4;
2
+
@apply max-w-full;
12
3
}
13
4
14
5
.subtitle {
15
6
@apply mb-2
7
+
mt-6
8
+
inline-block
16
9
text-xs
17
10
font-semibold
18
11
text-green-600
···
21
14
22
15
.title {
23
16
@apply mb-2
17
+
block
24
18
text-xl
25
19
font-semibold
26
20
text-neutral-900
+2
-1
components/Common/BlogPostCard/index.stories.tsx
+2
-1
components/Common/BlogPostCard/index.stories.tsx
···
8
8
export const Default: Story = {
9
9
args: {
10
10
title: 'Node.js March 17th Infrastructure Incident Post-mortem',
11
-
type: 'vulnerability',
11
+
category: 'vulnerability',
12
12
description:
13
13
'Starting on March 15th and going through to March 17th (with much of the issue being mitigated on the 16th), users were receiving intermittent 404 responses when trying to download Node.js from nodejs.org, or even accessing parts of the website.',
14
14
authors: [
···
17
17
src: 'https://avatars.githubusercontent.com/u/',
18
18
},
19
19
],
20
+
slug: '/blog/vulnerability/something',
20
21
date: new Date('17 October 2023'),
21
22
},
22
23
decorators: [
+29
-30
components/Common/BlogPostCard/index.tsx
+29
-30
components/Common/BlogPostCard/index.tsx
···
1
1
import { useTranslations } from 'next-intl';
2
-
import { useMemo } from 'react';
3
-
import type { ComponentProps, FC } from 'react';
2
+
import type { FC } from 'react';
4
3
5
4
import AvatarGroup from '@/components/Common/AvatarGroup';
6
5
import Preview from '@/components/Common/Preview';
7
6
import { Time } from '@/components/Common/Time';
7
+
import Link from '@/components/Link';
8
+
import { mapBlogCategoryToPreviewType } from '@/util/blogUtils';
8
9
9
10
import styles from './index.module.css';
10
11
11
-
type Author = {
12
-
fullName: string;
13
-
src: string;
14
-
};
12
+
// @todo: this should probably be a global type?
13
+
type Author = { fullName: string; src: string };
15
14
16
15
type BlogPostCardProps = {
17
-
title: ComponentProps<typeof Preview>['title'];
18
-
type: Required<ComponentProps<typeof Preview>>['type'];
19
-
description: string;
16
+
title: string;
17
+
category: string;
18
+
description?: string;
20
19
authors: Array<Author>;
21
20
date: Date;
21
+
slug: string;
22
22
};
23
23
24
24
const BlogPostCard: FC<BlogPostCardProps> = ({
25
25
title,
26
-
type,
26
+
slug,
27
+
category,
27
28
description,
28
29
authors,
29
30
date,
30
31
}) => {
31
32
const t = useTranslations();
32
33
33
-
const avatars = useMemo(
34
-
() =>
35
-
authors.map(({ fullName, src }) => ({
36
-
alt: fullName,
37
-
src,
38
-
toString: () => fullName,
39
-
})),
40
-
[authors]
41
-
);
34
+
const avatars = authors.map(({ fullName, src }) => ({ alt: fullName, src }));
35
+
36
+
const type = mapBlogCategoryToPreviewType(category);
42
37
43
38
return (
44
39
<article className={styles.container}>
45
-
<Preview
46
-
title={title}
47
-
type={type}
48
-
height="auto"
49
-
className={styles.preview}
50
-
/>
51
-
<p className={styles.subtitle}>{t(`components.common.card.${type}`)}</p>
52
-
<p aria-hidden="true" className={styles.title}>
40
+
<Link href={slug} aria-label={title}>
41
+
<Preview title={title} type={type} />
42
+
</Link>
43
+
44
+
<Link href={`/blog/${category}`} className={styles.subtitle}>
45
+
{t(`layouts.blog.categories.${category}`)}
46
+
</Link>
47
+
48
+
<Link href={slug} className={styles.title}>
53
49
{title}
54
-
</p>
55
-
<p className={styles.description}>{description}</p>
50
+
</Link>
51
+
52
+
{description && <p className={styles.description}>{description}</p>}
53
+
56
54
<footer className={styles.footer}>
57
55
<AvatarGroup avatars={avatars} />
56
+
58
57
<div className={styles.author}>
59
-
<p>{avatars.join(', ')}</p>
58
+
<p>{avatars.map(avatar => avatar.alt).join(', ')}</p>
60
59
61
60
<Time
62
61
date={date}
+1
-2
components/Common/Breadcrumbs/BreadcrumbHomeLink/index.module.css
+1
-2
components/Common/Breadcrumbs/BreadcrumbHomeLink/index.module.css
+1
-2
components/Common/Breadcrumbs/BreadcrumbItem/index.module.css
+1
-2
components/Common/Breadcrumbs/BreadcrumbItem/index.module.css
+4
components/Common/Button/index.module.css
+4
components/Common/Button/index.module.css
···
69
69
70
70
&::before {
71
71
@apply absolute
72
+
left-0
72
73
right-0
73
74
top-0
74
75
-z-10
76
+
mx-auto
75
77
h-full
76
78
w-full
77
79
bg-gradient-glow-backdrop
···
82
84
&::after {
83
85
@apply absolute
84
86
-top-px
87
+
left-0
85
88
right-0
89
+
mx-auto
86
90
h-px
87
91
w-2/5
88
92
bg-gradient-to-r
+1
-2
components/Common/CodeBox/index.module.css
+1
-2
components/Common/CodeBox/index.module.css
+40
-39
components/Common/CodeTabs/index.module.css
+40
-39
components/Common/CodeTabs/index.module.css
···
1
-
.root > [role='tabpanel'] > :first-child {
2
-
@apply rounded-t-none;
3
-
}
1
+
.root {
2
+
> [role='tabpanel'] > :first-child {
3
+
@apply rounded-t-none;
4
+
}
4
5
5
-
.header {
6
-
@apply flex
7
-
rounded-t
8
-
border-x
9
-
border-t
10
-
border-neutral-900
11
-
bg-neutral-950
12
-
px-4
13
-
pt-3
14
-
xs:px-2;
6
+
> div:nth-of-type(1) {
7
+
@apply flex
8
+
rounded-t
9
+
border-x
10
+
border-t
11
+
border-neutral-900
12
+
bg-neutral-950
13
+
px-4
14
+
pt-3
15
+
xs:px-2;
15
16
16
-
& [role='tablist'] > button {
17
-
@apply border-b
18
-
border-b-transparent
19
-
px-1
20
-
text-neutral-200;
17
+
> button {
18
+
@apply border-b
19
+
border-b-transparent
20
+
px-1
21
+
text-neutral-200;
21
22
22
-
&[aria-selected='true'] {
23
-
@apply border-b-green-400
24
-
text-green-400;
23
+
&[data-state='active'] {
24
+
@apply border-b-green-400
25
+
text-green-400;
26
+
}
25
27
}
26
-
}
27
28
28
-
.link {
29
-
@apply hidden
30
-
items-center
31
-
gap-2
32
-
text-center
33
-
text-neutral-200
34
-
lg:flex;
29
+
.link {
30
+
@apply hidden
31
+
items-center
32
+
gap-2
33
+
text-center
34
+
text-neutral-200
35
+
lg:flex;
35
36
36
-
& > .icon {
37
-
@apply h-4
38
-
w-4
39
-
text-neutral-300;
40
-
}
37
+
& > .icon {
38
+
@apply size-4
39
+
text-neutral-300;
40
+
}
41
41
42
-
&:is(:link, :visited) {
43
-
&:hover {
44
-
@apply text-neutral-400;
42
+
&:is(:link, :visited) {
43
+
&:hover {
44
+
@apply text-neutral-400;
45
45
46
-
& > .icon {
47
-
@apply text-neutral-600;
46
+
& > .icon {
47
+
@apply text-neutral-600;
48
+
}
48
49
}
49
50
}
50
51
}
+4
-8
components/Common/CodeTabs/index.tsx
+4
-8
components/Common/CodeTabs/index.tsx
···
6
6
7
7
import styles from './index.module.css';
8
8
9
-
export type CodeTabsExternaLink = {
9
+
type CodeTabsProps = Pick<
10
+
ComponentProps<typeof Tabs>,
11
+
'tabs' | 'defaultValue'
12
+
> & {
10
13
linkUrl?: string;
11
14
linkText?: string;
12
15
};
13
16
14
-
type CodeTabsProps = Pick<
15
-
ComponentProps<typeof Tabs>,
16
-
'tabs' | 'onValueChange' | 'defaultValue'
17
-
> &
18
-
CodeTabsExternaLink;
19
-
20
17
const CodeTabs: FC<PropsWithChildren<CodeTabsProps>> = ({
21
18
children,
22
19
linkUrl,
···
26
23
<Tabs
27
24
{...props}
28
25
className={styles.root}
29
-
headerClassName={styles.header}
30
26
addons={
31
27
linkUrl &&
32
28
linkText && (
+1
-2
components/Common/CrossLink/index.module.css
+1
-2
components/Common/CrossLink/index.module.css
+36
components/Common/LinkTabs/index.module.css
+36
components/Common/LinkTabs/index.module.css
···
1
+
.tabsList {
2
+
@apply mb-6
3
+
mt-10
4
+
flex
5
+
gap-2
6
+
border-b
7
+
border-b-neutral-200
8
+
font-open-sans
9
+
xs:hidden
10
+
dark:border-b-neutral-800;
11
+
12
+
.tabsTrigger {
13
+
@apply border-b-2
14
+
border-b-transparent
15
+
px-1
16
+
pb-[11px]
17
+
text-sm
18
+
font-semibold
19
+
text-neutral-800
20
+
dark:text-neutral-200;
21
+
22
+
&[data-state='active'] {
23
+
@apply border-b-green-600
24
+
text-green-600
25
+
dark:border-b-green-400
26
+
dark:text-green-400;
27
+
}
28
+
}
29
+
}
30
+
31
+
.tabsSelect > div {
32
+
@apply my-6
33
+
hidden
34
+
w-full
35
+
xs:flex;
36
+
}
+48
components/Common/LinkTabs/index.tsx
+48
components/Common/LinkTabs/index.tsx
···
1
+
import type { FC, PropsWithChildren } from 'react';
2
+
3
+
import Link from '@/components/Link';
4
+
import WithRouterSelect from '@/components/withRouterSelect';
5
+
6
+
import styles from './index.module.css';
7
+
8
+
type LinkTab = { key: string; label: string; link: string };
9
+
10
+
type LinkTabsProps = {
11
+
label?: string;
12
+
tabs: Array<LinkTab>;
13
+
activeTab: string;
14
+
};
15
+
16
+
const LinkTabs: FC<PropsWithChildren<LinkTabsProps>> = ({
17
+
tabs,
18
+
label,
19
+
activeTab,
20
+
children,
21
+
}) => (
22
+
<>
23
+
<div className={styles.tabsList}>
24
+
{tabs.map(tab => (
25
+
<Link
26
+
key={tab.key}
27
+
href={tab.link}
28
+
className={styles.tabsTrigger}
29
+
data-state={tab.key === activeTab ? 'active' : 'inactive'}
30
+
>
31
+
{tab.label}
32
+
</Link>
33
+
))}
34
+
</div>
35
+
36
+
<div className={styles.tabsSelect}>
37
+
<WithRouterSelect
38
+
label={label}
39
+
defaultValue={tabs.find(tab => tab.key === activeTab)?.link}
40
+
values={tabs.map(tab => ({ label: tab.label, value: tab.link }))}
41
+
/>
42
+
</div>
43
+
44
+
{children}
45
+
</>
46
+
);
47
+
48
+
export default LinkTabs;
+4
-3
components/Common/Pagination/PaginationListItem/index.module.css
+4
-3
components/Common/Pagination/PaginationListItem/index.module.css
···
2
2
.listItem:link,
3
3
.listItem:active {
4
4
@apply flex
5
-
h-10
6
-
w-10
5
+
size-10
7
6
items-center
8
7
justify-center
9
8
rounded
···
16
15
17
16
&:hover {
18
17
@apply bg-neutral-100
19
-
dark:bg-neutral-900;
18
+
text-neutral-800
19
+
dark:bg-neutral-900
20
+
dark:text-neutral-200;
20
21
}
21
22
}
+1
-1
components/Common/Pagination/index.module.css
+1
-1
components/Common/Pagination/index.module.css
+4
components/Common/Pagination/index.tsx
+4
components/Common/Pagination/index.tsx
···
41
41
disabled={currentPage === 1}
42
42
kind="secondary"
43
43
className={styles.previousButton}
44
+
href={pages[currentPage - 2]?.url}
44
45
>
45
46
<ArrowLeftIcon className={styles.arrowIcon} />
46
47
<span>{t('components.common.pagination.prev')}</span>
47
48
</Button>
49
+
48
50
<ol className={styles.list}>{parsedPages}</ol>
51
+
49
52
<Button
50
53
aria-label={t('components.common.pagination.nextAriaLabel')}
51
54
disabled={currentPage === pages.length}
52
55
kind="secondary"
53
56
className={styles.nextButton}
57
+
href={pages[currentPage]?.url}
54
58
>
55
59
<span>{t('components.common.pagination.next')}</span>
56
60
<ArrowRightIcon className={styles.arrowIcon} />
+29
-10
components/Common/Preview/index.module.css
+29
-10
components/Common/Preview/index.module.css
···
1
1
.root {
2
2
@apply relative
3
3
flex
4
+
aspect-[1.90/1]
4
5
items-center
6
+
rounded
7
+
border
8
+
border-neutral-900
5
9
bg-neutral-950
6
10
bg-[url('/static/images/patterns/hexagon-grid.svg')]
7
11
bg-contain
8
-
bg-center;
12
+
bg-center
13
+
@container/preview;
9
14
10
15
&::after {
11
16
@apply absolute
···
15
20
w-1/3
16
21
rounded-full
17
22
bg-gradient-radial
18
-
blur-3xl
19
-
content-[''];
23
+
blur-2xl
24
+
content-['']
25
+
@md/preview:blur-3xl;
20
26
21
-
&.announcement {
27
+
&.announcements {
22
28
@apply from-green-700/90;
23
29
}
24
30
···
31
37
}
32
38
}
33
39
34
-
& > .container {
40
+
.container {
35
41
@apply z-10
36
42
mx-auto
37
43
flex
44
+
w-2/3
38
45
max-w-xl
39
46
flex-col
40
-
gap-12
47
+
gap-4
41
48
text-center
42
-
text-3xl
49
+
text-xs
43
50
font-semibold
44
-
text-white;
51
+
text-white
52
+
@sm/preview:text-base
53
+
@md/preview:gap-6
54
+
@md/preview:text-lg
55
+
@lg/preview:gap-8
56
+
@lg/preview:text-xl
57
+
@xl/preview:gap-12
58
+
@xl/preview:text-2xl
59
+
@2xl/preview:text-3xl;
45
60
46
-
& > .logo {
47
-
@apply mx-auto;
61
+
.logo {
62
+
@apply mx-auto
63
+
size-6
64
+
@md/preview:size-14
65
+
@lg/preview:size-16
66
+
@xl/preview:size-20;
48
67
}
49
68
}
50
69
}
+8
-10
components/Common/Preview/index.stories.tsx
+8
-10
components/Common/Preview/index.stories.tsx
···
5
5
type Story = StoryObj<typeof Preview>;
6
6
type Meta = MetaObj<typeof Preview>;
7
7
8
-
export const Default: Story = {
9
-
args: {
10
-
title:
11
-
'Changing the End-of-Life Date for Node.js 16 to September 11th, 2023',
12
-
},
13
-
};
14
-
15
8
export const Announcement: Story = {
16
9
args: {
17
-
type: 'announcement',
10
+
type: 'announcements',
18
11
title:
19
12
'Changing the End-of-Life Date for Node.js 16 to September 11th, 2023',
20
13
},
···
38
31
args: {
39
32
title:
40
33
'Changing the End-of-Life Date for Node.js 16 to September 11th, 2023',
41
-
width: 600,
42
-
height: 315,
43
34
},
35
+
decorators: [
36
+
Story => (
37
+
<div className="w-[600px]">
38
+
<Story />
39
+
</div>
40
+
),
41
+
],
44
42
};
45
43
46
44
export default { component: Preview } as Meta;
+10
-21
components/Common/Preview/index.tsx
+10
-21
components/Common/Preview/index.tsx
···
1
1
import classNames from 'classnames';
2
-
import type { CSSProperties, ComponentProps, FC, ReactNode } from 'react';
2
+
import type { FC } from 'react';
3
3
4
4
import JsIconWhite from '@/components/Icons/Logos/JsIconWhite';
5
+
import type { BlogPreviewType } from '@/types';
5
6
6
7
import styles from './index.module.css';
7
8
8
9
type PreviewProps = {
9
-
type?: 'announcement' | 'release' | 'vulnerability';
10
-
title: ReactNode;
11
-
height?: CSSProperties['height'];
12
-
width?: CSSProperties['width'];
13
-
} & Omit<ComponentProps<'div'>, 'children'>;
10
+
title: string;
11
+
type?: BlogPreviewType;
12
+
};
14
13
15
-
const Preview: FC<PreviewProps> = ({
16
-
type = 'announcement',
17
-
title,
18
-
height = 630,
19
-
width = 1200,
20
-
...props
21
-
}) => (
22
-
<div
23
-
{...props}
24
-
style={{ width, height, ...props.style }}
25
-
className={classNames(styles.root, styles[type], props.className)}
26
-
>
27
-
<div className={styles.container}>
28
-
<JsIconWhite className={styles.logo} width={71} height={80} />
29
-
<h2>{title}</h2>
14
+
const Preview: FC<PreviewProps> = ({ type = 'announcements', title }) => (
15
+
<div className={classNames(styles.root, styles[type])}>
16
+
<div className={styles.container} aria-hidden={true}>
17
+
<JsIconWhite className={styles.logo} />
18
+
{title}
30
19
</div>
31
20
</div>
32
21
);
+1
-1
components/Common/ProgressionSidebar/index.stories.tsx
+1
-1
components/Common/ProgressionSidebar/index.stories.tsx
+34
-13
components/Common/ProgressionSidebar/index.tsx
+34
-13
components/Common/ProgressionSidebar/index.tsx
···
1
+
import { useTranslations } from 'next-intl';
1
2
import type { ComponentProps, FC } from 'react';
2
3
3
4
import ProgressionSidebarGroup from '@/components/Common/ProgressionSidebar/ProgressionSidebarGroup';
4
-
import WithSidebarSelect from '@/components/withSidebarSelect';
5
+
import WithRouterSelect from '@/components/withRouterSelect';
6
+
import { useClientContext } from '@/hooks/react-server';
5
7
6
8
import styles from './index.module.css';
7
9
···
9
11
groups: Array<ComponentProps<typeof ProgressionSidebarGroup>>;
10
12
};
11
13
12
-
const ProgressionSidebar: FC<ProgressionSidebarProps> = ({ groups }) => (
13
-
<nav className={styles.wrapper}>
14
-
{groups.map(({ groupName, items }) => (
15
-
<ProgressionSidebarGroup
16
-
key={groupName.toString()}
17
-
groupName={groupName}
18
-
items={items}
14
+
const ProgressionSidebar: FC<ProgressionSidebarProps> = ({ groups }) => {
15
+
const t = useTranslations();
16
+
const { pathname } = useClientContext();
17
+
18
+
const selectItems = groups.map(({ items, groupName }) => ({
19
+
label: groupName,
20
+
items: items.map(({ label, link }) => ({ value: link, label })),
21
+
}));
22
+
23
+
const currentItem = selectItems
24
+
.map(item => item.items)
25
+
.flat()
26
+
.find(item => pathname === item.value);
27
+
28
+
return (
29
+
<nav className={styles.wrapper}>
30
+
{groups.map(({ groupName, items }) => (
31
+
<ProgressionSidebarGroup
32
+
key={groupName.toString()}
33
+
groupName={groupName}
34
+
items={items}
35
+
/>
36
+
))}
37
+
38
+
<WithRouterSelect
39
+
label={t('components.common.sidebar.title')}
40
+
values={selectItems}
41
+
defaultValue={currentItem?.value}
19
42
/>
20
-
))}
21
-
22
-
<WithSidebarSelect groups={groups} />
23
-
</nav>
24
-
);
43
+
</nav>
44
+
);
45
+
};
25
46
26
47
export default ProgressionSidebar;
+1
-2
components/Common/Select/index.module.css
+1
-2
components/Common/Select/index.module.css
+1
-1
components/Common/Select/index.stories.tsx
+1
-1
components/Common/Select/index.stories.tsx
+11
-12
components/Common/Tabs/index.module.css
+11
-12
components/Common/Tabs/index.module.css
···
11
11
text-sm
12
12
font-semibold
13
13
text-neutral-800
14
-
data-[state=active]:border-b-green-600
15
-
data-[state=active]:text-green-600
16
-
dark:text-neutral-200
17
-
dark:data-[state=active]:border-b-green-400
18
-
dark:data-[state=active]:text-green-400;
14
+
dark:text-neutral-200;
15
+
16
+
&[data-state='active'] {
17
+
@apply border-b-green-600
18
+
text-green-600
19
+
dark:border-b-green-400
20
+
dark:text-green-400;
21
+
}
19
22
}
20
-
}
21
23
22
-
.tabsWithAddons {
23
-
@apply flex
24
-
justify-between;
25
-
26
-
& > .addons {
27
-
@apply border-b-2
24
+
.addons {
25
+
@apply ml-auto
26
+
border-b-2
28
27
border-b-transparent
29
28
px-1
30
29
pb-[11px]
+14
-26
components/Common/Tabs/index.tsx
+14
-26
components/Common/Tabs/index.tsx
···
1
1
import * as TabsPrimitive from '@radix-ui/react-tabs';
2
-
import classNames from 'classnames';
3
2
import type { FC, PropsWithChildren, ReactNode } from 'react';
4
3
5
4
import styles from './index.module.css';
6
5
7
-
type Tab = {
8
-
key: string;
9
-
label: string;
10
-
};
6
+
type Tab = { key: string; label: string };
11
7
12
-
type TabsProps = {
8
+
type TabsProps = TabsPrimitive.TabsProps & {
13
9
tabs: Array<Tab>;
14
10
addons?: ReactNode;
15
-
headerClassName?: string;
16
-
} & TabsPrimitive.TabsProps;
11
+
};
17
12
18
13
const Tabs: FC<PropsWithChildren<TabsProps>> = ({
19
14
tabs,
20
15
addons,
21
-
headerClassName,
22
16
children,
23
17
...props
24
18
}) => (
25
19
<TabsPrimitive.Root {...props}>
26
-
<div
27
-
className={classNames(headerClassName, {
28
-
[styles.tabsWithAddons]: addons != null,
29
-
})}
30
-
>
31
-
<TabsPrimitive.List className={classNames(styles.tabsList)}>
32
-
{tabs.map(tab => (
33
-
<TabsPrimitive.Trigger
34
-
key={tab.key}
35
-
value={tab.key}
36
-
className={styles.tabsTrigger}
37
-
>
38
-
{tab.label}
39
-
</TabsPrimitive.Trigger>
40
-
))}
41
-
</TabsPrimitive.List>
20
+
<TabsPrimitive.List className={styles.tabsList}>
21
+
{tabs.map(tab => (
22
+
<TabsPrimitive.Trigger
23
+
key={tab.key}
24
+
value={tab.key}
25
+
className={styles.tabsTrigger}
26
+
>
27
+
{tab.label}
28
+
</TabsPrimitive.Trigger>
29
+
))}
42
30
43
31
{addons && <div className={styles.addons}>{addons}</div>}
44
-
</div>
32
+
</TabsPrimitive.List>
45
33
46
34
{children}
47
35
</TabsPrimitive.Root>
+1
-2
components/Common/ThemeToggle/index.module.css
+1
-2
components/Common/ThemeToggle/index.module.css
+4
-2
components/Containers/MetaBar/index.module.css
+4
-2
components/Containers/MetaBar/index.module.css
···
3
3
flex-col
4
4
items-start
5
5
gap-8
6
+
overflow-y-auto
6
7
border-l
7
8
border-l-neutral-200
8
9
px-4
···
38
39
@apply font-semibold
39
40
text-neutral-900
40
41
underline
42
+
xs:inline-block
43
+
xs:py-1
41
44
dark:text-white;
42
45
43
46
&:hover {
···
55
58
}
56
59
57
60
svg {
58
-
@apply h-4
59
-
w-4
61
+
@apply size-4
60
62
text-neutral-600
61
63
dark:text-neutral-400;
62
64
}
+8
-6
components/Containers/MetaBar/index.tsx
+8
-6
components/Containers/MetaBar/index.tsx
···
29
29
return (
30
30
<div className={styles.wrapper}>
31
31
<dl>
32
-
{Object.entries(items).map(([key, value]) => (
33
-
<Fragment key={key}>
34
-
<dt>{t(key)}</dt>
35
-
<dd>{value}</dd>
36
-
</Fragment>
37
-
))}
32
+
{Object.entries(items)
33
+
.filter(([, value]) => !!value)
34
+
.map(([key, value]) => (
35
+
<Fragment key={key}>
36
+
<dt>{t(key)}</dt>
37
+
<dd>{value}</dd>
38
+
</Fragment>
39
+
))}
38
40
39
41
{heading.length > 0 && (
40
42
<>
+36
-13
components/Containers/Sidebar/index.tsx
+36
-13
components/Containers/Sidebar/index.tsx
···
1
+
import { useTranslations } from 'next-intl';
1
2
import type { ComponentProps, FC } from 'react';
2
3
3
4
import SidebarGroup from '@/components/Containers/Sidebar/SidebarGroup';
4
-
import WithSidebarSelect from '@/components/withSidebarSelect';
5
+
import WithRouterSelect from '@/components/withRouterSelect';
6
+
import { useClientContext } from '@/hooks/react-server';
5
7
6
8
import styles from './index.module.css';
7
9
···
9
11
groups: Array<ComponentProps<typeof SidebarGroup>>;
10
12
};
11
13
12
-
const SideBar: FC<SidebarProps> = ({ groups }) => (
13
-
<aside className={styles.wrapper}>
14
-
{groups.map(({ groupName, items }) => (
15
-
<SidebarGroup
16
-
key={groupName.toString()}
17
-
groupName={groupName}
18
-
items={items}
19
-
/>
20
-
))}
14
+
const SideBar: FC<SidebarProps> = ({ groups }) => {
15
+
const t = useTranslations();
16
+
const { pathname } = useClientContext();
17
+
18
+
const selectItems = groups.map(({ items, groupName }) => ({
19
+
label: groupName,
20
+
items: items.map(({ label, link }) => ({ value: link, label })),
21
+
}));
22
+
23
+
const currentItem = selectItems
24
+
.map(item => item.items)
25
+
.flat()
26
+
.find(item => pathname === item.value);
27
+
28
+
return (
29
+
<aside className={styles.wrapper}>
30
+
{groups.map(({ groupName, items }) => (
31
+
<SidebarGroup
32
+
key={groupName.toString()}
33
+
groupName={groupName}
34
+
items={items}
35
+
/>
36
+
))}
21
37
22
-
<WithSidebarSelect groups={groups} />
23
-
</aside>
24
-
);
38
+
{selectItems.length > 0 && (
39
+
<WithRouterSelect
40
+
label={t('components.common.sidebar.title')}
41
+
values={selectItems}
42
+
defaultValue={currentItem?.value}
43
+
/>
44
+
)}
45
+
</aside>
46
+
);
47
+
};
25
48
26
49
export default SideBar;
+2
-4
components/Downloads/ChangelogModal/index.module.css
+2
-4
components/Downloads/ChangelogModal/index.module.css
+3
-2
components/Downloads/DownloadButton/index.tsx
+3
-2
components/Downloads/DownloadButton/index.tsx
···
18
18
children,
19
19
}) => {
20
20
const { os, bitness } = useDetectOS();
21
+
const downloadLink = downloadUrlByOS(versionWithPrefix, os, bitness);
21
22
22
23
return (
23
24
<>
24
25
<Button
25
26
kind="special"
26
-
href={downloadUrlByOS(versionWithPrefix, os, bitness)}
27
+
href={downloadLink}
27
28
className={classNames(styles.downloadButton, 'hidden dark:flex')}
28
29
>
29
30
{children}
···
33
34
34
35
<Button
35
36
kind="primary"
36
-
href={downloadUrlByOS(versionWithPrefix, os, bitness)}
37
+
href={downloadLink}
37
38
className={classNames(styles.downloadButton, 'flex dark:hidden')}
38
39
>
39
40
{children}
+21
components/Downloads/DownloadLink.tsx
+21
components/Downloads/DownloadLink.tsx
···
1
+
'use client';
2
+
3
+
import type { FC, PropsWithChildren } from 'react';
4
+
5
+
import { useDetectOS } from '@/hooks';
6
+
import type { NodeRelease } from '@/types';
7
+
import { downloadUrlByOS } from '@/util/downloadUrlByOS';
8
+
9
+
type DownloadLinkProps = { release: NodeRelease };
10
+
11
+
const DownloadLink: FC<PropsWithChildren<DownloadLinkProps>> = ({
12
+
release: { versionWithPrefix },
13
+
children,
14
+
}) => {
15
+
const { os, bitness } = useDetectOS();
16
+
const downloadLink = downloadUrlByOS(versionWithPrefix, os, bitness);
17
+
18
+
return <a href={downloadLink}>{children}</a>;
19
+
};
20
+
21
+
export default DownloadLink;
+6
-4
components/MDX/CodeTabs/index.tsx
+6
-4
components/MDX/CodeTabs/index.tsx
···
1
1
'use client';
2
2
3
3
import * as TabsPrimitive from '@radix-ui/react-tabs';
4
-
import type { FC, ReactElement } from 'react';
4
+
import type { ComponentProps, FC, ReactElement } from 'react';
5
5
6
-
import type { CodeTabsExternaLink } from '@/components/Common/CodeTabs';
7
6
import CodeTabs from '@/components/Common/CodeTabs';
8
7
9
-
type MDXCodeTabsProps = {
8
+
type MDXCodeTabsProps = Pick<
9
+
ComponentProps<typeof CodeTabs>,
10
+
'linkText' | 'linkUrl'
11
+
> & {
10
12
children: Array<ReactElement>;
11
13
languages: string;
12
14
displayNames?: string;
13
15
defaultTab?: string;
14
-
} & CodeTabsExternaLink;
16
+
};
15
17
16
18
const MDXCodeTabs: FC<MDXCodeTabsProps> = ({
17
19
languages: rawLanguages,
+11
-8
components/Pagination.tsx
+11
-8
components/Pagination.tsx
···
2
2
import type { FC } from 'react';
3
3
4
4
import Link from '@/components/Link';
5
+
import type { BlogPagination } from '@/types';
5
6
6
-
type PaginationProps = { prev?: number | null; next?: number | null };
7
+
type PaginationProps = BlogPagination & { category: string };
7
8
8
-
const Pagination: FC<PaginationProps> = ({ next, prev }) => {
9
+
const Pagination: FC<PaginationProps> = ({ category, next, prev }) => {
9
10
const t = useTranslations();
10
11
11
12
return (
12
13
<nav aria-label="pagination" className="pagination">
13
-
{next && (
14
-
<Link href={`/blog/year-${next}`}>
15
-
< {t('components.pagination.next')}
14
+
{prev && (
15
+
<Link href={`/blog/${category}/page/${prev}`}>
16
+
< {t('components.pagination.previous')}
16
17
</Link>
17
18
)}
18
19
19
-
{prev && (
20
-
<Link href={`/blog/year-${prev}`}>
21
-
{t('components.pagination.previous')} >
20
+
{prev && next && ' | '}
21
+
22
+
{next && (
23
+
<Link href={`/blog/${category}/page/${next}`}>
24
+
{t('components.pagination.next')} >
22
25
</Link>
23
26
)}
24
27
</nav>
+3
-1
components/__design__/text.stories.tsx
+3
-1
components/__design__/text.stories.tsx
+60
components/withBlogCategories.tsx
+60
components/withBlogCategories.tsx
···
1
+
import { useTranslations } from 'next-intl';
2
+
import type { ComponentProps, FC } from 'react';
3
+
4
+
import BlogPostCard from '@/components/Common/BlogPostCard';
5
+
import LinkTabs from '@/components/Common/LinkTabs';
6
+
import Pagination from '@/components/Common/Pagination';
7
+
import type { BlogPostsRSC } from '@/types';
8
+
import { mapAuthorToCardAuthors } from '@/util/blogUtils';
9
+
10
+
type WithBlogCategoriesProps = {
11
+
categories: ComponentProps<typeof LinkTabs>['tabs'];
12
+
blogData: BlogPostsRSC & { category: string; page: number };
13
+
};
14
+
15
+
const mapPaginationPages = (category: string, pages: number) =>
16
+
[...Array(pages).keys()].map(page => ({
17
+
url: `/blog/${category}/page/${page + 1}`,
18
+
}));
19
+
20
+
const WithBlogCategories: FC<WithBlogCategoriesProps> = ({
21
+
categories,
22
+
blogData,
23
+
}) => {
24
+
const t = useTranslations();
25
+
26
+
return (
27
+
<>
28
+
<LinkTabs
29
+
label={t('layouts.blog.selectCategory')}
30
+
tabs={categories}
31
+
activeTab={blogData.category}
32
+
>
33
+
<div className="grid grid-cols-[repeat(auto-fill,minmax(theme(spacing.96),1fr))] [grid-gap:theme(spacing.12)_theme(spacing.8)]">
34
+
{blogData.posts.map(post => (
35
+
<BlogPostCard
36
+
key={post.slug}
37
+
title={post.title}
38
+
category={post.categories[0]}
39
+
authors={mapAuthorToCardAuthors(post.author)}
40
+
date={post.date}
41
+
slug={post.slug}
42
+
/>
43
+
))}
44
+
</div>
45
+
</LinkTabs>
46
+
47
+
<div className="mt-4 border-t border-t-neutral-200 pt-5 md:mt-8 dark:border-t-neutral-900">
48
+
<Pagination
49
+
currentPage={blogData.page}
50
+
pages={mapPaginationPages(
51
+
blogData.category,
52
+
blogData.pagination.pages
53
+
)}
54
+
/>
55
+
</div>
56
+
</>
57
+
);
58
+
};
59
+
60
+
export default WithBlogCategories;
+45
components/withBlogCrossLinks.tsx
+45
components/withBlogCrossLinks.tsx
···
1
+
import type { FC } from 'react';
2
+
3
+
import { getClientContext } from '@/client-context';
4
+
import CrossLink from '@/components/Common/CrossLink';
5
+
import getBlogData from '@/next-data/blogData';
6
+
7
+
const WithBlogCrossLinks: FC = async () => {
8
+
const { pathname } = getClientContext();
9
+
10
+
// Extracts from the static URL the components used for the Blog Post slug
11
+
const [, , category, postname] = pathname.split('/');
12
+
13
+
const { posts } = await getBlogData(category);
14
+
15
+
const currentItem = posts.findIndex(
16
+
({ slug }) => slug === `/blog/${category}/${postname}`
17
+
);
18
+
19
+
const [previousCrossLink, nextCrossLink] = [
20
+
posts[currentItem - 1],
21
+
posts[currentItem + 1],
22
+
];
23
+
24
+
return (
25
+
<div className="mt-4 grid w-full grid-cols-2 gap-4 xs:grid-cols-1">
26
+
{(previousCrossLink && (
27
+
<CrossLink
28
+
type="previous"
29
+
text={previousCrossLink.title}
30
+
link={previousCrossLink.slug}
31
+
/>
32
+
)) || <div />}
33
+
34
+
{nextCrossLink && (
35
+
<CrossLink
36
+
type="next"
37
+
text={nextCrossLink.title}
38
+
link={nextCrossLink.slug}
39
+
/>
40
+
)}
41
+
</div>
42
+
);
43
+
};
44
+
45
+
export default WithBlogCrossLinks;
+3
-5
components/withCrossLinks.tsx
components/withSidebarCrossLinks.tsx
+3
-5
components/withCrossLinks.tsx
components/withSidebarCrossLinks.tsx
···
4
4
import { useClientContext, useSiteNavigation } from '@/hooks/server';
5
5
import type { NavigationKeys } from '@/types';
6
6
7
-
type WithCrossLinksProps = {
8
-
navKey: NavigationKeys;
9
-
};
7
+
type WithCrossLinksProps = { navKey: NavigationKeys };
10
8
11
-
const WithCrossLinks: FC<WithCrossLinksProps> = ({ navKey }) => {
9
+
const WithSidebarCrossLinks: FC<WithCrossLinksProps> = ({ navKey }) => {
12
10
const { getSideNavigation } = useSiteNavigation();
13
11
const { pathname } = useClientContext();
14
12
···
46
44
);
47
45
};
48
46
49
-
export default WithCrossLinks;
47
+
export default WithSidebarCrossLinks;
+4
components/withLayout.tsx
+4
components/withLayout.tsx
···
9
9
import LegacyIndexLayout from '@/layouts/IndexLayout';
10
10
import LegacyLearnLayout from '@/layouts/LearnLayout';
11
11
import AboutLayout from '@/layouts/New/About';
12
+
import BlogLayout from '@/layouts/New/Blog';
12
13
import DefaultLayout from '@/layouts/New/Default';
13
14
import DocsLayout from '@/layouts/New/Docs';
14
15
import HomeLayout from '@/layouts/New/Home';
15
16
import LearnLayout from '@/layouts/New/Learn';
17
+
import PostLayout from '@/layouts/New/Post';
16
18
import { ENABLE_WEBSITE_REDESIGN } from '@/next.constants.mjs';
17
19
import type { Layouts, LegacyLayouts } from '@/types';
18
20
···
35
37
'home.hbs': HomeLayout,
36
38
'learn.hbs': LearnLayout,
37
39
'page.hbs': DefaultLayout,
40
+
'blog-post.hbs': PostLayout,
41
+
'blog-category.hbs': BlogLayout,
38
42
} satisfies Record<Layouts, FC>;
39
43
40
44
type WithLayout<L = Layouts | LegacyLayouts> = PropsWithChildren<{ layout: L }>;
+9
-5
components/withMetaBar.tsx
+9
-5
components/withMetaBar.tsx
···
7
7
import { useClientContext } from '@/hooks/server';
8
8
import { getGitHubEditPageUrl } from '@/util/gitHubUtils';
9
9
10
+
const DATE_FORMAT = {
11
+
month: 'short',
12
+
day: '2-digit',
13
+
year: 'numeric',
14
+
} as const;
15
+
10
16
const WithMetaBar: FC = () => {
11
17
const { headings, readingTime, frontmatter, filename } = useClientContext();
12
18
const formatter = useFormatter();
13
19
14
-
const lastUpdated = formatter.dateTime(frontmatter.date ?? new Date(), {
15
-
month: 'short',
16
-
day: '2-digit',
17
-
year: 'numeric',
18
-
});
20
+
const lastUpdated = frontmatter.date
21
+
? formatter.dateTime(new Date(frontmatter.date), DATE_FORMAT)
22
+
: undefined;
19
23
20
24
return (
21
25
<MetaBar
+30
components/withRouterSelect.tsx
+30
components/withRouterSelect.tsx
···
1
+
'use client';
2
+
3
+
import type { ComponentProps, FC } from 'react';
4
+
5
+
import Select from '@/components/Common/Select';
6
+
import { useRouter } from '@/navigation.mjs';
7
+
8
+
type WithSidebarSelectProps = Pick<
9
+
ComponentProps<typeof Select>,
10
+
'values' | 'defaultValue' | 'label'
11
+
>;
12
+
13
+
const WithRouterSelect: FC<WithSidebarSelectProps> = ({
14
+
values,
15
+
label,
16
+
defaultValue,
17
+
}) => {
18
+
const { push } = useRouter();
19
+
20
+
return (
21
+
<Select
22
+
label={label}
23
+
values={values}
24
+
defaultValue={defaultValue}
25
+
onChange={value => push(value)}
26
+
/>
27
+
);
28
+
};
29
+
30
+
export default WithRouterSelect;
-46
components/withSidebarSelect.tsx
-46
components/withSidebarSelect.tsx
···
1
-
'use client';
2
-
3
-
import { useTranslations } from 'next-intl';
4
-
import type { FC } from 'react';
5
-
6
-
import Select from '@/components/Common/Select';
7
-
import { useClientContext } from '@/hooks';
8
-
import { useRouter } from '@/navigation.mjs';
9
-
import type { FormattedMessage } from '@/types';
10
-
11
-
type SelectItem = {
12
-
label: FormattedMessage;
13
-
link: string;
14
-
};
15
-
16
-
type WithSidebarSelectProps = {
17
-
groups: Array<{ groupName: FormattedMessage; items: Array<SelectItem> }>;
18
-
};
19
-
20
-
const WithSidebarSelect: FC<WithSidebarSelectProps> = ({ groups }) => {
21
-
const t = useTranslations();
22
-
23
-
const { pathname } = useClientContext();
24
-
const { push } = useRouter();
25
-
26
-
const selectItems = groups.map(({ items, groupName }) => ({
27
-
label: groupName,
28
-
items: items.map(({ label, link }) => ({ value: link, label })),
29
-
}));
30
-
31
-
const currentItem = selectItems
32
-
.map(item => item.items)
33
-
.flat()
34
-
.find(item => pathname === item.value);
35
-
36
-
return (
37
-
<Select
38
-
label={t('components.common.sidebar.title')}
39
-
values={selectItems}
40
-
defaultValue={currentItem?.value}
41
-
onChange={value => push(value)}
42
-
/>
43
-
);
44
-
};
45
-
46
-
export default WithSidebarSelect;
+24
-9
i18n/locales/en.json
+24
-9
i18n/locales/en.json
···
38
38
"howMuchJavascriptDoYouNeedToKnowToUseNodejs": "How much JavaScript do you need to know to use Node.js?",
39
39
"differencesBetweenNodejsAndTheBrowser": "Differences between Node.js and the Browser",
40
40
"theV8JavascriptEngine": "The V8 JavaScript Engine",
41
-
"anIntroductionToTheNpmPackageManager": "An introduction to the NPM package manager",
41
+
"anIntroductionToTheNpmPackageManager": "An introduction to the npm package manager",
42
42
"ecmascript2015Es6AndBeyond": "ECMAScript 2015 (ES6) and beyond",
43
43
"nodejsTheDifferenceBetweenDevelopmentAndProduction": "Node.js, the difference between development and production",
44
44
"nodejsWithTypescript": "Node.js with TypeScript",
···
118
118
"docs": "Docs"
119
119
},
120
120
"pagination": {
121
-
"next": "Newer | ",
122
-
"previous": "Older"
121
+
"next": "Next",
122
+
"previous": "Previous"
123
123
},
124
124
"common": {
125
125
"breadcrumbs": {
···
146
146
},
147
147
"languageDropdown": {
148
148
"label": "Choose Language"
149
-
},
150
-
"card": {
151
-
"announcement": "Announcements",
152
-
"release": "Releases",
153
-
"vulnerability": "Vulnerabilities"
154
149
}
155
150
},
156
151
"metabar": {
···
192
187
}
193
188
},
194
189
"blogIndex": {
195
-
"currentYear": "Blog from {year}"
190
+
"categoryName": "{category, select, all {Blog} other {{category} Blog Posts}}"
191
+
},
192
+
"blog": {
193
+
"title": "Blog",
194
+
"subtitle": "The latest Node.js news, case studies, tutorials, and resources.",
195
+
"selectCategory": "Categories",
196
+
"categories": {
197
+
"all": "Everything",
198
+
"announcements": "Announcements",
199
+
"release": "Releases",
200
+
"vulnerability": "Vulnerabilities",
201
+
"advisory-board": "Advisory Board",
202
+
"community": "Community",
203
+
"feature": "Feature",
204
+
"module": "Module",
205
+
"npm": "npm",
206
+
"uncategorized": "Uncategorized",
207
+
"video": "Video",
208
+
"weekly-updates": "Weekly Updates",
209
+
"wg": "Working Groups"
210
+
}
196
211
}
197
212
},
198
213
"pages": {
+19
-43
layouts/BlogCategoryLayout.tsx
+19
-43
layouts/BlogCategoryLayout.tsx
···
1
-
import { notFound } from 'next/navigation';
2
1
import { getTranslations } from 'next-intl/server';
3
2
import type { FC } from 'react';
4
3
···
9
8
import getBlogData from '@/next-data/blogData';
10
9
11
10
const getCategoryData = async (pathname: string) => {
12
-
// We split the pathname to retrieve the blog category from it since the
13
-
// URL is usually /blog/{category} the second path piece is usually the
14
-
// category name, which usually year-YYYY
15
-
const [, _pathname, category] = pathname.split('/');
16
-
if (_pathname === 'blog' && category && category.length) {
17
-
const data = await getBlogData(category);
18
-
return { ...data, category };
19
-
}
11
+
// pathname format can either be: /en/blog/{category}
12
+
// or /en/blog/{category}/page/{page}
13
+
// hence we attempt to interpolate the full /en/blog/{categoy}/page/{page}
14
+
// and in case of course no page argument is provided we define it to 1
15
+
// note that malformed routes can't happen as they are all statically generated
16
+
const [, , category = 'all', , page = 1] = pathname.split('/');
20
17
21
-
// If the pathname does not match to a blog page,
22
-
// which should not happen (as this hook should only be used in blog pages),
23
-
// or, if there is no category in the URL,
24
-
// which happens when we're on the blog overview page (index),
25
-
// then we attempt to get the posts for the current year
26
-
// @TODO: Year-based pagination is deprecated and going away soon
27
-
let year = `year-${new Date().getFullYear()}`;
28
-
let data = await getBlogData(year);
29
-
30
-
// If there are no posts in the current year,
31
-
// and there is at least one year in the pagination array,
32
-
// we'll get the posts for the most recent year
33
-
if (!data.posts.length && data.meta.pagination.length) {
34
-
year = `year-${Math.max(...data.meta.pagination)}`;
35
-
data = await getBlogData(year);
36
-
}
18
+
const { posts, pagination } = await getBlogData(category, Number(page));
37
19
38
-
return { ...data, category: year };
20
+
return { posts, category, pagination };
39
21
};
40
22
41
23
// This is a React Async Server Component
42
24
// Note that Hooks cannot be used in a RSC async component
43
25
// Async Components do not get re-rendered at all.
44
26
const BlogCategoryLayout: FC = async () => {
45
-
const { frontmatter, pathname } = getClientContext();
27
+
const { pathname } = getClientContext();
46
28
47
29
const t = await getTranslations();
48
30
49
31
const { posts, pagination, category } = await getCategoryData(pathname);
50
32
51
-
// This only applies if current category is a year category
52
-
const year = category.replace('year-', '');
53
-
const title = category.startsWith('year-')
54
-
? t('layouts.blogIndex.currentYear', { year })
55
-
: frontmatter.title;
56
-
57
-
// This ensures that whenever we're running on dynamic generation (SSG)
58
-
// that invalid categories or categories/pages without posts will redirect to the 404 page
59
-
// however, the blog overview page (index) will always be generated, even if there are no posts
60
-
if (posts.length === 0 && pathname !== '/blog') {
61
-
return notFound();
62
-
}
63
-
64
33
return (
65
34
<div className="container" dir="auto">
66
-
<h2>{title}</h2>
35
+
<h2 style={{ textTransform: 'capitalize' }}>
36
+
{t('layouts.blogIndex.categoryName', {
37
+
category: category.replace('year-', ''),
38
+
})}
39
+
</h2>
67
40
68
41
<ul className="blog-index">
69
42
{posts.map(({ slug, date, title }) => (
70
43
<li key={slug}>
71
-
<Time date={date} format={{ month: 'short', day: '2-digit' }} />
44
+
<Time
45
+
date={date}
46
+
format={{ year: 'numeric', month: 'short', day: '2-digit' }}
47
+
/>
72
48
<Link href={slug}>{title}</Link>
73
49
</li>
74
50
))}
75
51
</ul>
76
52
77
-
<Pagination {...pagination} />
53
+
<Pagination category={category} {...pagination} />
78
54
</div>
79
55
);
80
56
};
+65
layouts/New/Blog.tsx
+65
layouts/New/Blog.tsx
···
1
+
import { getTranslations } from 'next-intl/server';
2
+
import type { FC } from 'react';
3
+
4
+
import { getClientContext } from '@/client-context';
5
+
import WithBlogCategories from '@/components/withBlogCategories';
6
+
import WithFooter from '@/components/withFooter';
7
+
import WithNavBar from '@/components/withNavBar';
8
+
import getBlogData from '@/next-data/blogData';
9
+
10
+
import styles from './layouts.module.css';
11
+
12
+
const getBlogCategory = async (pathname: string) => {
13
+
// pathname format can either be: /en/blog/{category}
14
+
// or /en/blog/{category}/page/{page}
15
+
// hence we attempt to interpolate the full /en/blog/{categoy}/page/{page}
16
+
// and in case of course no page argument is provided we define it to 1
17
+
// note that malformed routes can't happen as they are all statically generated
18
+
const [, , category = 'all', , page = 1] = pathname.split('/');
19
+
20
+
const { posts, pagination } = await getBlogData(category, Number(page));
21
+
22
+
return { category, posts, pagination, page: Number(page) };
23
+
};
24
+
25
+
const BlogLayout: FC = async () => {
26
+
const { pathname } = getClientContext();
27
+
const t = await getTranslations();
28
+
29
+
const mapCategoriesToTabs = (categories: Array<string>) =>
30
+
categories.map(category => ({
31
+
key: category,
32
+
label: t(`layouts.blog.categories.${category}`),
33
+
link: `/blog/${category}`,
34
+
}));
35
+
36
+
const blogData = await getBlogCategory(pathname);
37
+
38
+
return (
39
+
<>
40
+
<WithNavBar />
41
+
42
+
<div className={styles.blogLayout}>
43
+
<main>
44
+
<h1>{t('layouts.blog.title')}</h1>
45
+
46
+
<p>{t('layouts.blog.subtitle')}</p>
47
+
48
+
<WithBlogCategories
49
+
blogData={blogData}
50
+
categories={mapCategoriesToTabs([
51
+
'all',
52
+
'announcements',
53
+
'release',
54
+
'vulnerability',
55
+
])}
56
+
/>
57
+
</main>
58
+
</div>
59
+
60
+
<WithFooter />
61
+
</>
62
+
);
63
+
};
64
+
65
+
export default BlogLayout;
+9
layouts/New/Content.tsx
+9
layouts/New/Content.tsx
+6
-7
layouts/New/Docs.tsx
+6
-7
layouts/New/Docs.tsx
···
3
3
import WithFooter from '@/components/withFooter';
4
4
import WithMetaBar from '@/components/withMetaBar';
5
5
import WithNavBar from '@/components/withNavBar';
6
-
import WithSideBar from '@/components/withSidebar';
7
-
import ArticleLayout from '@/layouts/New/Article';
6
+
import ContentLayout from '@/layouts/New/Content';
8
7
9
8
// @deprecated: This Layout is Temporary. The `en/docs` route should eventually be removed
10
9
// and all "guides" moved to the Learn section.
···
13
12
<>
14
13
<WithNavBar />
15
14
16
-
<ArticleLayout>
17
-
<WithSideBar navKeys={[]} />
18
-
19
-
<main>{children}</main>
15
+
<ContentLayout>
16
+
<div>
17
+
<main>{children}</main>
18
+
</div>
20
19
21
20
<WithMetaBar />
22
-
</ArticleLayout>
21
+
</ContentLayout>
23
22
24
23
<WithFooter />
25
24
</>
+4
-4
layouts/New/Home.tsx
+4
-4
layouts/New/Home.tsx
···
9
9
<>
10
10
<WithNavBar />
11
11
12
-
<main className={styles.homeLayout}>
13
-
<div className={styles.hexagonBackdrop} />
12
+
<div className={styles.homeLayout}>
13
+
<div className="glowingBackdrop" />
14
14
15
-
{children}
16
-
</main>
15
+
<main>{children}</main>
16
+
</div>
17
17
18
18
<WithFooter />
19
19
</>
+2
-2
layouts/New/Learn.tsx
+2
-2
layouts/New/Learn.tsx
···
1
1
import type { FC, PropsWithChildren } from 'react';
2
2
3
3
import WithBreadcrumbs from '@/components/withBreadcrumbs';
4
-
import WithCrossLinks from '@/components/withCrossLinks';
5
4
import WithMetaBar from '@/components/withMetaBar';
6
5
import WithNavBar from '@/components/withNavBar';
7
6
import WithProgressionSidebar from '@/components/withProgressionSidebar';
7
+
import WithSidebarCrossLinks from '@/components/withSidebarCrossLinks';
8
8
import ArticleLayout from '@/layouts/New/Article';
9
9
10
10
const LearnLayout: FC<PropsWithChildren> = ({ children }) => (
···
17
17
<main>
18
18
{children}
19
19
20
-
<WithCrossLinks navKey="learn" />
20
+
<WithSidebarCrossLinks navKey="learn" />
21
21
</main>
22
22
23
23
<WithMetaBar />
+60
layouts/New/Post.tsx
+60
layouts/New/Post.tsx
···
1
+
import type { FC, PropsWithChildren } from 'react';
2
+
3
+
import AvatarGroup from '@/components/Common/AvatarGroup';
4
+
import Preview from '@/components/Common/Preview';
5
+
import WithBlogCrossLinks from '@/components/withBlogCrossLinks';
6
+
import WithFooter from '@/components/withFooter';
7
+
import WithMetaBar from '@/components/withMetaBar';
8
+
import WithNavBar from '@/components/withNavBar';
9
+
import { useClientContext } from '@/hooks/react-server';
10
+
import ContentLayout from '@/layouts/New/Content';
11
+
import {
12
+
mapAuthorToCardAuthors,
13
+
mapBlogCategoryToPreviewType,
14
+
} from '@/util/blogUtils';
15
+
16
+
import styles from './layouts.module.css';
17
+
18
+
const PostLayout: FC<PropsWithChildren> = ({ children }) => {
19
+
const { frontmatter } = useClientContext();
20
+
21
+
const authors = mapAuthorToCardAuthors(frontmatter.author);
22
+
const type = mapBlogCategoryToPreviewType(frontmatter.category);
23
+
24
+
return (
25
+
<>
26
+
<WithNavBar />
27
+
28
+
<ContentLayout>
29
+
<div className={styles.postLayout}>
30
+
<main>
31
+
<h1>{frontmatter.title}</h1>
32
+
33
+
<section>
34
+
<AvatarGroup
35
+
avatars={authors.map(author => ({
36
+
alt: author.fullName,
37
+
src: author.src,
38
+
}))}
39
+
/>
40
+
41
+
<p>{authors.map(author => author.fullName).join(', ')}</p>
42
+
</section>
43
+
44
+
<Preview title={frontmatter.title!} type={type} />
45
+
46
+
{children}
47
+
48
+
<WithBlogCrossLinks />
49
+
</main>
50
+
</div>
51
+
52
+
<WithMetaBar />
53
+
</ContentLayout>
54
+
55
+
<WithFooter />
56
+
</>
57
+
);
58
+
};
59
+
60
+
export default PostLayout;
+134
-90
layouts/New/layouts.module.css
+134
-90
layouts/New/layouts.module.css
···
1
1
.baseLayout {
2
2
@apply grid
3
-
h-screen
4
-
w-screen
3
+
size-full
5
4
grid-cols-[1fr]
6
5
grid-rows-[auto_1fr_auto];
7
6
}
···
25
24
}
26
25
27
26
> *:nth-child(2) {
28
-
@apply flex
29
-
w-full
30
-
flex-col
31
-
items-start
32
-
gap-6
33
-
self-stretch
34
-
overflow-y-auto
27
+
@apply overflow-y-auto
35
28
overflow-x-hidden
36
29
bg-gradient-subtle
37
30
p-12
···
66
59
}
67
60
68
61
.homeLayout {
69
-
@apply mx-auto
70
-
flex
62
+
@apply flex
71
63
w-full
72
-
flex-col
73
64
items-center
74
-
gap-8
75
-
self-stretch
65
+
justify-center
76
66
px-4
77
67
py-14
78
-
md:w-auto
79
-
md:flex-row
80
-
md:gap-14
81
68
md:px-14
82
-
md:py-0
83
-
lg:gap-28
84
69
lg:px-28;
85
70
86
-
.hexagonBackdrop {
87
-
@apply absolute
88
-
left-0
89
-
-z-10
90
-
h-full
91
-
w-full
92
-
bg-[url('/static/images/patterns/hexagon-grid.svg')]
93
-
bg-center
94
-
bg-no-repeat
95
-
opacity-50
96
-
md:opacity-100;
71
+
main {
72
+
@apply items-center
73
+
justify-center
74
+
gap-8
75
+
md:flex-row
76
+
md:gap-14
77
+
xl:gap-28
78
+
2xl:gap-32;
97
79
98
-
&::after {
99
-
@apply absolute
100
-
inset-0
101
-
m-auto
102
-
aspect-square
103
-
w-[300px]
104
-
rounded-full
105
-
bg-green-300
106
-
blur-[120px]
107
-
content-['']
108
-
dark:bg-green-700;
109
-
}
110
-
}
80
+
section {
81
+
&:nth-of-type(1) {
82
+
@apply flex
83
+
max-w-[500px]
84
+
flex-[1_0]
85
+
flex-col
86
+
gap-8;
111
87
112
-
section {
113
-
&:nth-of-type(1) {
114
-
@apply flex
115
-
max-w-md
116
-
flex-[1_0_0]
117
-
flex-col
118
-
items-start
119
-
gap-8;
88
+
> div {
89
+
@apply flex
90
+
max-w-[400px]
91
+
flex-col
92
+
gap-4;
120
93
121
-
h1 {
122
-
@apply bg-gradient-subtle-gray
123
-
bg-clip-text
124
-
text-4xl
125
-
-tracking-[0.045rem]
126
-
[-webkit-text-fill-color:transparent]
127
-
md:text-5xl
128
-
md:-tracking-[0.06rem]
129
-
dark:bg-gradient-subtle-white;
130
-
}
94
+
p {
95
+
@apply text-base
96
+
md:text-lg;
97
+
}
131
98
132
-
p {
133
-
@apply max-w-[400px]
134
-
text-base
135
-
text-neutral-900
136
-
md:text-lg
137
-
dark:text-white;
99
+
small {
100
+
@apply text-center
101
+
text-sm
102
+
text-neutral-800
103
+
xs:text-xs
104
+
dark:text-neutral-400;
105
+
}
106
+
}
138
107
}
139
108
140
-
> div {
141
-
&:nth-of-type(1) {
142
-
@apply flex
143
-
flex-col
144
-
gap-4;
109
+
&:nth-of-type(2) {
110
+
@apply flex
111
+
max-w-md
112
+
flex-[1_1]
113
+
flex-col
114
+
items-center
115
+
gap-4
116
+
md:max-w-2xl
117
+
lg:max-w-3xl;
118
+
119
+
> div {
120
+
@apply w-fit;
121
+
122
+
div[data-state='active'] a {
123
+
@apply border-none
124
+
bg-neutral-900
125
+
px-3
126
+
py-1.5
127
+
text-sm
128
+
font-medium;
129
+
130
+
&:hover {
131
+
@apply bg-neutral-800;
132
+
}
133
+
}
145
134
}
146
135
147
-
&:nth-of-type(2) {
148
-
@apply flex
149
-
w-full
150
-
flex-col
151
-
gap-2
152
-
xl:flex-row;
136
+
> p {
137
+
@apply text-center
138
+
text-sm
139
+
text-neutral-800
140
+
dark:text-neutral-200;
153
141
}
154
142
}
155
143
}
144
+
}
145
+
}
156
146
157
-
&:nth-of-type(2) {
147
+
.blogLayout {
148
+
@apply flex
149
+
w-full
150
+
justify-center
151
+
bg-gradient-subtle
152
+
xs:bg-none
153
+
dark:bg-gradient-subtle-dark
154
+
xs:dark:bg-none;
155
+
156
+
main {
157
+
@apply max-w-8xl
158
+
gap-4
159
+
px-4
160
+
py-12
161
+
md:px-14
162
+
lg:px-28;
163
+
164
+
p {
165
+
@apply text-lg
166
+
font-medium
167
+
text-neutral-800
168
+
dark:text-neutral-200;
169
+
}
170
+
}
171
+
}
172
+
173
+
.contentLayout {
174
+
@apply grid
175
+
w-full
176
+
max-w-8xl
177
+
grid-rows-[1fr]
178
+
sm:grid-cols-[1fr_theme(spacing.52)]
179
+
xl:grid-cols-[1fr_theme(spacing.80)]
180
+
xs:m-0
181
+
xs:block;
182
+
183
+
> *:nth-child(1) {
184
+
@apply flex
185
+
w-full
186
+
justify-center
187
+
bg-gradient-subtle
188
+
px-4
189
+
py-14
190
+
md:px-14
191
+
lg:px-28
192
+
xs:bg-none
193
+
xs:pb-4
194
+
dark:bg-gradient-subtle-dark
195
+
xs:dark:bg-none;
196
+
197
+
main {
198
+
@apply max-w-[660px]
199
+
gap-4;
200
+
}
201
+
}
202
+
}
203
+
204
+
.postLayout {
205
+
main {
206
+
> section {
158
207
@apply flex
159
-
max-w-md
160
-
flex-col
161
-
content-center
208
+
flex-row
162
209
items-center
163
-
gap-4
164
-
md:max-w-2xl;
210
+
gap-4;
211
+
}
165
212
166
-
p {
167
-
@apply text-center
168
-
text-sm
169
-
text-neutral-800
170
-
dark:text-neutral-200;
171
-
}
213
+
> div:nth-of-type(1) {
214
+
@apply mb-4
215
+
mt-2;
172
216
}
173
217
}
174
218
}
+10
-4
next-data/blogData.ts
+10
-4
next-data/blogData.ts
···
4
4
NEXT_DATA_URL,
5
5
VERCEL_ENV,
6
6
} from '@/next.constants.mjs';
7
-
import type { BlogDataRSC } from '@/types';
7
+
import type { BlogPostsRSC } from '@/types';
8
8
9
-
const getBlogData = (category: string): Promise<BlogDataRSC> => {
9
+
const getBlogData = (cat: string, page?: number): Promise<BlogPostsRSC> => {
10
10
// When we're using Static Exports the Next.js Server is not running (during build-time)
11
11
// hence the self-ingestion APIs will not be available. In this case we want to load
12
12
// the data directly within the current thread, which will anyways be loaded only once
13
13
// We use lazy-imports to prevent `provideBlogData` from executing on import
14
14
if (ENABLE_STATIC_EXPORT || (!IS_DEVELOPMENT && !VERCEL_ENV)) {
15
15
return import('@/next-data/providers/blogData').then(
16
-
({ default: provideBlogData }) => provideBlogData(category)
16
+
({ provideBlogPosts, providePaginatedBlogPosts }) =>
17
+
page ? providePaginatedBlogPosts(cat, page) : provideBlogPosts(cat)
17
18
);
18
19
}
19
20
21
+
const fetchURL = page
22
+
? // Provides a conditional fetch URL based on the given function parameters
23
+
`${NEXT_DATA_URL}blog-data/${cat}/${page}`
24
+
: `${NEXT_DATA_URL}blog-data/${cat}/0`;
25
+
20
26
// When we're on RSC with Server capabilities we prefer using Next.js Data Fetching
21
27
// as this will load cached data from the server instead of generating data on the fly
22
28
// this is extremely useful for ISR and SSG as it will not generate this data on every request
23
-
return fetch(`${NEXT_DATA_URL}blog-data/${category}`).then(r => r.json());
29
+
return fetch(fetchURL).then(r => r.json());
24
30
};
25
31
26
32
export default getBlogData;
+18
-25
next-data/generators/blogData.mjs
+18
-25
next-data/generators/blogData.mjs
···
12
12
const blogPath = join(process.cwd(), 'pages/en/blog');
13
13
14
14
/**
15
-
* This contains the metadata of all available blog categories and
16
-
* available pagination entries (years)
17
-
*
18
-
* @type {{ pagination: Set<number>; categories: Set<string>}}
15
+
* This contains the metadata of all available blog categories
19
16
*/
20
-
const blogMetadata = { pagination: new Set(), categories: new Set() };
17
+
const blogCategories = new Set(['all']);
21
18
22
19
/**
23
20
* This method parses the source (raw) Markdown content into Frontmatter
···
34
31
category = 'uncategorized',
35
32
} = graymatter(source).data;
36
33
37
-
// we add the year to the pagination set
38
-
blogMetadata.pagination.add(new Date(date).getUTCFullYear());
34
+
// We also use publishing years as categories for the blog
35
+
const publishYear = new Date(date).getUTCFullYear();
36
+
37
+
// Provides a full list of categories for the Blog Post which consists of
38
+
// all = (all blog posts), publish year and the actual blog category
39
+
const categories = [category, `year-${publishYear}`, 'all'];
40
+
41
+
// we add the year to the categories set
42
+
blogCategories.add(`year-${publishYear}`);
39
43
40
44
// we add the category to the categories set
41
-
blogMetadata.categories.add(category);
45
+
blogCategories.add(category);
42
46
43
47
// this is the url used for the blog post it based on the category and filename
44
48
const slug = `/blog/${category}/${basename(filename, extname(filename))}`;
45
49
46
-
return { title, author, date: new Date(date), category, slug };
50
+
return { title, author, date: new Date(date), categories, slug };
47
51
};
48
52
49
53
/**
···
53
57
* @return {Promise<import('../../types').BlogData>}
54
58
*/
55
59
const generateBlogData = async () => {
56
-
// we retrieve all the filenames of all blog posts
60
+
// We retrieve the full pathnames of all Blog Posts to read each file individually
57
61
const filenames = await getMarkdownFiles(process.cwd(), 'pages/en/blog', [
58
62
'**/index.md',
59
-
'**/pagination.md',
60
63
]);
61
64
62
65
return new Promise(resolve => {
63
-
const blogPosts = [];
66
+
const posts = [];
64
67
const rawFrontmatter = [];
65
68
66
69
filenames.forEach(filename => {
···
95
98
// This allows us to only read the frontmatter part of each file
96
99
// and optimise the read-process as we have thousands of markdown files
97
100
_readLine.on('close', () => {
98
-
const frontmatter = getFrontMatter(
99
-
filename,
100
-
rawFrontmatter[filename][1]
101
-
);
102
-
103
-
blogPosts.push(frontmatter);
101
+
posts.push(getFrontMatter(filename, rawFrontmatter[filename][1]));
104
102
105
-
// Once we finish reading all fles
106
-
if (blogPosts.length === filenames.length) {
107
-
resolve({
108
-
pagination: [...blogMetadata.pagination].sort(),
109
-
categories: [...blogMetadata.categories].sort(),
110
-
posts: blogPosts.sort((a, b) => b.date - a.date),
111
-
});
103
+
if (posts.length === filenames.length) {
104
+
resolve({ categories: [...blogCategories], posts });
112
105
}
113
106
});
114
107
});
+30
-32
next-data/generators/websiteFeeds.mjs
+30
-32
next-data/generators/websiteFeeds.mjs
···
13
13
* This method generates RSS website feeds based on the current website configuration
14
14
* and the current blog data that is available
15
15
*
16
-
* @param {Promise<import('../../types').BlogDataRSC>} blogData
16
+
* @param {import('../../types').BlogPostsRSC} blogData
17
17
*/
18
-
const generateWebsiteFeeds = blogData => {
19
-
return blogData.then(({ posts }) => {
20
-
/**
21
-
* This generates all the Website RSS Feeds that are used for the website
22
-
*
23
-
* @type {[string, Feed][]}
24
-
*/
25
-
const websiteFeeds = siteConfig.rssFeeds.map(
26
-
({ category, title, description, file }) => {
27
-
const feed = new Feed({
28
-
id: file,
29
-
title: title,
30
-
language: 'en',
31
-
link: `${canonicalUrl}/feed/${file}`,
32
-
description: description,
33
-
});
18
+
const generateWebsiteFeeds = ({ posts }) => {
19
+
/**
20
+
* This generates all the Website RSS Feeds that are used for the website
21
+
*
22
+
* @type {[string, Feed][]}
23
+
*/
24
+
const websiteFeeds = siteConfig.rssFeeds.map(
25
+
({ category, title, description, file }) => {
26
+
const feed = new Feed({
27
+
id: file,
28
+
title: title,
29
+
language: 'en',
30
+
link: `${canonicalUrl}/feed/${file}`,
31
+
description: description,
32
+
});
34
33
35
-
const blogFeedEntries = posts
36
-
.filter(post => !category || post.category === category)
37
-
.map(post => ({
38
-
id: post.slug,
39
-
title: post.title,
40
-
author: post.author,
41
-
date: new Date(post.date),
42
-
link: `${canonicalUrl}${post.slug}`,
43
-
}));
34
+
const blogFeedEntries = posts
35
+
.filter(post => post.categories.includes(category))
36
+
.map(post => ({
37
+
id: post.slug,
38
+
title: post.title,
39
+
author: post.author,
40
+
date: new Date(post.date),
41
+
link: `${canonicalUrl}${post.slug}`,
42
+
}));
44
43
45
-
blogFeedEntries.forEach(entry => feed.addItem(entry));
44
+
blogFeedEntries.forEach(entry => feed.addItem(entry));
46
45
47
-
return [file, feed];
48
-
}
49
-
);
46
+
return [file, feed];
47
+
}
48
+
);
50
49
51
-
return new Map(websiteFeeds);
52
-
});
50
+
return new Map(websiteFeeds);
53
51
};
54
52
55
53
export default generateWebsiteFeeds;
+54
-33
next-data/providers/blogData.ts
+54
-33
next-data/providers/blogData.ts
···
1
1
import { cache } from 'react';
2
2
3
3
import generateBlogData from '@/next-data/generators/blogData.mjs';
4
-
import type { BlogDataRSC } from '@/types';
4
+
import { BLOG_POSTS_PER_PAGE } from '@/next.constants.mjs';
5
+
import type { BlogPostsRSC } from '@/types';
5
6
6
-
const blogData = generateBlogData();
7
+
const { categories, posts } = await generateBlogData();
7
8
8
-
const provideBlogData = cache(
9
-
async (category?: string): Promise<BlogDataRSC> => {
10
-
return blogData.then(({ posts, categories, pagination }) => {
11
-
const meta = { categories, pagination };
9
+
export const provideBlogCategories = cache(() => categories);
12
10
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
-
}
11
+
export const provideBlogPosts = cache((category: string): BlogPostsRSC => {
12
+
const categoryPosts = posts
13
+
.filter(post => post.categories.includes(category))
14
+
.sort((a, b) => b.date.getTime() - a.date.getTime());
20
15
21
-
if (category && category.startsWith('year-')) {
22
-
const paramYear = Number(category.replace('year-', ''));
16
+
// Total amount of possible pages given the amount of blog posts
17
+
const total = categoryPosts.length / BLOG_POSTS_PER_PAGE;
23
18
24
-
const isEqualYear = (date: string) =>
25
-
new Date(date).getFullYear() === paramYear;
19
+
return {
20
+
posts: categoryPosts,
21
+
pagination: {
22
+
prev: null,
23
+
next: null,
24
+
// In case the division results on a remainder we need
25
+
// to have an extra page containing the remainder entries
26
+
pages: Math.floor(total % 1 === 0 ? total : total + 1),
27
+
total: categoryPosts.length,
28
+
},
29
+
};
30
+
});
26
31
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
-
}
32
+
export const providePaginatedBlogPosts = cache(
33
+
(category: string, page: number): BlogPostsRSC => {
34
+
const { posts, pagination } = provideBlogPosts(category);
35
+
36
+
// This autocorrects if invalid numbers are given to only allow
37
+
// actual valid numbers to be provided
38
+
const actualPage = page < 1 ? 1 : page;
36
39
37
-
if (category && !categories.includes(category)) {
38
-
return { posts: [], pagination: { next: null, prev: null }, meta };
39
-
}
40
+
// If the page is within the allowed range then we calculate
41
+
// the pagination of Blog Posts for a given current page "page"
42
+
if (actualPage <= pagination.pages) {
43
+
return {
44
+
posts: posts.slice(
45
+
BLOG_POSTS_PER_PAGE * (actualPage - 1),
46
+
BLOG_POSTS_PER_PAGE * actualPage
47
+
),
48
+
pagination: {
49
+
prev: actualPage > 1 ? actualPage - 1 : null,
50
+
next: actualPage < pagination.pages ? actualPage + 1 : null,
51
+
pages: pagination.pages,
52
+
total: posts.length,
53
+
},
54
+
};
55
+
}
40
56
41
-
return { posts, pagination: { next: null, prev: null }, meta };
42
-
});
57
+
return {
58
+
posts: [],
59
+
pagination: {
60
+
prev: pagination.total,
61
+
next: null,
62
+
pages: pagination.pages,
63
+
total: posts.length,
64
+
},
65
+
};
43
66
}
44
67
);
45
-
46
-
export default provideBlogData;
+2
-2
next-data/providers/releaseData.ts
+2
-2
next-data/providers/releaseData.ts
···
2
2
3
3
import generateReleaseData from '@/next-data/generators/releaseData.mjs';
4
4
5
-
const releaseData = generateReleaseData();
5
+
const releaseData = await generateReleaseData();
6
6
7
-
const provideReleaseData = cache(async () => releaseData);
7
+
const provideReleaseData = cache(() => releaseData);
8
8
9
9
export default provideReleaseData;
+7
-9
next-data/providers/websiteFeeds.ts
+7
-9
next-data/providers/websiteFeeds.ts
···
1
1
import { cache } from 'react';
2
2
3
3
import generateWebsiteFeeds from '@/next-data/generators/websiteFeeds.mjs';
4
-
import provideBlogData from '@/next-data/providers/blogData';
4
+
import { provideBlogPosts } from '@/next-data/providers/blogData';
5
5
6
-
const websiteFeeds = generateWebsiteFeeds(provideBlogData());
6
+
const websiteFeeds = await generateWebsiteFeeds(provideBlogPosts('all'));
7
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
-
}
8
+
const provideWebsiteFeeds = cache((feed: string) => {
9
+
if (feed.includes('.xml') && websiteFeeds.has(feed)) {
10
+
return websiteFeeds.get(feed)?.rss2();
11
+
}
13
12
14
-
return undefined;
15
-
});
13
+
return undefined;
16
14
});
17
15
18
16
export default provideWebsiteFeeds;
+4
-2
next.config.mjs
+4
-2
next.config.mjs
···
9
9
BASE_PATH,
10
10
ENABLE_STATIC_EXPORT,
11
11
ENABLE_WEBSITE_REDESIGN,
12
+
} from './next.constants.mjs';
13
+
import { redirects, rewrites } from './next.rewrites.mjs';
14
+
import {
12
15
SENTRY_DSN,
13
16
SENTRY_ENABLE,
14
17
SENTRY_EXTENSIONS,
15
18
SENTRY_TUNNEL,
16
-
} from './next.constants.mjs';
17
-
import { redirects, rewrites } from './next.rewrites.mjs';
19
+
} from './sentry.constants.mjs';
18
20
19
21
/** @type {import('next').NextConfig} */
20
22
const nextConfig = {
+14
-58
next.constants.mjs
+14
-58
next.constants.mjs
···
1
1
'use strict';
2
2
3
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
4
* This is used to verify if the current Website is running on a Development Environment
12
5
*/
13
6
export const IS_DEVELOPMENT = process.env.NODE_ENV === 'development';
···
115
108
*/
116
109
export const MD_EXTENSION_REGEX = /((\/)?(index))?\.mdx?$/i;
117
110
111
+
/**
112
+
* This defines how many blog posts each pagination page should have
113
+
*
114
+
* @todo: update the value when moving to website redesign
115
+
*/
116
+
export const BLOG_POSTS_PER_PAGE = ENABLE_WEBSITE_REDESIGN ? 6 : 20;
117
+
118
+
/**
119
+
* The `localStorage` key to store the theme choice of `next-themes`
120
+
*
121
+
* This is what allows us to store user preference for theming
122
+
*/
123
+
export const THEME_STORAGE_KEY = 'theme';
124
+
118
125
/***
119
126
* This is a list of all external links that are used on website sitemap.
120
127
* @see https://github.com/nodejs/nodejs.org/issues/5813 for more context
···
128
135
'https://trademark-list.openjsf.org/',
129
136
'https://www.linuxfoundation.org/cookies',
130
137
];
131
-
132
-
/**
133
-
* The `localStorage` key to store the theme choice of `next-themes`
134
-
*
135
-
* This is what allows us to store user preference for theming
136
-
*/
137
-
export const THEME_STORAGE_KEY = 'theme';
138
-
139
-
/**
140
-
* This is the Sentry DSN for the Node.js Website Project
141
-
*/
142
-
export const SENTRY_DSN =
143
-
'https://02884d0745aecaadf5f780278fe5fe70@o4506191161786368.ingest.sentry.io/4506191307735040';
144
-
145
-
/**
146
-
* This states if Sentry should be enabled and bundled within our App
147
-
*
148
-
* We enable sentry by default if we're om development mode or deployed
149
-
* on Vercel (either production or preview branches)
150
-
*/
151
-
export const SENTRY_ENABLE = IS_DEVELOPMENT || !!VERCEL_ENV;
152
-
153
-
/**
154
-
* This configures the sampling rate for Sentry
155
-
*
156
-
* We always want to capture 100% on Vercel Preview Branches
157
-
* and not when it's on Production Mode (nodejs.org)
158
-
*/
159
-
export const SENTRY_CAPTURE_RATE =
160
-
SENTRY_ENABLE && VERCEL_ENV && BASE_URL !== 'https://nodejs.org' ? 1.0 : 0.01;
161
-
162
-
/**
163
-
* Provides the Route for Sentry's Server-Side Tunnel
164
-
*
165
-
* This is a `@sentry/nextjs` specific feature
166
-
*/
167
-
export const SENTRY_TUNNEL = (components = '') =>
168
-
SENTRY_ENABLE ? `/monitoring${components}` : undefined;
169
-
170
-
/**
171
-
* This configures which Sentry features to tree-shake/remove from the Sentry bundle
172
-
*
173
-
* @see https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/tree-shaking/
174
-
*/
175
-
export const SENTRY_EXTENSIONS = {
176
-
__SENTRY_DEBUG__: false,
177
-
__SENTRY_TRACING__: false,
178
-
__RRWEB_EXCLUDE_IFRAME__: true,
179
-
__RRWEB_EXCLUDE_SHADOW_DOM__: true,
180
-
__SENTRY_EXCLUDE_REPLAY_WORKER__: true,
181
-
};
+29
-49
next.dynamic.constants.mjs
+29
-49
next.dynamic.constants.mjs
···
1
1
'use strict';
2
2
3
-
import { BASE_PATH, BASE_URL, CURRENT_YEAR } from './next.constants.mjs';
3
+
import {
4
+
provideBlogCategories,
5
+
provideBlogPosts,
6
+
} from './next-data/providers/blogData';
7
+
import { BASE_PATH, BASE_URL } from './next.constants.mjs';
4
8
import { siteConfig } from './next.json.mjs';
5
9
import { defaultLocale } from './next.locales.mjs';
6
10
···
10
14
*
11
15
* @type {((route: import('./types').RouteSegment) => boolean)[]} A list of Ignored Routes by Regular Expressions
12
16
*/
13
-
export const STATIC_ROUTES_IGNORES = [
14
-
// Ignore the 404 route on Static Generation
17
+
export const IGNORED_ROUTES = [
18
+
// This is used to ignore the 404 route for the static generation (/404)
15
19
({ pathname }) => pathname === '404',
16
-
// This is used to ignore is used to ignore all blog routes except for the English language
20
+
// This is used to ignore all blog routes except for the English language
17
21
({ locale, pathname }) =>
18
22
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
23
];
47
24
48
25
/**
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`.
26
+
* This constant is used to create static routes on-the-fly that do not have a file-system
27
+
* counterpart route. This is useful for providing routes with matching Layout Names
28
+
* but that do not have Markdown content and a matching file for the route
55
29
*
56
-
* @type {string[]} A list of all the Dynamic Routes that are generated by the Website
57
-
* @deprecated remove with website redesign
30
+
* @type {Map<string, import('./types').Layouts>} A Map of pathname and Layout Name
58
31
*/
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
-
];
32
+
export const DYNAMIC_ROUTES = new Map([
33
+
// Provides Routes for all Blog Categories
34
+
...provideBlogCategories().map(c => [`blog/${c}`, 'blog-category.hbs']),
35
+
// Provides Routes for all Blog Categories w/ Pagination
36
+
...provideBlogCategories()
37
+
// retrieves the amount of pages for each blog category
38
+
.map(c => [c, provideBlogPosts(c).pagination.pages])
39
+
// creates a numeric array for each page and define a pathname for
40
+
// each page for a category (i.e. blog/all/page/1)
41
+
.map(([c, t]) => [...Array(t).keys()].map(p => `blog/${c}/page/${p + 1}`))
42
+
// creates a tuple of each pathname and layout for the route
43
+
.map(paths => paths.map(path => [path, 'blog-category.hbs']))
44
+
// flattens the array since we have a .map inside another .map
45
+
.flat(),
46
+
]);
67
47
68
48
/**
69
49
* This is the default Next.js Page Metadata for all pages
70
50
*
71
51
* @type {import('next').Metadata}
72
52
*/
73
-
export const DEFAULT_METADATA = {
53
+
export const PAGE_METADATA = {
74
54
metadataBase: new URL(`${BASE_URL}${BASE_PATH}`),
75
55
title: siteConfig.title,
76
56
description: siteConfig.description,
···
100
80
*
101
81
* @return {import('next').Viewport}
102
82
*/
103
-
export const DEFAULT_VIEWPORT = {
83
+
export const PAGE_VIEWPORT = {
104
84
themeColor: siteConfig.accentColor,
105
85
width: 'device-width',
106
86
initialScale: 1,
+11
-30
next.dynamic.mjs
+11
-30
next.dynamic.mjs
···
8
8
import { cache } from 'react';
9
9
import { VFile } from 'vfile';
10
10
11
+
import { BASE_URL, BASE_PATH, IS_DEVELOPMENT } from './next.constants.mjs';
11
12
import {
12
-
MD_EXTENSION_REGEX,
13
-
BASE_URL,
14
-
BASE_PATH,
15
-
IS_DEVELOPMENT,
16
-
} from './next.constants.mjs';
17
-
import {
18
-
DYNAMIC_ROUTES_IGNORES,
19
-
DYNAMIC_ROUTES_REWRITES,
20
-
STATIC_ROUTES_IGNORES,
21
-
DYNAMIC_GENERATED_ROUTES,
22
-
DEFAULT_METADATA,
13
+
IGNORED_ROUTES,
14
+
DYNAMIC_ROUTES,
15
+
PAGE_METADATA,
23
16
} from './next.dynamic.constants.mjs';
24
17
import { getMarkdownFiles } from './next.helpers.mjs';
25
18
import { siteConfig } from './next.json.mjs';
···
32
25
// This is a small utility that allows us to quickly separate locale from the remaning pathname
33
26
const getPathname = (path = []) => path.join('/');
34
27
35
-
// This tests if the current pathname matches any expression that belongs
36
-
// to the list of ignored routes and if it does we return `true` to indicate that
37
-
const shouldIgnoreRoute = pathname =>
38
-
pathname.length > 0 && DYNAMIC_ROUTES_IGNORES.some(e => e.test(pathname));
39
-
40
-
// This tests if the current pathname matches any sort of rewrite rule
41
-
// and if it does we return a the replacement expression for the pathname
42
-
const getRouteRewrite = pathname =>
43
-
(pathname.length > 0 &&
44
-
DYNAMIC_ROUTES_REWRITES.find(([e]) => e.test(pathname))) ||
45
-
[];
46
-
47
28
// This maps a pathname into an actual route object that can be used
48
29
// we use a platform-specific separator to split the pathname
49
30
// since we're using filepaths here and not URL paths
···
79
60
);
80
61
81
62
websitePages.forEach(filename => {
82
-
let pathname = filename.replace(MD_EXTENSION_REGEX, '');
63
+
// This Regular Expression is used to remove the `index.md(x)` suffix
64
+
// of a name and to remove the `.md(x)` extensions of a filename.
65
+
let pathname = filename.replace(/((\/)?(index))?\.mdx?$/i, '');
83
66
84
67
if (pathname.length > 1 && pathname.endsWith(sep)) {
85
68
pathname = pathname.substring(0, pathname.length - 1);
···
100
83
*/
101
84
const getRoutesByLanguage = async (locale = defaultLocale.code) => {
102
85
const shouldIgnoreStaticRoute = pathname =>
103
-
STATIC_ROUTES_IGNORES.every(e => !e({ pathname, locale }));
86
+
IGNORED_ROUTES.every(e => !e({ pathname, locale }));
104
87
105
88
return [...pathnameToFilename.keys()]
106
89
.filter(shouldIgnoreStaticRoute)
107
-
.concat(DYNAMIC_GENERATED_ROUTES);
90
+
.concat([...DYNAMIC_ROUTES.keys()]);
108
91
};
109
92
110
93
/**
···
207
190
* @returns {import('next').Metadata}
208
191
*/
209
192
const _getPageMetadata = async (locale = defaultLocale.code, path = '') => {
210
-
const pageMetadata = { ...DEFAULT_METADATA };
193
+
const pageMetadata = { ...PAGE_METADATA };
211
194
212
195
const { source = '' } = await getMarkdownFile(locale, path);
213
196
214
197
const { data } = matter(source);
215
198
216
199
pageMetadata.title = data.title
217
-
? `${data.title} | ${siteConfig.title}`
200
+
? `${siteConfig.title} — ${data.title}`
218
201
: siteConfig.title;
219
202
220
203
pageMetadata.twitter.title = pageMetadata.title;
···
246
229
247
230
return {
248
231
mapPathToRoute,
249
-
shouldIgnoreRoute,
250
232
getPathname,
251
-
getRouteRewrite,
252
233
getRoutesByLanguage,
253
234
getMDXContent,
254
235
getMarkdownFile,
+2
-1
next.mdx.compiler.mjs
+2
-1
next.mdx.compiler.mjs
···
19
19
* @returns {Promise<{
20
20
* MDXContent: import('mdx/types').MDXContent;
21
21
* headings: import('@vcarl/remark-headings').Heading[];
22
-
* frontmatter: Record<string, any>, readingTime: import('reading-time').ReadTimeResults
22
+
* frontmatter: Record<string, any>;
23
+
* readingTime: import('reading-time').ReadTimeResults;
23
24
* }>}
24
25
*/
25
26
export async function compileMDX(source, fileExtension) {
+50
-96
next.mdx.shiki.mjs
+50
-96
next.mdx.shiki.mjs
···
48
48
* @return {boolean} - True when it is a valid code element, false otherwise.
49
49
*/
50
50
function isCodeBlock(node) {
51
-
return node?.tagName === 'pre' && node?.children[0].tagName === 'code';
52
-
}
53
-
54
-
/**
55
-
* Retrieves a list indicating the starting, and ending indexes of sequential
56
-
* code elements.
57
-
*
58
-
* @param {Node} tree - The current MDX resolved content.
59
-
*
60
-
* @return {{start: number, end: number}[]} - The list containing every range of
61
-
* sequential code elements.
62
-
*/
63
-
function getCodeTabsRange(tree) {
64
-
const rangeMap = {};
65
-
let start = null;
66
-
67
-
visit(tree, 'element', (node, index, parent) => {
68
-
// Adding 2 since there is one text node between every element
69
-
const next = index + 2;
70
-
71
-
if (isCodeBlock(node) && isCodeBlock(parent?.children[next])) {
72
-
start ??= index;
73
-
rangeMap[start] = next;
74
-
75
-
// Prevent visiting the code block children
76
-
return SKIP;
77
-
}
78
-
79
-
// End of sequential code elements, reset the start for the next range
80
-
start = null;
81
-
});
82
-
83
-
return Object.entries(rangeMap).map(([start, end]) => ({
84
-
start: Number(start),
85
-
end: Number(end),
86
-
}));
51
+
return Boolean(
52
+
node?.tagName === 'pre' && node?.children[0].tagName === 'code'
53
+
);
87
54
}
88
55
89
56
export default function rehypeShikiji() {
90
57
return async function (tree) {
91
-
// Retrieve all sequential code boxes to transform
92
-
const ranges = getCodeTabsRange(tree);
93
-
94
-
if (ranges.length > 0) {
95
-
// Make a mutable clone without reference
96
-
const children = [...tree.children];
97
-
98
-
for (const range of ranges) {
99
-
// Simple tree containing the sequential code boxes among text nodes
100
-
const slicedTree = {
101
-
type: 'root',
102
-
children: tree.children.slice(range.start, range.end + 1),
103
-
};
104
-
105
-
const languages = [];
106
-
const displayNames = [];
107
-
const codeTabsChildren = [];
58
+
visit(tree, 'element', (_, index, parent) => {
59
+
const languages = [];
60
+
const displayNames = [];
61
+
const codeTabsChildren = [];
108
62
109
-
let defaultTab = '0';
63
+
let defaultTab = '0';
64
+
let currentIndex = index;
110
65
111
-
visit(slicedTree, 'element', node => {
112
-
const codeElement = node.children[0];
66
+
while (isCodeBlock(parent?.children[currentIndex])) {
67
+
const codeElement = parent?.children[currentIndex].children[0];
113
68
114
-
const displayName = getMetaParameter(
115
-
codeElement.data?.meta,
116
-
'displayName'
117
-
);
69
+
const displayName = getMetaParameter(
70
+
codeElement.data?.meta,
71
+
'displayName'
72
+
);
118
73
119
-
// We should get the language name from the class name
120
-
if (codeElement.properties.className?.length) {
121
-
const className = codeElement.properties.className.join(' ');
122
-
const matches = className.match(/language-(?<language>.*)/);
74
+
// We should get the language name from the class name
75
+
if (codeElement.properties.className?.length) {
76
+
const className = codeElement.properties.className.join(' ');
77
+
const matches = className.match(/language-(?<language>.*)/);
123
78
124
-
languages.push(matches?.groups.language ?? 'text');
125
-
}
79
+
languages.push(matches?.groups.language ?? 'text');
80
+
}
126
81
127
-
// Map the display names of each variant for the CodeTab
128
-
displayNames.push(displayName?.replaceAll('|', '') ?? '');
129
-
codeTabsChildren.push(node);
82
+
// Map the display names of each variant for the CodeTab
83
+
displayNames.push(displayName?.replaceAll('|', '') ?? '');
130
84
131
-
// If `active="true"` is provided in a CodeBox
132
-
// then the default selected entry of the CodeTabs will be the desired entry
133
-
const specificActive = getMetaParameter(
134
-
codeElement.data?.meta,
135
-
'active'
136
-
);
85
+
codeTabsChildren.push(parent?.children[currentIndex]);
137
86
138
-
if (specificActive === 'true') {
139
-
defaultTab = String(codeTabsChildren.length - 1);
140
-
}
87
+
// If `active="true"` is provided in a CodeBox
88
+
// then the default selected entry of the CodeTabs will be the desired entry
89
+
const specificActive = getMetaParameter(
90
+
codeElement.data?.meta,
91
+
'active'
92
+
);
141
93
142
-
// Prevent visiting the code block children
143
-
return SKIP;
144
-
});
94
+
if (specificActive === 'true') {
95
+
defaultTab = String(codeTabsChildren.length - 1);
96
+
}
145
97
146
-
// Each iteration reduces the `children` length, so it needs to be
147
-
// accounted in the following operations
148
-
const lengthOffset = tree.children.length - children.length;
149
-
const compensatedRange = {
150
-
start: range.start - lengthOffset,
151
-
end: range.end - lengthOffset,
152
-
};
98
+
const nextNode = parent?.children[currentIndex + 1];
153
99
154
-
const deleteCount = compensatedRange.end - compensatedRange.start + 1;
100
+
// If the CodeBoxes are on the root tree the next Element will be
101
+
// an empty text element so we should skip it
102
+
currentIndex += nextNode && nextNode?.type === 'text' ? 2 : 1;
103
+
}
155
104
156
-
// Replace the sequential code boxes with a code tabs element
157
-
children.splice(compensatedRange.start, deleteCount, {
105
+
if (codeTabsChildren.length >= 2) {
106
+
const codeTabElement = {
158
107
type: 'element',
159
108
tagName: 'CodeTabs',
160
109
children: codeTabsChildren,
···
163
112
displayNames: displayNames.join('|'),
164
113
defaultTab,
165
114
},
166
-
});
167
-
}
115
+
};
168
116
169
-
// Update the tree with the transformed children
170
-
Object.assign(tree, { children: children });
171
-
}
117
+
// This removes all the original Code Elements and adds a new CodeTab Element
118
+
// at the original start of the first Code Element
119
+
parent.children.splice(index, currentIndex, codeTabElement);
120
+
121
+
// Prevent visiting the code block children and for the next N Elements
122
+
// since all of them belong to this CodeTabs Element
123
+
return [SKIP, currentIndex];
124
+
}
125
+
});
172
126
173
127
visit(tree, 'element', (node, index, parent) => {
174
128
// We only want to process <pre>...</pre> elements
+4
-3
next.mdx.use.mjs
+4
-3
next.mdx.use.mjs
···
3
3
import Blockquote from './components/Common/Blockquote';
4
4
import Button from './components/Common/Button';
5
5
import DownloadButton from './components/Downloads/DownloadButton';
6
+
import DownloadLink from './components/Downloads/DownloadLink';
6
7
import DownloadReleasesTable from './components/Downloads/DownloadReleasesTable';
7
8
import HomeDownloadButton from './components/Home/HomeDownloadButton';
8
9
import Link from './components/Link';
···
33
34
CodeTabs: MDXCodeTabs,
34
35
// Renders a Download Button
35
36
DownloadButton: DownloadButton,
37
+
// Renders a Download Link
38
+
DownloadLink: DownloadLink,
36
39
// Renders a Button Component for `button` tags
37
40
Button: Button,
38
41
};
···
51
54
? Blockquote
52
55
: ({ children }) => <div className="highlight-box">{children}</div>,
53
56
// Renders a CodeBox Component for `pre` tags
54
-
pre: ({ children, ...props }) => (
55
-
<MDXCodeBox {...props}>{children}</MDXCodeBox>
56
-
),
57
+
pre: MDXCodeBox,
57
58
};
+9
package-lock.json
+9
package-lock.json
···
19
19
"@radix-ui/react-toast": "^1.1.5",
20
20
"@savvywombat/tailwindcss-grid-areas": "~3.1.0",
21
21
"@sentry/nextjs": "~7.86.0",
22
+
"@tailwindcss/container-queries": "~0.1.1",
22
23
"@types/node": "20.10.6",
23
24
"@vcarl/remark-headings": "~0.1.0",
24
25
"@vercel/analytics": "~1.1.1",
···
6957
6958
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz",
6958
6959
"integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==",
6959
6960
"dev": true
6961
+
},
6962
+
"node_modules/@tailwindcss/container-queries": {
6963
+
"version": "0.1.1",
6964
+
"resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz",
6965
+
"integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==",
6966
+
"peerDependencies": {
6967
+
"tailwindcss": ">=3.2.0"
6968
+
}
6960
6969
},
6961
6970
"node_modules/@testing-library/dom": {
6962
6971
"version": "9.3.3",
+1
-5
package.json
+1
-5
package.json
···
50
50
"@radix-ui/react-toast": "^1.1.5",
51
51
"@savvywombat/tailwindcss-grid-areas": "~3.1.0",
52
52
"@sentry/nextjs": "~7.86.0",
53
+
"@tailwindcss/container-queries": "~0.1.1",
53
54
"@types/node": "20.10.6",
54
55
"@vcarl/remark-headings": "~0.1.0",
55
56
"@vercel/analytics": "~1.1.1",
···
123
124
"stylelint-order": "6.0.4",
124
125
"stylelint-selector-bem-pattern": "3.0.1",
125
126
"user-agent-data-types": "0.4.2"
126
-
},
127
-
"overrides": {
128
-
"stylelint-selector-bem-pattern": {
129
-
"stylelint": "16.1.0"
130
-
}
131
127
}
132
128
}
-4
pages/en/blog/advisory-board/index.md
-4
pages/en/blog/advisory-board/index.md
-4
pages/en/blog/announcements/index.md
-4
pages/en/blog/announcements/index.md
-4
pages/en/blog/community/index.md
-4
pages/en/blog/community/index.md
-4
pages/en/blog/feature/index.md
-4
pages/en/blog/feature/index.md
-4
pages/en/blog/module/index.md
-4
pages/en/blog/module/index.md
-54
pages/en/blog/nodejs-road-ahead.md
-54
pages/en/blog/nodejs-road-ahead.md
···
1
-
---
2
-
date: '2014-01-16T23:00:00.000Z'
3
-
title: Node.js and the Road Ahead
4
-
layout: blog-post.hbs
5
-
author: Timothy J Fontaine
6
-
---
7
-
8
-
As the new project lead for Node.js I am excited for our future, and want to
9
-
give you an update on where we are.
10
-
11
-
One of Node's major goals is to provide a small core, one that provides the
12
-
right amount of surface area for consumers to achieve and innovate, without
13
-
Node itself getting in the way. That ethos is alive and well, we're going to
14
-
continue to provide a small, simple, and stable set of APIs that facilitate the
15
-
amazing uses the community finds for Node. We're going to keep providing
16
-
backward compatible APIs, so code you write today will continue to work on
17
-
future versions of Node. And of course, performance tuning and bug fixing will
18
-
always be an important part of every release cycle.
19
-
20
-
The release of Node v0.12 is imminent, and a lot of significant work has gone
21
-
into this release. There's streams3, a better keep alive agent for http, the vm
22
-
module is now based on contextify, and significant performance work done in
23
-
core features (Buffers, TLS, streams). We have a few APIs that are still being
24
-
ironed out before we can feature freeze and branch (execSync, AsyncListeners,
25
-
user definable instrumentation). We are definitely in the home stretch.
26
-
27
-
But Node is far from done. In the short term there will be new releases of v8
28
-
that we'll need to track, as well as integrating the new ABI stable C module
29
-
interface. There are interesting language features that we can use to extend
30
-
Node APIs (extend not replace). We need to write more tooling, we need to
31
-
expose more interfaces to further enable innovation. We can explore
32
-
functionality to embed Node in your existing project.
33
-
34
-
The list can go on and on. Yet, Node is larger than the software itself. Node
35
-
is also the community, the businesses, the ecosystems, and their related
36
-
events. With that in mind there are things we can work to improve.
37
-
38
-
The core team will be improving its procedures such that we can quickly and
39
-
efficiently communicate with you. We want to provide high quality and timely
40
-
responses to issues, describe our development roadmap, as well as provide our
41
-
progress during each release cycle. We know you're interested in our plans for
42
-
Node, and it's important we're able to provide that information. Communication
43
-
should be bidirectional: we want to continue to receive feedback about how
44
-
you're using Node, and what your pain points are.
45
-
46
-
After the release of v0.12 we will facilitate the community to contribute and
47
-
curate content for nodejs.org. Allowing the community to continue to invest in
48
-
Node will ensure nodejs.org is an excellent starting point and the primary
49
-
resource for tutorials, documentation, and materials regarding Node. We have an
50
-
awesome and engaged community, and they're paramount to our success.
51
-
52
-
I'm excited for Node's future, to see new and interesting use cases, and to
53
-
continue to help businesses scale and innovate with Node. We have a lot we can
54
-
accomplish together, and I look forward to seeing those results.
-5
pages/en/blog/pagination.md
-5
pages/en/blog/pagination.md
-4
pages/en/blog/release/index.md
-4
pages/en/blog/release/index.md
+1
-1
pages/en/blog/release/v0.6.3.md
+1
-1
pages/en/blog/release/v0.6.3.md
···
8
8
9
9
2011.11.25, Version 0.6.3 (stable)
10
10
11
-
- #2083 Land NPM in Node. It is included in packages/installers and installed on `make install`.
11
+
- #2083 Land npm in Node. It is included in packages/installers and installed on `make install`.
12
12
- #2076 Add logos to windows installer.
13
13
- #1711 Correctly handle http requests without headers. (Ben Noordhuis, Felix Geisendörfer)
14
14
- TLS: expose more openssl SSL context options and constants. (Ben Noordhuis)
-4
pages/en/blog/uncategorized/index.md
-4
pages/en/blog/uncategorized/index.md
-4
pages/en/blog/vulnerability/index.md
-4
pages/en/blog/vulnerability/index.md
-4
pages/en/blog/weekly-updates/index.md
-4
pages/en/blog/weekly-updates/index.md
+1
-1
pages/en/blog/weekly-updates/weekly-update.2015-04-17.md
+1
-1
pages/en/blog/weekly-updates/weekly-update.2015-04-17.md
···
31
31
## Community Updates
32
32
33
33
- Difference between io.js and The Node Foundation [iojs/io.js#1416](https://github.com/nodejs/node/issues/1416).
34
-
- NPM launches private modules and npm inc [raises](http://techcrunch.com/2015/04/14/popular-javascript-package-manager-npm-raises-8m-launches-private-modules/).
34
+
- npm launches private modules and npm inc [raises](http://techcrunch.com/2015/04/14/popular-javascript-package-manager-npm-raises-8m-launches-private-modules/).
35
35
- Thoughts of Node.js Foundation on [Medium](https://medium.com/@programmer/thoughts-on-node-foundation-abcf86c72786).
36
36
- io.js v1.8.0 crypto performance on [io.js wiki](https://github.com/nodejs/node/wiki/Crypto-Performance-Notes-for-OpenSSL-1.0.2a-on-iojs-v1.8.0).
37
37
- io.js mention on [Oracle's blog](https://blogs.oracle.com/java-platform-group/entry/node_js_and_io_js).
+1
-1
pages/en/blog/weekly-updates/weekly-update.2015-04-24.md
+1
-1
pages/en/blog/weekly-updates/weekly-update.2015-04-24.md
···
37
37
38
38
- Fedor Indutny opened discussion about removing TLS `newSession` and `resumeSession` event. [iojs/io.js#1462](https://github.com/nodejs/node/issues/1462)
39
39
- Proposal to change the C HTTP parser JS HTTP parser [here](https://github.com/nodejs/node/pull/1457)
40
-
- NPM founder talks about io.js at [InfoWorld](http://www.infoworld.com/article/2910594/node-js/npm-founder-foresees-merger-node-js-io-js.html)
40
+
- npm founder talks about io.js at [InfoWorld](http://www.infoworld.com/article/2910594/node-js/npm-founder-foresees-merger-node-js-io-js.html)
41
41
- Proposal to add mikeal, mscdex, shigeki as new TC members. [iojs/io.js#1483](https://github.com/nodejs/node/issues/1483#issuecomment-95128140)
42
42
43
43
## Upcoming Events
+1
-1
pages/en/download/package-manager.md
+1
-1
pages/en/download/package-manager.md
+2
-2
pages/en/learn/getting-started/an-introduction-to-the-npm-package-manager.md
+2
-2
pages/en/learn/getting-started/an-introduction-to-the-npm-package-manager.md
···
1
1
---
2
-
title: An introduction to the NPM package manager
2
+
title: An introduction to the npm package manager
3
3
layout: learn.hbs
4
4
authors: flaviocopes, MylesBorins, LaRuaNa, jgb-solutions, amiller-gh, ahmadawais
5
5
---
6
6
7
-
# An introduction to the NPM package manager
7
+
# An introduction to the npm package manager
8
8
9
9
## Introduction to npm
10
10
-1
pages/en/new-design/.gitkeep
-1
pages/en/new-design/.gitkeep
···
1
-
!.gitignore
+82
-32
pages/en/new-design/index.mdx
+82
-32
pages/en/new-design/index.mdx
···
1
1
---
2
+
title: Run JavaScript Everywhere
2
3
layout: home.hbs
3
4
---
4
5
···
6
7
<WithBadge section="index" />
7
8
8
9
<div>
9
-
# Run JavaScript Everywhere
10
+
<h1 className="special">Run JavaScript Everywhere</h1>
10
11
11
-
Node.js is a free, open-sourced, cross-platform JavaScript run-time
12
+
Node.js is a free, open-source, cross-platform JavaScript runtime
12
13
environment that lets developers write command line tools and server-side
13
14
scripts outside of a browser.
14
15
15
16
</div>
17
+
16
18
<div>
17
19
<WithNodeRelease status={['Active LTS', 'Maintenance LTS']}>
18
20
{({ release }) => (
19
-
<DownloadButton release={release}>Download Node.js (LTS)</DownloadButton>
21
+
<>
22
+
<DownloadButton release={release}>Download Node.js (LTS)</DownloadButton>
23
+
<small>
24
+
Downloads Node.js <b>{release.versionWithPrefix}</b> with long-term support.
25
+
Node.js can also be installed via <a href="/download/package-manager">package managers</a>.
26
+
</small>
27
+
</>
28
+
)}
29
+
</WithNodeRelease>
30
+
<WithNodeRelease status="Current">
31
+
{({ release }) => (
32
+
<small>
33
+
Want new features sooner?
34
+
Get <b>Node.js <DownloadLink release={release}>{release.versionWithPrefix}</DownloadLink></b> instead.
35
+
</small>
20
36
)}
21
37
</WithNodeRelease>
22
-
23
-
<Button kind="secondary" href="/learn">Get Started</Button>
24
-
25
38
</div>
26
39
</section>
27
40
28
41
<section>
29
-
<CodeTabs
30
-
languages="bash|bash"
31
-
displayNames="macOS / Linux (nvm)|Windows (Chocolatey)"
32
-
linkUrl="/download/package-manager"
33
-
linkText="More Options"
34
-
>
35
-
```bash
36
-
# Install Node Version Manager (NVM)
37
-
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
42
+
<div>
43
+
```js displayName="Create an HTTP Server"
44
+
import { createServer } from 'node:http';
45
+
46
+
const server = createServer((req, res) => {
47
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
48
+
res.end('Hello World!\n');
49
+
});
38
50
39
-
# Install Node.js
40
-
nvm install --lts
51
+
// starts a simple http server locally on port 3000
52
+
server.listen(3000, '127.0.0.1', () => {
53
+
console.log('Listening on 127.0.0.1:3000');
54
+
});
55
+
```
41
56
42
-
# Check that Node is installed
43
-
node -v
57
+
```js displayName="Write Tests"
58
+
import assert from 'node:assert';
59
+
import test from 'node:test';
44
60
45
-
# Check your NPM version
46
-
npm -v
61
+
test('that 1 is equal 1', () => {
62
+
assert.strictEqual(1, 1);
63
+
});
64
+
65
+
test('that throws as 1 is not equal 2', () => {
66
+
// throws an exception because 1!=2
67
+
assert.strictEqual(1, 2);
68
+
});
47
69
```
48
70
49
-
```bash
50
-
# Install Chocolatey
51
-
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
71
+
```js displayName="Read and Hash a File"
72
+
import { createHash } from 'node:crypto';
73
+
import { readFile } from 'node:fs/promises';
74
+
75
+
const hasher = createHash('sha1');
76
+
const fileContent = await readFile('./package.json');
77
+
78
+
hasher.setEncoding('hex');
79
+
hasher.write(fileContent);
80
+
hasher.end();
81
+
82
+
const fileHash = hasher.read();
83
+
```
84
+
85
+
```js displayName="Read Streams"
86
+
import { createReadStream, createWriteStream } from 'node:fs';
87
+
88
+
const res = await fetch('https://nodejs.org/dist/index.json');
89
+
const json = await res.json(); // yields a json object
90
+
91
+
const readableStream = createReadStream('./package.json');
92
+
const writableStream = createWriteStream('./package2.json');
52
93
53
-
# Install Node.js
54
-
choco install nodejs-lts
94
+
readableStream.setEncoding('utf8');
55
95
56
-
# Check that Node is installed
57
-
node -v
96
+
readableStream.on('data', chunk => writableStream.write(chunk));
97
+
```
58
98
59
-
# Check your NPM version
60
-
npm -v
99
+
```js displayName="Work with Workers"
100
+
// file containing main thread (main.mjs)
101
+
import { Worker } from 'node:worker_threads';
102
+
103
+
const w = new Worker('./worker.mjs', { workerData: 'hello!' });
104
+
w.on('message', data => console.log('processed data:', data));
105
+
106
+
// file containing worker (worker.mjs)
107
+
import { parentPort, workerData } from 'node:worker_threads';
108
+
109
+
// do some complex computational workload
110
+
parentPort.postMessage(btoa(workerData));
61
111
```
62
112
63
-
</CodeTabs>
64
-
Copy and paste this snippet to install Node.js LTS via a Package Manager
113
+
</div>
114
+
Learn more what Node.js is able to offer with our [Learning materials](/learn).
65
115
</section>
+13
-20
providers/matterProvider.tsx
+13
-20
providers/matterProvider.tsx
···
1
1
'use client';
2
2
3
-
import type { Heading } from '@vcarl/remark-headings';
4
3
import { createContext } from 'react';
5
4
import type { FC, PropsWithChildren } from 'react';
6
-
import type { ReadTimeResults } from 'reading-time';
7
5
8
-
import type { LegacyFrontMatter } from '@/types';
6
+
import type { ClientSharedServerContext } from '@/types';
7
+
import { assignClientContext } from '@/util/assignClientContext';
9
8
10
-
type MatterContext = {
11
-
frontmatter: LegacyFrontMatter;
12
-
pathname: string;
13
-
headings: Array<Heading>;
14
-
readingTime: ReadTimeResults;
15
-
filename: string;
16
-
};
9
+
export const MatterContext = createContext<ClientSharedServerContext>(
10
+
assignClientContext({})
11
+
);
17
12
18
-
export const MatterContext = createContext<MatterContext>({
19
-
frontmatter: {},
20
-
pathname: '',
21
-
headings: [],
22
-
readingTime: { text: '', minutes: 0, time: 0, words: 0 },
23
-
filename: '',
24
-
});
25
-
26
-
type MatterProviderProps = PropsWithChildren<MatterContext>;
13
+
type MatterProviderProps = PropsWithChildren<
14
+
Partial<ClientSharedServerContext>
15
+
>;
27
16
28
17
export const MatterProvider: FC<MatterProviderProps> = ({
29
18
children,
30
19
...data
31
-
}) => <MatterContext.Provider value={data}>{children}</MatterContext.Provider>;
20
+
}) => (
21
+
<MatterContext.Provider value={assignClientContext(data)}>
22
+
{children}
23
+
</MatterContext.Provider>
24
+
);
+1
-1
sentry.client.config.ts
+1
-1
sentry.client.config.ts
+45
sentry.constants.mjs
+45
sentry.constants.mjs
···
1
+
import { BASE_URL, IS_DEVELOPMENT, VERCEL_ENV } from './next.constants.mjs';
2
+
3
+
/**
4
+
* This is the Sentry DSN for the Node.js Website Project
5
+
*/
6
+
export const SENTRY_DSN =
7
+
'https://02884d0745aecaadf5f780278fe5fe70@o4506191161786368.ingest.sentry.io/4506191307735040';
8
+
9
+
/**
10
+
* This states if Sentry should be enabled and bundled within our App
11
+
*
12
+
* We enable sentry by default if we're om development mode or deployed
13
+
* on Vercel (either production or preview branches)
14
+
*/
15
+
export const SENTRY_ENABLE = IS_DEVELOPMENT || !!VERCEL_ENV;
16
+
17
+
/**
18
+
* This configures the sampling rate for Sentry
19
+
*
20
+
* We always want to capture 100% on Vercel Preview Branches
21
+
* and not when it's on Production Mode (nodejs.org)
22
+
*/
23
+
export const SENTRY_CAPTURE_RATE =
24
+
SENTRY_ENABLE && VERCEL_ENV && BASE_URL !== 'https://nodejs.org' ? 1.0 : 0.01;
25
+
26
+
/**
27
+
* Provides the Route for Sentry's Server-Side Tunnel
28
+
*
29
+
* This is a `@sentry/nextjs` specific feature
30
+
*/
31
+
export const SENTRY_TUNNEL = (components = '') =>
32
+
SENTRY_ENABLE ? `/monitoring${components}` : undefined;
33
+
34
+
/**
35
+
* This configures which Sentry features to tree-shake/remove from the Sentry bundle
36
+
*
37
+
* @see https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/tree-shaking/
38
+
*/
39
+
export const SENTRY_EXTENSIONS = {
40
+
__SENTRY_DEBUG__: false,
41
+
__SENTRY_TRACING__: false,
42
+
__RRWEB_EXCLUDE_IFRAME__: true,
43
+
__RRWEB_EXCLUDE_SHADOW_DOM__: true,
44
+
__SENTRY_EXCLUDE_REPLAY_WORKER__: true,
45
+
};
+1
-1
sentry.edge.config.ts
+1
-1
sentry.edge.config.ts
+1
-1
sentry.server.config.ts
+1
-1
sentry.server.config.ts
+2
-1
site.json
+2
-1
site.json
+37
styles/new/effects.css
+37
styles/new/effects.css
···
1
+
h1.special {
2
+
@apply bg-gradient-subtle-gray
3
+
bg-clip-text
4
+
text-4xl
5
+
leading-[3rem]
6
+
-tracking-[0.045rem]
7
+
[-webkit-text-fill-color:transparent]
8
+
md:text-5xl
9
+
md:leading-[4rem]
10
+
md:-tracking-[0.06rem]
11
+
dark:bg-gradient-subtle-white;
12
+
}
13
+
14
+
div.glowingBackdrop {
15
+
@apply absolute
16
+
left-0
17
+
-z-10
18
+
size-full
19
+
bg-[url('/static/images/patterns/hexagon-grid.svg')]
20
+
bg-center
21
+
bg-no-repeat
22
+
opacity-50
23
+
md:opacity-100;
24
+
25
+
&::after {
26
+
@apply absolute
27
+
inset-0
28
+
m-auto
29
+
aspect-square
30
+
w-[300px]
31
+
rounded-full
32
+
bg-green-300
33
+
blur-[120px]
34
+
content-['']
35
+
dark:bg-green-700;
36
+
}
37
+
}
+1
styles/new/index.css
+1
styles/new/index.css
+16
styles/new/markdown.css
+16
styles/new/markdown.css
···
1
1
main {
2
+
@apply flex
3
+
w-full
4
+
flex-col
5
+
gap-6;
6
+
2
7
hr {
3
8
@apply w-full
4
9
border-t
···
53
58
dark:text-white;
54
59
}
55
60
61
+
p {
62
+
@apply text-neutral-900
63
+
dark:text-white;
64
+
}
65
+
56
66
a {
57
67
@apply text-green-600
68
+
xs:underline
58
69
dark:text-green-400;
59
70
60
71
&:hover {
61
72
@apply text-green-900
62
73
dark:text-green-300;
74
+
}
75
+
76
+
&:has(code) {
77
+
@apply xs:decoration-neutral-800
78
+
dark:xs:decoration-neutral-200;
63
79
}
64
80
}
65
81
+6
-3
tailwind.config.ts
+6
-3
tailwind.config.ts
···
117
117
'ibm-plex-mono': ['var(--font-ibm-plex-mono)'],
118
118
},
119
119
extend: {
120
-
screens: { xs: { max: '670px' } },
120
+
screens: { xs: { max: '670px', min: '0px' } },
121
121
backgroundImage: {
122
122
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
123
123
'gradient-subtle':
···
129
129
'gradient-subtle-white':
130
130
'linear-gradient(180deg, theme(colors.white) 0%, theme(colors.white / 80%) 100%)',
131
131
'gradient-glow-backdrop':
132
-
'radial-gradient(8em circle at calc(100% - 40px) 10px, theme(colors.green.500), transparent 30%)',
132
+
'radial-gradient(8em circle at calc(50%) 10px, theme(colors.green.500), transparent 30%)',
133
133
},
134
134
boxShadow: {
135
135
xs: '0px 1px 2px 0px theme(colors.shadow / 5%)',
···
141
141
},
142
142
},
143
143
darkMode: ['class', '[data-theme="dark"]'],
144
-
plugins: [require('@savvywombat/tailwindcss-grid-areas')],
144
+
plugins: [
145
+
require('@savvywombat/tailwindcss-grid-areas'),
146
+
require('@tailwindcss/container-queries'),
147
+
],
145
148
} satisfies Config;
+13
-12
types/blog.ts
+13
-12
types/blog.ts
···
1
+
export type BlogPreviewType = 'announcements' | 'release' | 'vulnerability';
2
+
1
3
export interface BlogPost {
2
4
title: string;
3
5
author: string;
4
-
date: string;
5
-
category: string;
6
+
date: Date;
7
+
categories: Array<string>;
6
8
slug: string;
7
9
}
8
10
9
11
export interface BlogData {
10
12
posts: Array<BlogPost>;
11
-
pagination: Array<number>;
12
13
categories: Array<string>;
13
14
}
14
15
15
-
export interface BlogDataRSC {
16
+
export interface BlogPagination {
17
+
next: number | null;
18
+
prev: number | null;
19
+
pages: number;
20
+
total: number;
21
+
}
22
+
23
+
export interface BlogPostsRSC {
16
24
posts: Array<BlogPost>;
17
-
pagination: {
18
-
next: number | null;
19
-
prev: number | null;
20
-
};
21
-
meta: {
22
-
categories: Array<string>;
23
-
pagination: Array<number>;
24
-
};
25
+
pagination: BlogPagination;
25
26
}
+1
-1
types/features.ts
+1
-1
types/features.ts
+3
-1
types/layouts.ts
+3
-1
types/layouts.ts
+17
util/assignClientContext.ts
+17
util/assignClientContext.ts
···
1
+
import type { ClientSharedServerContext } from '@/types';
2
+
3
+
export const assignClientContext = <T extends ClientSharedServerContext>(
4
+
props: Partial<T>
5
+
) =>
6
+
({
7
+
frontmatter: props.frontmatter ?? {},
8
+
pathname: props.pathname ?? '',
9
+
headings: props.headings ?? [],
10
+
readingTime: props.readingTime ?? {
11
+
text: '',
12
+
minutes: 0,
13
+
time: 0,
14
+
words: 0,
15
+
},
16
+
filename: props.filename ?? '',
17
+
}) as T;
+24
util/blogUtils.ts
+24
util/blogUtils.ts
···
1
+
import type { BlogPreviewType } from '@/types';
2
+
3
+
export const mapBlogCategoryToPreviewType = (type: string): BlogPreviewType => {
4
+
switch (type) {
5
+
case 'announcements':
6
+
case 'release':
7
+
case 'vulnerability':
8
+
return type;
9
+
default:
10
+
return 'announcements';
11
+
}
12
+
};
13
+
14
+
// @todo: we should check about the future of GitHub avatars
15
+
// and mapping them to the respective users
16
+
// @see https://github.com/nodejs/nodejs.dev/blob/main/src/data/blog/authors.yaml
17
+
export const mapAuthorToCardAuthors = (author: string) => {
18
+
const authors = author.split(/, | and |;| by /i);
19
+
20
+
return authors.map(fullName => ({
21
+
fullName,
22
+
src: `https://ui-avatars.com/api/?name=${fullName}`,
23
+
}));
24
+
};