The Node.js® Website

feat: new home page, blog engine, blog layouts and routing engine (#6215)

Co-authored-by: Caner Akdas <canerakdas@gmail.com>

authored by Claudio W Caner Akdas and committed by GitHub c811ac78 2c878257

Changed files
+1530 -1101
.github
app
[locale]
[[...path]]
feed
[feed]
next-data
blog-data
[category]
release-data
components
i18n
locales
layouts
next-data
pages
en
blog
advisory-board
announcements
community
feature
module
npm
release
uncategorized
video
vulnerability
weekly-updates
download
learn
new-design
providers
styles
types
util
+2 -2
.github/workflows/build.yml
··· 91 node-version-file: '.nvmrc' 92 cache: 'npm' 93 94 - - name: Install NPM packages 95 - # We want to avoid NPM from running the Audit Step and Funding messages on a CI environment 96 # We also use `npm i` instead of `npm ci` so that the node_modules/.cache folder doesn't get deleted 97 # We also use `--omit=dev` to avoid installing devDependencies as we don't need them during the build step 98 run: npm i --no-audit --no-fund --userconfig=/dev/null --omit=dev
··· 91 node-version-file: '.nvmrc' 92 cache: 'npm' 93 94 + - name: Install npm packages 95 + # We want to avoid npm from running the Audit Step and Funding messages on a CI environment 96 # We also use `npm i` instead of `npm ci` so that the node_modules/.cache folder doesn't get deleted 97 # We also use `--omit=dev` to avoid installing devDependencies as we don't need them during the build step 98 run: npm i --no-audit --no-fund --userconfig=/dev/null --omit=dev
+4 -4
.github/workflows/lint-and-tests.yml
··· 111 node-version-file: '.nvmrc' 112 cache: 'npm' 113 114 - - name: Install NPM packages 115 - # We want to avoid NPM from running the Audit Step and Funding messages on a CI environment 116 # We also use `npm i` instead of `npm ci` so that the node_modules/.cache folder doesn't get deleted 117 run: npm i --no-audit --no-fund --ignore-scripts --userconfig=/dev/null 118 ··· 209 node-version-file: '.nvmrc' 210 cache: 'npm' 211 212 - - name: Install NPM packages 213 - # We want to avoid NPM from running the Audit Step and Funding messages on a CI environment 214 # We also use `npm i` instead of `npm ci` so that the node_modules/.cache folder doesn't get deleted 215 run: npm i --no-audit --no-fund --userconfig=/dev/null 216
··· 111 node-version-file: '.nvmrc' 112 cache: 'npm' 113 114 + - name: Install npm packages 115 + # We want to avoid npm from running the Audit Step and Funding messages on a CI environment 116 # We also use `npm i` instead of `npm ci` so that the node_modules/.cache folder doesn't get deleted 117 run: npm i --no-audit --no-fund --ignore-scripts --userconfig=/dev/null 118 ··· 209 node-version-file: '.nvmrc' 210 cache: 'npm' 211 212 + - name: Install npm packages 213 + # We want to avoid npm from running the Audit Step and Funding messages on a CI environment 214 # We also use `npm i` instead of `npm ci` so that the node_modules/.cache folder doesn't get deleted 215 run: npm i --no-audit --no-fund --userconfig=/dev/null 216
+2 -2
.github/workflows/translations-pr.yml
··· 100 node-version-file: '.nvmrc' 101 cache: 'npm' 102 103 - - name: Install NPM packages 104 - # We want to avoid NPM from running the Audit Step and Funding messages on a CI environment 105 # We also use `npm i` instead of `npm ci` so that the node_modules/.cache folder doesn't get deleted 106 run: npm i --no-audit --no-fund --ignore-scripts --userconfig=/dev/null 107
··· 100 node-version-file: '.nvmrc' 101 cache: 'npm' 102 103 + - name: Install npm packages 104 + # We want to avoid npm from running the Audit Step and Funding messages on a CI environment 105 # We also use `npm i` instead of `npm ci` so that the node_modules/.cache folder doesn't get deleted 106 run: npm i --no-audit --no-fund --ignore-scripts --userconfig=/dev/null 107
+1 -1
DEPENDENCY_PINNING.md
··· 1 ## Dependency Pinning 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. 4 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
··· 1 ## Dependency Pinning 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. 4 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
+44 -25
app/[locale]/[[...path]]/page.tsx
··· 7 import { MDXRenderer } from '@/components/mdxRenderer'; 8 import WithLayout from '@/components/withLayout'; 9 import { ENABLE_STATIC_EXPORT, VERCEL_REVALIDATE } from '@/next.constants.mjs'; 10 - import { DEFAULT_VIEWPORT } from '@/next.dynamic.constants.mjs'; 11 import { dynamicRouter } from '@/next.dynamic.mjs'; 12 import { availableLocaleCodes, defaultLocale } from '@/next.locales.mjs'; 13 import { MatterProvider } from '@/providers/matterProvider'; ··· 17 18 // This is the default Viewport Metadata 19 // @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function 20 - export const generateViewport = async () => ({ ...DEFAULT_VIEWPORT }); 21 22 // This generates each page's HTML Metadata 23 // @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata ··· 26 27 const pathname = dynamicRouter.getPathname(path); 28 29 - // Retrieves and rewriting rule if the pathname matches any rule 30 - const [, rewriteRule] = dynamicRouter.getRouteRewrite(pathname); 31 32 - return dynamicRouter.getPageMetadata( 33 - locale, 34 - rewriteRule ? rewriteRule(pathname) : pathname 35 ); 36 }; 37 ··· 40 export const generateStaticParams = async () => { 41 const paths: Array<DynamicStaticPaths> = []; 42 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) 54 ); 55 56 - paths.push(...mappedRoutesWithLocale); 57 } 58 59 return paths.sort(); ··· 76 // Configures the current Locale to be the given Locale of the Request 77 unstable_setRequestLocale(locale); 78 79 const pathname = dynamicRouter.getPathname(path); 80 81 - if (dynamicRouter.shouldIgnoreRoute(pathname)) { 82 - return notFound(); 83 - } 84 85 - // Retrieves and rewriting rule if the pathname matches any rule 86 - const [, rewriteRule] = dynamicRouter.getRouteRewrite(pathname); 87 88 // We retrieve the source of the Markdown file by doing an educated guess 89 // of what possible files could be the source of the page, since the extension 90 // context is lost from `getStaticProps` as a limitation of Next.js itself 91 const { source, filename } = await dynamicRouter.getMarkdownFile( 92 locale, 93 - rewriteRule ? rewriteRule(pathname) : pathname 94 ); 95 96 // Decorate the Locale and current Pathname to Sentry
··· 7 import { MDXRenderer } from '@/components/mdxRenderer'; 8 import WithLayout from '@/components/withLayout'; 9 import { ENABLE_STATIC_EXPORT, VERCEL_REVALIDATE } from '@/next.constants.mjs'; 10 + import { PAGE_VIEWPORT, DYNAMIC_ROUTES } from '@/next.dynamic.constants.mjs'; 11 import { dynamicRouter } from '@/next.dynamic.mjs'; 12 import { availableLocaleCodes, defaultLocale } from '@/next.locales.mjs'; 13 import { MatterProvider } from '@/providers/matterProvider'; ··· 17 18 // This is the default Viewport Metadata 19 // @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function 20 + export const generateViewport = async () => ({ ...PAGE_VIEWPORT }); 21 22 // This generates each page's HTML Metadata 23 // @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata ··· 26 27 const pathname = dynamicRouter.getPathname(path); 28 29 + return dynamicRouter.getPageMetadata(locale, pathname); 30 + }; 31 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) 38 ); 39 }; 40 ··· 43 export const generateStaticParams = async () => { 44 const paths: Array<DynamicStaticPaths> = []; 45 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) 51 ); 52 53 + paths.push(...allAvailableRoutes.flat()); 54 } 55 56 return paths.sort(); ··· 73 // Configures the current Locale to be the given Locale of the Request 74 unstable_setRequestLocale(locale); 75 76 + // Gets the current full pathname for a given path 77 const pathname = dynamicRouter.getPathname(path); 78 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; 82 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 + } 106 107 // We retrieve the source of the Markdown file by doing an educated guess 108 // of what possible files could be the source of the page, since the extension 109 // context is lost from `getStaticProps` as a limitation of Next.js itself 110 const { source, filename } = await dynamicRouter.getMarkdownFile( 111 locale, 112 + pathname 113 ); 114 115 // Decorate the Locale and current Pathname to Sentry
+1 -1
app/[locale]/feed/[feed]/route.ts
··· 15 // @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers 16 export const GET = async (_: Request, { params }: StaticParams) => { 17 // Generate the Feed for the given feed type (blog, releases, etc) 18 - const websiteFeed = await provideWebsiteFeeds(params.feed); 19 20 return new NextResponse(websiteFeed, { 21 headers: { 'Content-Type': 'application/xml' },
··· 15 // @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers 16 export const GET = async (_: Request, { params }: StaticParams) => { 17 // Generate the Feed for the given feed type (blog, releases, etc) 18 + const websiteFeed = provideWebsiteFeeds(params.feed); 19 20 return new NextResponse(websiteFeed, { 21 headers: { 'Content-Type': 'application/xml' },
+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
··· 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
··· 9 // for generating static data related to the Node.js Release Data 10 // @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers 11 export const GET = async () => { 12 - const releaseData = await provideReleaseData(); 13 14 return Response.json(releaseData); 15 };
··· 9 // for generating static data related to the Node.js Release Data 10 // @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers 11 export const GET = async () => { 12 + const releaseData = provideReleaseData(); 13 14 return Response.json(releaseData); 15 };
+11 -13
client-context.ts
··· 2 3 import type { ClientSharedServerContext } from '@/types'; 4 5 // This allows us to have Server-Side Context's of the shared "contextual" data 6 // which includes the frontmatter, the current pathname from the dynamic segments 7 // and the current headings of the current markdown context 8 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 - }; 16 17 return serverSharedContext; 18 }); 19 20 // This is used by the dynamic router to define on the request 21 // 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; 28 };
··· 2 3 import type { ClientSharedServerContext } from '@/types'; 4 5 + import { assignClientContext } from './util/assignClientContext'; 6 + 7 // This allows us to have Server-Side Context's of the shared "contextual" data 8 // which includes the frontmatter, the current pathname from the dynamic segments 9 // and the current headings of the current markdown context 10 export const getClientContext = cache(() => { 11 + const serverSharedContext = assignClientContext({}); 12 13 return serverSharedContext; 14 }); 15 16 // This is used by the dynamic router to define on the request 17 // the current set of information we use (shared) 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; 26 };
+2 -4
components/Common/AvatarGroup/Avatar/index.module.css
··· 1 .avatar { 2 @apply flex 3 - h-8 4 - w-8 5 items-center 6 justify-center 7 rounded-full ··· 18 19 .avatarRoot { 20 @apply -ml-2 21 - h-8 22 - w-8 23 flex-shrink-0 24 first:ml-0; 25 }
··· 1 .avatar { 2 @apply flex 3 + size-8 4 items-center 5 justify-center 6 rounded-full ··· 17 18 .avatarRoot { 19 @apply -ml-2 20 + size-8 21 flex-shrink-0 22 first:ml-0; 23 }
+6 -1
components/Common/AvatarGroup/Avatar/index.tsx
··· 10 11 const Avatar: FC<AvatarProps> = ({ src, alt }) => ( 12 <RadixAvatar.Root className={styles.avatarRoot}> 13 - <RadixAvatar.Image src={src} alt={alt} className={styles.avatar} /> 14 <RadixAvatar.Fallback delayMs={500} className={styles.avatar}> 15 {alt} 16 </RadixAvatar.Fallback>
··· 10 11 const Avatar: FC<AvatarProps> = ({ src, alt }) => ( 12 <RadixAvatar.Root className={styles.avatarRoot}> 13 + <RadixAvatar.Image 14 + loading="lazy" 15 + src={src} 16 + alt={alt} 17 + className={styles.avatar} 18 + /> 19 <RadixAvatar.Fallback delayMs={500} className={styles.avatar}> 20 {alt} 21 </RadixAvatar.Fallback>
+2
components/Common/AvatarGroup/index.tsx
··· 1 import classNames from 'classnames'; 2 import type { ComponentProps, FC } from 'react'; 3 import { useState, useMemo } from 'react';
··· 1 + 'use client'; 2 + 3 import classNames from 'classnames'; 4 import type { ComponentProps, FC } from 'react'; 5 import { useState, useMemo } from 'react';
+1 -2
components/Common/Badge/index.module.css
··· 11 font-medium; 12 13 .icon { 14 - @apply h-4 15 - w-4; 16 } 17 18 .badge {
··· 11 font-medium; 12 13 .icon { 14 + @apply size-4; 15 } 16 17 .badge {
+1 -2
components/Common/Banner/index.module.css
··· 22 } 23 24 svg { 25 - @apply h-4 26 - w-4 27 text-white/50; 28 } 29 }
··· 22 } 23 24 svg { 25 + @apply size-4 26 text-white/50; 27 } 28 }
+10 -11
components/Common/BlogPostCard/__tests__/index.test.mjs
··· 8 description = 'Blog post description', 9 authors = [], 10 date = new Date(), 11 }) { 12 render( 13 <BlogPostCard 14 title={title} 15 - type={type} 16 description={description} 17 authors={authors} 18 date={date} 19 /> 20 ); 21 ··· 33 it('Renders the title prop correctly', () => { 34 const { title } = renderBlogPostCard({}); 35 36 - // Title from Preview component 37 - expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent( 38 - title 39 - ); 40 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( 44 'aria-hidden', 45 'true' 46 ); ··· 53 }); 54 55 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' }, 59 ])( 60 'Renders "%label" text when passing it the type "%type"', 61 ({ label, type }) => {
··· 8 description = 'Blog post description', 9 authors = [], 10 date = new Date(), 11 + slug = '', 12 }) { 13 render( 14 <BlogPostCard 15 title={title} 16 + category={type} 17 description={description} 18 authors={authors} 19 date={date} 20 + slug={slug} 21 /> 22 ); 23 ··· 35 it('Renders the title prop correctly', () => { 36 const { title } = renderBlogPostCard({}); 37 38 + expect(screen.getAllByText(title).length).toBe(2); 39 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( 43 'aria-hidden', 44 'true' 45 ); ··· 52 }); 53 54 it.each([ 55 + { label: 'layouts.blog.categories.vulnerability', type: 'vulnerability' }, 56 + { label: 'layouts.blog.categories.announcements', type: 'announcements' }, 57 + { label: 'layouts.blog.categories.release', type: 'release' }, 58 ])( 59 'Renders "%label" text when passing it the type "%type"', 60 ({ label, type }) => {
+4 -10
components/Common/BlogPostCard/index.module.css
··· 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; 12 } 13 14 .subtitle { 15 @apply mb-2 16 text-xs 17 font-semibold 18 text-green-600 ··· 21 22 .title { 23 @apply mb-2 24 text-xl 25 font-semibold 26 text-neutral-900
··· 1 .container { 2 + @apply max-w-full; 3 } 4 5 .subtitle { 6 @apply mb-2 7 + mt-6 8 + inline-block 9 text-xs 10 font-semibold 11 text-green-600 ··· 14 15 .title { 16 @apply mb-2 17 + block 18 text-xl 19 font-semibold 20 text-neutral-900
+2 -1
components/Common/BlogPostCard/index.stories.tsx
··· 8 export const Default: Story = { 9 args: { 10 title: 'Node.js March 17th Infrastructure Incident Post-mortem', 11 - type: 'vulnerability', 12 description: 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 authors: [ ··· 17 src: 'https://avatars.githubusercontent.com/u/', 18 }, 19 ], 20 date: new Date('17 October 2023'), 21 }, 22 decorators: [
··· 8 export const Default: Story = { 9 args: { 10 title: 'Node.js March 17th Infrastructure Incident Post-mortem', 11 + category: 'vulnerability', 12 description: 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 authors: [ ··· 17 src: 'https://avatars.githubusercontent.com/u/', 18 }, 19 ], 20 + slug: '/blog/vulnerability/something', 21 date: new Date('17 October 2023'), 22 }, 23 decorators: [
+29 -30
components/Common/BlogPostCard/index.tsx
··· 1 import { useTranslations } from 'next-intl'; 2 - import { useMemo } from 'react'; 3 - import type { ComponentProps, FC } from 'react'; 4 5 import AvatarGroup from '@/components/Common/AvatarGroup'; 6 import Preview from '@/components/Common/Preview'; 7 import { Time } from '@/components/Common/Time'; 8 9 import styles from './index.module.css'; 10 11 - type Author = { 12 - fullName: string; 13 - src: string; 14 - }; 15 16 type BlogPostCardProps = { 17 - title: ComponentProps<typeof Preview>['title']; 18 - type: Required<ComponentProps<typeof Preview>>['type']; 19 - description: string; 20 authors: Array<Author>; 21 date: Date; 22 }; 23 24 const BlogPostCard: FC<BlogPostCardProps> = ({ 25 title, 26 - type, 27 description, 28 authors, 29 date, 30 }) => { 31 const t = useTranslations(); 32 33 - const avatars = useMemo( 34 - () => 35 - authors.map(({ fullName, src }) => ({ 36 - alt: fullName, 37 - src, 38 - toString: () => fullName, 39 - })), 40 - [authors] 41 - ); 42 43 return ( 44 <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}> 53 {title} 54 - </p> 55 - <p className={styles.description}>{description}</p> 56 <footer className={styles.footer}> 57 <AvatarGroup avatars={avatars} /> 58 <div className={styles.author}> 59 - <p>{avatars.join(', ')}</p> 60 61 <Time 62 date={date}
··· 1 import { useTranslations } from 'next-intl'; 2 + import type { FC } from 'react'; 3 4 import AvatarGroup from '@/components/Common/AvatarGroup'; 5 import Preview from '@/components/Common/Preview'; 6 import { Time } from '@/components/Common/Time'; 7 + import Link from '@/components/Link'; 8 + import { mapBlogCategoryToPreviewType } from '@/util/blogUtils'; 9 10 import styles from './index.module.css'; 11 12 + // @todo: this should probably be a global type? 13 + type Author = { fullName: string; src: string }; 14 15 type BlogPostCardProps = { 16 + title: string; 17 + category: string; 18 + description?: string; 19 authors: Array<Author>; 20 date: Date; 21 + slug: string; 22 }; 23 24 const BlogPostCard: FC<BlogPostCardProps> = ({ 25 title, 26 + slug, 27 + category, 28 description, 29 authors, 30 date, 31 }) => { 32 const t = useTranslations(); 33 34 + const avatars = authors.map(({ fullName, src }) => ({ alt: fullName, src })); 35 + 36 + const type = mapBlogCategoryToPreviewType(category); 37 38 return ( 39 <article className={styles.container}> 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}> 49 {title} 50 + </Link> 51 + 52 + {description && <p className={styles.description}>{description}</p>} 53 + 54 <footer className={styles.footer}> 55 <AvatarGroup avatars={avatars} /> 56 + 57 <div className={styles.author}> 58 + <p>{avatars.map(avatar => avatar.alt).join(', ')}</p> 59 60 <Time 61 date={date}
+1 -2
components/Common/Breadcrumbs/BreadcrumbHomeLink/index.module.css
··· 1 .icon { 2 - @apply h-4 3 - w-4; 4 }
··· 1 .icon { 2 + @apply size-4; 3 }
+1 -2
components/Common/Breadcrumbs/BreadcrumbItem/index.module.css
··· 22 } 23 24 .separator { 25 - @apply h-4 26 - w-4 27 flex-shrink-0 28 flex-grow 29 text-neutral-600
··· 22 } 23 24 .separator { 25 + @apply size-4 26 flex-shrink-0 27 flex-grow 28 text-neutral-600
+4
components/Common/Button/index.module.css
··· 69 70 &::before { 71 @apply absolute 72 right-0 73 top-0 74 -z-10 75 h-full 76 w-full 77 bg-gradient-glow-backdrop ··· 82 &::after { 83 @apply absolute 84 -top-px 85 right-0 86 h-px 87 w-2/5 88 bg-gradient-to-r
··· 69 70 &::before { 71 @apply absolute 72 + left-0 73 right-0 74 top-0 75 -z-10 76 + mx-auto 77 h-full 78 w-full 79 bg-gradient-glow-backdrop ··· 84 &::after { 85 @apply absolute 86 -top-px 87 + left-0 88 right-0 89 + mx-auto 90 h-px 91 w-2/5 92 bg-gradient-to-r
+1 -2
components/Common/CodeBox/index.module.css
··· 83 } 84 85 .icon { 86 - @apply h-4 87 - w-4; 88 }
··· 83 } 84 85 .icon { 86 + @apply size-4; 87 }
+40 -39
components/Common/CodeTabs/index.module.css
··· 1 - .root > [role='tabpanel'] > :first-child { 2 - @apply rounded-t-none; 3 - } 4 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; 15 16 - & [role='tablist'] > button { 17 - @apply border-b 18 - border-b-transparent 19 - px-1 20 - text-neutral-200; 21 22 - &[aria-selected='true'] { 23 - @apply border-b-green-400 24 - text-green-400; 25 } 26 - } 27 28 - .link { 29 - @apply hidden 30 - items-center 31 - gap-2 32 - text-center 33 - text-neutral-200 34 - lg:flex; 35 36 - & > .icon { 37 - @apply h-4 38 - w-4 39 - text-neutral-300; 40 - } 41 42 - &:is(:link, :visited) { 43 - &:hover { 44 - @apply text-neutral-400; 45 46 - & > .icon { 47 - @apply text-neutral-600; 48 } 49 } 50 }
··· 1 + .root { 2 + > [role='tabpanel'] > :first-child { 3 + @apply rounded-t-none; 4 + } 5 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; 16 17 + > button { 18 + @apply border-b 19 + border-b-transparent 20 + px-1 21 + text-neutral-200; 22 23 + &[data-state='active'] { 24 + @apply border-b-green-400 25 + text-green-400; 26 + } 27 } 28 29 + .link { 30 + @apply hidden 31 + items-center 32 + gap-2 33 + text-center 34 + text-neutral-200 35 + lg:flex; 36 37 + & > .icon { 38 + @apply size-4 39 + text-neutral-300; 40 + } 41 42 + &:is(:link, :visited) { 43 + &:hover { 44 + @apply text-neutral-400; 45 46 + & > .icon { 47 + @apply text-neutral-600; 48 + } 49 } 50 } 51 }
+4 -8
components/Common/CodeTabs/index.tsx
··· 6 7 import styles from './index.module.css'; 8 9 - export type CodeTabsExternaLink = { 10 linkUrl?: string; 11 linkText?: string; 12 }; 13 14 - type CodeTabsProps = Pick< 15 - ComponentProps<typeof Tabs>, 16 - 'tabs' | 'onValueChange' | 'defaultValue' 17 - > & 18 - CodeTabsExternaLink; 19 - 20 const CodeTabs: FC<PropsWithChildren<CodeTabsProps>> = ({ 21 children, 22 linkUrl, ··· 26 <Tabs 27 {...props} 28 className={styles.root} 29 - headerClassName={styles.header} 30 addons={ 31 linkUrl && 32 linkText && (
··· 6 7 import styles from './index.module.css'; 8 9 + type CodeTabsProps = Pick< 10 + ComponentProps<typeof Tabs>, 11 + 'tabs' | 'defaultValue' 12 + > & { 13 linkUrl?: string; 14 linkText?: string; 15 }; 16 17 const CodeTabs: FC<PropsWithChildren<CodeTabsProps>> = ({ 18 children, 19 linkUrl, ··· 23 <Tabs 24 {...props} 25 className={styles.root} 26 addons={ 27 linkUrl && 28 linkText && (
+1 -2
components/Common/CrossLink/index.module.css
··· 28 } 29 30 .icon { 31 - @apply h-4 32 - w-4 33 text-neutral-600 34 dark:text-neutral-400; 35 }
··· 28 } 29 30 .icon { 31 + @apply size-4 32 text-neutral-600 33 dark:text-neutral-400; 34 }
+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
···
··· 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
··· 2 .listItem:link, 3 .listItem:active { 4 @apply flex 5 - h-10 6 - w-10 7 items-center 8 justify-center 9 rounded ··· 16 17 &:hover { 18 @apply bg-neutral-100 19 - dark:bg-neutral-900; 20 } 21 }
··· 2 .listItem:link, 3 .listItem:active { 4 @apply flex 5 + size-10 6 items-center 7 justify-center 8 rounded ··· 15 16 &:hover { 17 @apply bg-neutral-100 18 + text-neutral-800 19 + dark:bg-neutral-900 20 + dark:text-neutral-200; 21 } 22 }
+1 -1
components/Common/Pagination/index.module.css
··· 35 @apply flex 36 list-none 37 justify-center 38 - gap-0.5 39 [grid-area:pages]; 40 }
··· 35 @apply flex 36 list-none 37 justify-center 38 + gap-1 39 [grid-area:pages]; 40 }
+4
components/Common/Pagination/index.tsx
··· 41 disabled={currentPage === 1} 42 kind="secondary" 43 className={styles.previousButton} 44 > 45 <ArrowLeftIcon className={styles.arrowIcon} /> 46 <span>{t('components.common.pagination.prev')}</span> 47 </Button> 48 <ol className={styles.list}>{parsedPages}</ol> 49 <Button 50 aria-label={t('components.common.pagination.nextAriaLabel')} 51 disabled={currentPage === pages.length} 52 kind="secondary" 53 className={styles.nextButton} 54 > 55 <span>{t('components.common.pagination.next')}</span> 56 <ArrowRightIcon className={styles.arrowIcon} />
··· 41 disabled={currentPage === 1} 42 kind="secondary" 43 className={styles.previousButton} 44 + href={pages[currentPage - 2]?.url} 45 > 46 <ArrowLeftIcon className={styles.arrowIcon} /> 47 <span>{t('components.common.pagination.prev')}</span> 48 </Button> 49 + 50 <ol className={styles.list}>{parsedPages}</ol> 51 + 52 <Button 53 aria-label={t('components.common.pagination.nextAriaLabel')} 54 disabled={currentPage === pages.length} 55 kind="secondary" 56 className={styles.nextButton} 57 + href={pages[currentPage]?.url} 58 > 59 <span>{t('components.common.pagination.next')}</span> 60 <ArrowRightIcon className={styles.arrowIcon} />
+29 -10
components/Common/Preview/index.module.css
··· 1 .root { 2 @apply relative 3 flex 4 items-center 5 bg-neutral-950 6 bg-[url('/static/images/patterns/hexagon-grid.svg')] 7 bg-contain 8 - bg-center; 9 10 &::after { 11 @apply absolute ··· 15 w-1/3 16 rounded-full 17 bg-gradient-radial 18 - blur-3xl 19 - content-['']; 20 21 - &.announcement { 22 @apply from-green-700/90; 23 } 24 ··· 31 } 32 } 33 34 - & > .container { 35 @apply z-10 36 mx-auto 37 flex 38 max-w-xl 39 flex-col 40 - gap-12 41 text-center 42 - text-3xl 43 font-semibold 44 - text-white; 45 46 - & > .logo { 47 - @apply mx-auto; 48 } 49 } 50 }
··· 1 .root { 2 @apply relative 3 flex 4 + aspect-[1.90/1] 5 items-center 6 + rounded 7 + border 8 + border-neutral-900 9 bg-neutral-950 10 bg-[url('/static/images/patterns/hexagon-grid.svg')] 11 bg-contain 12 + bg-center 13 + @container/preview; 14 15 &::after { 16 @apply absolute ··· 20 w-1/3 21 rounded-full 22 bg-gradient-radial 23 + blur-2xl 24 + content-[''] 25 + @md/preview:blur-3xl; 26 27 + &.announcements { 28 @apply from-green-700/90; 29 } 30 ··· 37 } 38 } 39 40 + .container { 41 @apply z-10 42 mx-auto 43 flex 44 + w-2/3 45 max-w-xl 46 flex-col 47 + gap-4 48 text-center 49 + text-xs 50 font-semibold 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; 60 61 + .logo { 62 + @apply mx-auto 63 + size-6 64 + @md/preview:size-14 65 + @lg/preview:size-16 66 + @xl/preview:size-20; 67 } 68 } 69 }
+8 -10
components/Common/Preview/index.stories.tsx
··· 5 type Story = StoryObj<typeof Preview>; 6 type Meta = MetaObj<typeof Preview>; 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 export const Announcement: Story = { 16 args: { 17 - type: 'announcement', 18 title: 19 'Changing the End-of-Life Date for Node.js 16 to September 11th, 2023', 20 }, ··· 38 args: { 39 title: 40 'Changing the End-of-Life Date for Node.js 16 to September 11th, 2023', 41 - width: 600, 42 - height: 315, 43 }, 44 }; 45 46 export default { component: Preview } as Meta;
··· 5 type Story = StoryObj<typeof Preview>; 6 type Meta = MetaObj<typeof Preview>; 7 8 export const Announcement: Story = { 9 args: { 10 + type: 'announcements', 11 title: 12 'Changing the End-of-Life Date for Node.js 16 to September 11th, 2023', 13 }, ··· 31 args: { 32 title: 33 'Changing the End-of-Life Date for Node.js 16 to September 11th, 2023', 34 }, 35 + decorators: [ 36 + Story => ( 37 + <div className="w-[600px]"> 38 + <Story /> 39 + </div> 40 + ), 41 + ], 42 }; 43 44 export default { component: Preview } as Meta;
+10 -21
components/Common/Preview/index.tsx
··· 1 import classNames from 'classnames'; 2 - import type { CSSProperties, ComponentProps, FC, ReactNode } from 'react'; 3 4 import JsIconWhite from '@/components/Icons/Logos/JsIconWhite'; 5 6 import styles from './index.module.css'; 7 8 type PreviewProps = { 9 - type?: 'announcement' | 'release' | 'vulnerability'; 10 - title: ReactNode; 11 - height?: CSSProperties['height']; 12 - width?: CSSProperties['width']; 13 - } & Omit<ComponentProps<'div'>, 'children'>; 14 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> 30 </div> 31 </div> 32 );
··· 1 import classNames from 'classnames'; 2 + import type { FC } from 'react'; 3 4 import JsIconWhite from '@/components/Icons/Logos/JsIconWhite'; 5 + import type { BlogPreviewType } from '@/types'; 6 7 import styles from './index.module.css'; 8 9 type PreviewProps = { 10 + title: string; 11 + type?: BlogPreviewType; 12 + }; 13 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} 19 </div> 20 </div> 21 );
+1 -1
components/Common/ProgressionSidebar/index.stories.tsx
··· 32 link: '/the-v8-javascript-engine', 33 }, 34 { 35 - label: 'An introduction to the NPM package manager', 36 link: '/an-introduction-to-the-npm-package-manager', 37 }, 38 ],
··· 32 link: '/the-v8-javascript-engine', 33 }, 34 { 35 + label: 'An introduction to the npm package manager', 36 link: '/an-introduction-to-the-npm-package-manager', 37 }, 38 ],
+34 -13
components/Common/ProgressionSidebar/index.tsx
··· 1 import type { ComponentProps, FC } from 'react'; 2 3 import ProgressionSidebarGroup from '@/components/Common/ProgressionSidebar/ProgressionSidebarGroup'; 4 - import WithSidebarSelect from '@/components/withSidebarSelect'; 5 6 import styles from './index.module.css'; 7 ··· 9 groups: Array<ComponentProps<typeof ProgressionSidebarGroup>>; 10 }; 11 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} 19 /> 20 - ))} 21 - 22 - <WithSidebarSelect groups={groups} /> 23 - </nav> 24 - ); 25 26 export default ProgressionSidebar;
··· 1 + import { useTranslations } from 'next-intl'; 2 import type { ComponentProps, FC } from 'react'; 3 4 import ProgressionSidebarGroup from '@/components/Common/ProgressionSidebar/ProgressionSidebarGroup'; 5 + import WithRouterSelect from '@/components/withRouterSelect'; 6 + import { useClientContext } from '@/hooks/react-server'; 7 8 import styles from './index.module.css'; 9 ··· 11 groups: Array<ComponentProps<typeof ProgressionSidebarGroup>>; 12 }; 13 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} 42 /> 43 + </nav> 44 + ); 45 + }; 46 47 export default ProgressionSidebar;
+1 -2
components/Common/Select/index.module.css
··· 109 } 110 111 .icon { 112 - @apply h-4 113 - w-4; 114 } 115 116 .text {
··· 109 } 110 111 .icon { 112 + @apply size-4; 113 } 114 115 .text {
+1 -1
components/Common/Select/index.stories.tsx
··· 51 }, 52 { 53 value: 'section-6', 54 - label: 'An introduction to the NPM package manager', 55 }, 56 { 57 value: 'section-7',
··· 51 }, 52 { 53 value: 'section-6', 54 + label: 'An introduction to the npm package manager', 55 }, 56 { 57 value: 'section-7',
+11 -12
components/Common/Tabs/index.module.css
··· 11 text-sm 12 font-semibold 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; 19 } 20 - } 21 22 - .tabsWithAddons { 23 - @apply flex 24 - justify-between; 25 - 26 - & > .addons { 27 - @apply border-b-2 28 border-b-transparent 29 px-1 30 pb-[11px]
··· 11 text-sm 12 font-semibold 13 text-neutral-800 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 + } 22 } 23 24 + .addons { 25 + @apply ml-auto 26 + border-b-2 27 border-b-transparent 28 px-1 29 pb-[11px]
+14 -26
components/Common/Tabs/index.tsx
··· 1 import * as TabsPrimitive from '@radix-ui/react-tabs'; 2 - import classNames from 'classnames'; 3 import type { FC, PropsWithChildren, ReactNode } from 'react'; 4 5 import styles from './index.module.css'; 6 7 - type Tab = { 8 - key: string; 9 - label: string; 10 - }; 11 12 - type TabsProps = { 13 tabs: Array<Tab>; 14 addons?: ReactNode; 15 - headerClassName?: string; 16 - } & TabsPrimitive.TabsProps; 17 18 const Tabs: FC<PropsWithChildren<TabsProps>> = ({ 19 tabs, 20 addons, 21 - headerClassName, 22 children, 23 ...props 24 }) => ( 25 <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> 42 43 {addons && <div className={styles.addons}>{addons}</div>} 44 - </div> 45 46 {children} 47 </TabsPrimitive.Root>
··· 1 import * as TabsPrimitive from '@radix-ui/react-tabs'; 2 import type { FC, PropsWithChildren, ReactNode } from 'react'; 3 4 import styles from './index.module.css'; 5 6 + type Tab = { key: string; label: string }; 7 8 + type TabsProps = TabsPrimitive.TabsProps & { 9 tabs: Array<Tab>; 10 addons?: ReactNode; 11 + }; 12 13 const Tabs: FC<PropsWithChildren<TabsProps>> = ({ 14 tabs, 15 addons, 16 children, 17 ...props 18 }) => ( 19 <TabsPrimitive.Root {...props}> 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 + ))} 30 31 {addons && <div className={styles.addons}>{addons}</div>} 32 + </TabsPrimitive.List> 33 34 {children} 35 </TabsPrimitive.Root>
+1 -2
components/Common/ThemeToggle/index.module.css
··· 1 .themeToggle { 2 - @apply h-9 3 - w-9 4 rounded-md 5 p-2 6 text-neutral-700
··· 1 .themeToggle { 2 + @apply size-9 3 rounded-md 4 p-2 5 text-neutral-700
+4 -2
components/Containers/MetaBar/index.module.css
··· 3 flex-col 4 items-start 5 gap-8 6 border-l 7 border-l-neutral-200 8 px-4 ··· 38 @apply font-semibold 39 text-neutral-900 40 underline 41 dark:text-white; 42 43 &:hover { ··· 55 } 56 57 svg { 58 - @apply h-4 59 - w-4 60 text-neutral-600 61 dark:text-neutral-400; 62 }
··· 3 flex-col 4 items-start 5 gap-8 6 + overflow-y-auto 7 border-l 8 border-l-neutral-200 9 px-4 ··· 39 @apply font-semibold 40 text-neutral-900 41 underline 42 + xs:inline-block 43 + xs:py-1 44 dark:text-white; 45 46 &:hover { ··· 58 } 59 60 svg { 61 + @apply size-4 62 text-neutral-600 63 dark:text-neutral-400; 64 }
+8 -6
components/Containers/MetaBar/index.tsx
··· 29 return ( 30 <div className={styles.wrapper}> 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 - ))} 38 39 {heading.length > 0 && ( 40 <>
··· 29 return ( 30 <div className={styles.wrapper}> 31 <dl> 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 + ))} 40 41 {heading.length > 0 && ( 42 <>
+1 -2
components/Containers/NavBar/NavItem/index.module.css
··· 13 } 14 15 .icon { 16 - @apply h-3 17 - w-3 18 text-neutral-500 19 dark:text-neutral-200; 20 }
··· 13 } 14 15 .icon { 16 + @apply size-3 17 text-neutral-500 18 dark:text-neutral-200; 19 }
+2 -4
components/Containers/NavBar/index.module.css
··· 45 } 46 47 .navInteractionIcon { 48 - @apply h-6 49 - w-6; 50 } 51 52 .sidebarItemTogglerLabel { ··· 91 } 92 93 .ghIconWrapper { 94 - @apply h-9 95 - w-9 96 rounded-md 97 p-2; 98
··· 45 } 46 47 .navInteractionIcon { 48 + @apply size-6; 49 } 50 51 .sidebarItemTogglerLabel { ··· 90 } 91 92 .ghIconWrapper { 93 + @apply size-9 94 rounded-md 95 p-2; 96
+36 -13
components/Containers/Sidebar/index.tsx
··· 1 import type { ComponentProps, FC } from 'react'; 2 3 import SidebarGroup from '@/components/Containers/Sidebar/SidebarGroup'; 4 - import WithSidebarSelect from '@/components/withSidebarSelect'; 5 6 import styles from './index.module.css'; 7 ··· 9 groups: Array<ComponentProps<typeof SidebarGroup>>; 10 }; 11 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 - ))} 21 22 - <WithSidebarSelect groups={groups} /> 23 - </aside> 24 - ); 25 26 export default SideBar;
··· 1 + import { useTranslations } from 'next-intl'; 2 import type { ComponentProps, FC } from 'react'; 3 4 import SidebarGroup from '@/components/Containers/Sidebar/SidebarGroup'; 5 + import WithRouterSelect from '@/components/withRouterSelect'; 6 + import { useClientContext } from '@/hooks/react-server'; 7 8 import styles from './index.module.css'; 9 ··· 11 groups: Array<ComponentProps<typeof SidebarGroup>>; 12 }; 13 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 + ))} 37 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 + }; 48 49 export default SideBar;
+2 -4
components/Downloads/ChangelogModal/index.module.css
··· 36 right-3 37 top-3 38 block 39 - h-6 40 - w-6 41 cursor-pointer 42 sm:hidden; 43 } ··· 76 } 77 78 svg { 79 - @apply h-3 80 - w-3 81 text-neutral-600; 82 } 83 }
··· 36 right-3 37 top-3 38 block 39 + size-6 40 cursor-pointer 41 sm:hidden; 42 } ··· 75 } 76 77 svg { 78 + @apply size-3 79 text-neutral-600; 80 } 81 }
+3 -2
components/Downloads/DownloadButton/index.tsx
··· 18 children, 19 }) => { 20 const { os, bitness } = useDetectOS(); 21 22 return ( 23 <> 24 <Button 25 kind="special" 26 - href={downloadUrlByOS(versionWithPrefix, os, bitness)} 27 className={classNames(styles.downloadButton, 'hidden dark:flex')} 28 > 29 {children} ··· 33 34 <Button 35 kind="primary" 36 - href={downloadUrlByOS(versionWithPrefix, os, bitness)} 37 className={classNames(styles.downloadButton, 'flex dark:hidden')} 38 > 39 {children}
··· 18 children, 19 }) => { 20 const { os, bitness } = useDetectOS(); 21 + const downloadLink = downloadUrlByOS(versionWithPrefix, os, bitness); 22 23 return ( 24 <> 25 <Button 26 kind="special" 27 + href={downloadLink} 28 className={classNames(styles.downloadButton, 'hidden dark:flex')} 29 > 30 {children} ··· 34 35 <Button 36 kind="primary" 37 + href={downloadLink} 38 className={classNames(styles.downloadButton, 'flex dark:hidden')} 39 > 40 {children}
+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
··· 1 'use client'; 2 3 import * as TabsPrimitive from '@radix-ui/react-tabs'; 4 - import type { FC, ReactElement } from 'react'; 5 6 - import type { CodeTabsExternaLink } from '@/components/Common/CodeTabs'; 7 import CodeTabs from '@/components/Common/CodeTabs'; 8 9 - type MDXCodeTabsProps = { 10 children: Array<ReactElement>; 11 languages: string; 12 displayNames?: string; 13 defaultTab?: string; 14 - } & CodeTabsExternaLink; 15 16 const MDXCodeTabs: FC<MDXCodeTabsProps> = ({ 17 languages: rawLanguages,
··· 1 'use client'; 2 3 import * as TabsPrimitive from '@radix-ui/react-tabs'; 4 + import type { ComponentProps, FC, ReactElement } from 'react'; 5 6 import CodeTabs from '@/components/Common/CodeTabs'; 7 8 + type MDXCodeTabsProps = Pick< 9 + ComponentProps<typeof CodeTabs>, 10 + 'linkText' | 'linkUrl' 11 + > & { 12 children: Array<ReactElement>; 13 languages: string; 14 displayNames?: string; 15 defaultTab?: string; 16 + }; 17 18 const MDXCodeTabs: FC<MDXCodeTabsProps> = ({ 19 languages: rawLanguages,
+11 -8
components/Pagination.tsx
··· 2 import type { FC } from 'react'; 3 4 import Link from '@/components/Link'; 5 6 - type PaginationProps = { prev?: number | null; next?: number | null }; 7 8 - const Pagination: FC<PaginationProps> = ({ next, prev }) => { 9 const t = useTranslations(); 10 11 return ( 12 <nav aria-label="pagination" className="pagination"> 13 - {next && ( 14 - <Link href={`/blog/year-${next}`}> 15 - &lt; {t('components.pagination.next')} 16 </Link> 17 )} 18 19 - {prev && ( 20 - <Link href={`/blog/year-${prev}`}> 21 - {t('components.pagination.previous')} &gt; 22 </Link> 23 )} 24 </nav>
··· 2 import type { FC } from 'react'; 3 4 import Link from '@/components/Link'; 5 + import type { BlogPagination } from '@/types'; 6 7 + type PaginationProps = BlogPagination & { category: string }; 8 9 + const Pagination: FC<PaginationProps> = ({ category, next, prev }) => { 10 const t = useTranslations(); 11 12 return ( 13 <nav aria-label="pagination" className="pagination"> 14 + {prev && ( 15 + <Link href={`/blog/${category}/page/${prev}`}> 16 + &lt; {t('components.pagination.previous')} 17 </Link> 18 )} 19 20 + {prev && next && ' | '} 21 + 22 + {next && ( 23 + <Link href={`/blog/${category}/page/${next}`}> 24 + {t('components.pagination.next')} &gt; 25 </Link> 26 )} 27 </nav>
+3 -1
components/__design__/text.stories.tsx
··· 24 export const InlineCode: StoryObj = { 25 render: () => ( 26 <main> 27 - This is an example of <code>inline code block</code> 28 </main> 29 ), 30 };
··· 24 export const InlineCode: StoryObj = { 25 render: () => ( 26 <main> 27 + <p> 28 + This is an example of <code>inline code block</code> 29 + </p> 30 </main> 31 ), 32 };
+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
···
··· 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
··· 4 import { useClientContext, useSiteNavigation } from '@/hooks/server'; 5 import type { NavigationKeys } from '@/types'; 6 7 - type WithCrossLinksProps = { 8 - navKey: NavigationKeys; 9 - }; 10 11 - const WithCrossLinks: FC<WithCrossLinksProps> = ({ navKey }) => { 12 const { getSideNavigation } = useSiteNavigation(); 13 const { pathname } = useClientContext(); 14 ··· 46 ); 47 }; 48 49 - export default WithCrossLinks;
··· 4 import { useClientContext, useSiteNavigation } from '@/hooks/server'; 5 import type { NavigationKeys } from '@/types'; 6 7 + type WithCrossLinksProps = { navKey: NavigationKeys }; 8 9 + const WithSidebarCrossLinks: FC<WithCrossLinksProps> = ({ navKey }) => { 10 const { getSideNavigation } = useSiteNavigation(); 11 const { pathname } = useClientContext(); 12 ··· 44 ); 45 }; 46 47 + export default WithSidebarCrossLinks;
+4
components/withLayout.tsx
··· 9 import LegacyIndexLayout from '@/layouts/IndexLayout'; 10 import LegacyLearnLayout from '@/layouts/LearnLayout'; 11 import AboutLayout from '@/layouts/New/About'; 12 import DefaultLayout from '@/layouts/New/Default'; 13 import DocsLayout from '@/layouts/New/Docs'; 14 import HomeLayout from '@/layouts/New/Home'; 15 import LearnLayout from '@/layouts/New/Learn'; 16 import { ENABLE_WEBSITE_REDESIGN } from '@/next.constants.mjs'; 17 import type { Layouts, LegacyLayouts } from '@/types'; 18 ··· 35 'home.hbs': HomeLayout, 36 'learn.hbs': LearnLayout, 37 'page.hbs': DefaultLayout, 38 } satisfies Record<Layouts, FC>; 39 40 type WithLayout<L = Layouts | LegacyLayouts> = PropsWithChildren<{ layout: L }>;
··· 9 import LegacyIndexLayout from '@/layouts/IndexLayout'; 10 import LegacyLearnLayout from '@/layouts/LearnLayout'; 11 import AboutLayout from '@/layouts/New/About'; 12 + import BlogLayout from '@/layouts/New/Blog'; 13 import DefaultLayout from '@/layouts/New/Default'; 14 import DocsLayout from '@/layouts/New/Docs'; 15 import HomeLayout from '@/layouts/New/Home'; 16 import LearnLayout from '@/layouts/New/Learn'; 17 + import PostLayout from '@/layouts/New/Post'; 18 import { ENABLE_WEBSITE_REDESIGN } from '@/next.constants.mjs'; 19 import type { Layouts, LegacyLayouts } from '@/types'; 20 ··· 37 'home.hbs': HomeLayout, 38 'learn.hbs': LearnLayout, 39 'page.hbs': DefaultLayout, 40 + 'blog-post.hbs': PostLayout, 41 + 'blog-category.hbs': BlogLayout, 42 } satisfies Record<Layouts, FC>; 43 44 type WithLayout<L = Layouts | LegacyLayouts> = PropsWithChildren<{ layout: L }>;
+9 -5
components/withMetaBar.tsx
··· 7 import { useClientContext } from '@/hooks/server'; 8 import { getGitHubEditPageUrl } from '@/util/gitHubUtils'; 9 10 const WithMetaBar: FC = () => { 11 const { headings, readingTime, frontmatter, filename } = useClientContext(); 12 const formatter = useFormatter(); 13 14 - const lastUpdated = formatter.dateTime(frontmatter.date ?? new Date(), { 15 - month: 'short', 16 - day: '2-digit', 17 - year: 'numeric', 18 - }); 19 20 return ( 21 <MetaBar
··· 7 import { useClientContext } from '@/hooks/server'; 8 import { getGitHubEditPageUrl } from '@/util/gitHubUtils'; 9 10 + const DATE_FORMAT = { 11 + month: 'short', 12 + day: '2-digit', 13 + year: 'numeric', 14 + } as const; 15 + 16 const WithMetaBar: FC = () => { 17 const { headings, readingTime, frontmatter, filename } = useClientContext(); 18 const formatter = useFormatter(); 19 20 + const lastUpdated = frontmatter.date 21 + ? formatter.dateTime(new Date(frontmatter.date), DATE_FORMAT) 22 + : undefined; 23 24 return ( 25 <MetaBar
+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
··· 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
··· 38 "howMuchJavascriptDoYouNeedToKnowToUseNodejs": "How much JavaScript do you need to know to use Node.js?", 39 "differencesBetweenNodejsAndTheBrowser": "Differences between Node.js and the Browser", 40 "theV8JavascriptEngine": "The V8 JavaScript Engine", 41 - "anIntroductionToTheNpmPackageManager": "An introduction to the NPM package manager", 42 "ecmascript2015Es6AndBeyond": "ECMAScript 2015 (ES6) and beyond", 43 "nodejsTheDifferenceBetweenDevelopmentAndProduction": "Node.js, the difference between development and production", 44 "nodejsWithTypescript": "Node.js with TypeScript", ··· 118 "docs": "Docs" 119 }, 120 "pagination": { 121 - "next": "Newer | ", 122 - "previous": "Older" 123 }, 124 "common": { 125 "breadcrumbs": { ··· 146 }, 147 "languageDropdown": { 148 "label": "Choose Language" 149 - }, 150 - "card": { 151 - "announcement": "Announcements", 152 - "release": "Releases", 153 - "vulnerability": "Vulnerabilities" 154 } 155 }, 156 "metabar": { ··· 192 } 193 }, 194 "blogIndex": { 195 - "currentYear": "Blog from {year}" 196 } 197 }, 198 "pages": {
··· 38 "howMuchJavascriptDoYouNeedToKnowToUseNodejs": "How much JavaScript do you need to know to use Node.js?", 39 "differencesBetweenNodejsAndTheBrowser": "Differences between Node.js and the Browser", 40 "theV8JavascriptEngine": "The V8 JavaScript Engine", 41 + "anIntroductionToTheNpmPackageManager": "An introduction to the npm package manager", 42 "ecmascript2015Es6AndBeyond": "ECMAScript 2015 (ES6) and beyond", 43 "nodejsTheDifferenceBetweenDevelopmentAndProduction": "Node.js, the difference between development and production", 44 "nodejsWithTypescript": "Node.js with TypeScript", ··· 118 "docs": "Docs" 119 }, 120 "pagination": { 121 + "next": "Next", 122 + "previous": "Previous" 123 }, 124 "common": { 125 "breadcrumbs": { ··· 146 }, 147 "languageDropdown": { 148 "label": "Choose Language" 149 } 150 }, 151 "metabar": { ··· 187 } 188 }, 189 "blogIndex": { 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 + } 211 } 212 }, 213 "pages": {
+19 -43
layouts/BlogCategoryLayout.tsx
··· 1 - import { notFound } from 'next/navigation'; 2 import { getTranslations } from 'next-intl/server'; 3 import type { FC } from 'react'; 4 ··· 9 import getBlogData from '@/next-data/blogData'; 10 11 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 - } 20 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 - } 37 38 - return { ...data, category: year }; 39 }; 40 41 // This is a React Async Server Component 42 // Note that Hooks cannot be used in a RSC async component 43 // Async Components do not get re-rendered at all. 44 const BlogCategoryLayout: FC = async () => { 45 - const { frontmatter, pathname } = getClientContext(); 46 47 const t = await getTranslations(); 48 49 const { posts, pagination, category } = await getCategoryData(pathname); 50 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 return ( 65 <div className="container" dir="auto"> 66 - <h2>{title}</h2> 67 68 <ul className="blog-index"> 69 {posts.map(({ slug, date, title }) => ( 70 <li key={slug}> 71 - <Time date={date} format={{ month: 'short', day: '2-digit' }} /> 72 <Link href={slug}>{title}</Link> 73 </li> 74 ))} 75 </ul> 76 77 - <Pagination {...pagination} /> 78 </div> 79 ); 80 };
··· 1 import { getTranslations } from 'next-intl/server'; 2 import type { FC } from 'react'; 3 ··· 8 import getBlogData from '@/next-data/blogData'; 9 10 const getCategoryData = async (pathname: string) => { 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('/'); 17 18 + const { posts, pagination } = await getBlogData(category, Number(page)); 19 20 + return { posts, category, pagination }; 21 }; 22 23 // This is a React Async Server Component 24 // Note that Hooks cannot be used in a RSC async component 25 // Async Components do not get re-rendered at all. 26 const BlogCategoryLayout: FC = async () => { 27 + const { pathname } = getClientContext(); 28 29 const t = await getTranslations(); 30 31 const { posts, pagination, category } = await getCategoryData(pathname); 32 33 return ( 34 <div className="container" dir="auto"> 35 + <h2 style={{ textTransform: 'capitalize' }}> 36 + {t('layouts.blogIndex.categoryName', { 37 + category: category.replace('year-', ''), 38 + })} 39 + </h2> 40 41 <ul className="blog-index"> 42 {posts.map(({ slug, date, title }) => ( 43 <li key={slug}> 44 + <Time 45 + date={date} 46 + format={{ year: 'numeric', month: 'short', day: '2-digit' }} 47 + /> 48 <Link href={slug}>{title}</Link> 49 </li> 50 ))} 51 </ul> 52 53 + <Pagination category={category} {...pagination} /> 54 </div> 55 ); 56 };
+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
···
··· 1 + import type { FC, PropsWithChildren } from 'react'; 2 + 3 + import styles from './layouts.module.css'; 4 + 5 + const ContentLayout: FC<PropsWithChildren> = ({ children }) => ( 6 + <div className={styles.contentLayout}>{children}</div> 7 + ); 8 + 9 + export default ContentLayout;
+6 -7
layouts/New/Docs.tsx
··· 3 import WithFooter from '@/components/withFooter'; 4 import WithMetaBar from '@/components/withMetaBar'; 5 import WithNavBar from '@/components/withNavBar'; 6 - import WithSideBar from '@/components/withSidebar'; 7 - import ArticleLayout from '@/layouts/New/Article'; 8 9 // @deprecated: This Layout is Temporary. The `en/docs` route should eventually be removed 10 // and all "guides" moved to the Learn section. ··· 13 <> 14 <WithNavBar /> 15 16 - <ArticleLayout> 17 - <WithSideBar navKeys={[]} /> 18 - 19 - <main>{children}</main> 20 21 <WithMetaBar /> 22 - </ArticleLayout> 23 24 <WithFooter /> 25 </>
··· 3 import WithFooter from '@/components/withFooter'; 4 import WithMetaBar from '@/components/withMetaBar'; 5 import WithNavBar from '@/components/withNavBar'; 6 + import ContentLayout from '@/layouts/New/Content'; 7 8 // @deprecated: This Layout is Temporary. The `en/docs` route should eventually be removed 9 // and all "guides" moved to the Learn section. ··· 12 <> 13 <WithNavBar /> 14 15 + <ContentLayout> 16 + <div> 17 + <main>{children}</main> 18 + </div> 19 20 <WithMetaBar /> 21 + </ContentLayout> 22 23 <WithFooter /> 24 </>
+4 -4
layouts/New/Home.tsx
··· 9 <> 10 <WithNavBar /> 11 12 - <main className={styles.homeLayout}> 13 - <div className={styles.hexagonBackdrop} /> 14 15 - {children} 16 - </main> 17 18 <WithFooter /> 19 </>
··· 9 <> 10 <WithNavBar /> 11 12 + <div className={styles.homeLayout}> 13 + <div className="glowingBackdrop" /> 14 15 + <main>{children}</main> 16 + </div> 17 18 <WithFooter /> 19 </>
+2 -2
layouts/New/Learn.tsx
··· 1 import type { FC, PropsWithChildren } from 'react'; 2 3 import WithBreadcrumbs from '@/components/withBreadcrumbs'; 4 - import WithCrossLinks from '@/components/withCrossLinks'; 5 import WithMetaBar from '@/components/withMetaBar'; 6 import WithNavBar from '@/components/withNavBar'; 7 import WithProgressionSidebar from '@/components/withProgressionSidebar'; 8 import ArticleLayout from '@/layouts/New/Article'; 9 10 const LearnLayout: FC<PropsWithChildren> = ({ children }) => ( ··· 17 <main> 18 {children} 19 20 - <WithCrossLinks navKey="learn" /> 21 </main> 22 23 <WithMetaBar />
··· 1 import type { FC, PropsWithChildren } from 'react'; 2 3 import WithBreadcrumbs from '@/components/withBreadcrumbs'; 4 import WithMetaBar from '@/components/withMetaBar'; 5 import WithNavBar from '@/components/withNavBar'; 6 import WithProgressionSidebar from '@/components/withProgressionSidebar'; 7 + import WithSidebarCrossLinks from '@/components/withSidebarCrossLinks'; 8 import ArticleLayout from '@/layouts/New/Article'; 9 10 const LearnLayout: FC<PropsWithChildren> = ({ children }) => ( ··· 17 <main> 18 {children} 19 20 + <WithSidebarCrossLinks navKey="learn" /> 21 </main> 22 23 <WithMetaBar />
+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
··· 1 .baseLayout { 2 @apply grid 3 - h-screen 4 - w-screen 5 grid-cols-[1fr] 6 grid-rows-[auto_1fr_auto]; 7 } ··· 25 } 26 27 > *: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 35 overflow-x-hidden 36 bg-gradient-subtle 37 p-12 ··· 66 } 67 68 .homeLayout { 69 - @apply mx-auto 70 - flex 71 w-full 72 - flex-col 73 items-center 74 - gap-8 75 - self-stretch 76 px-4 77 py-14 78 - md:w-auto 79 - md:flex-row 80 - md:gap-14 81 md:px-14 82 - md:py-0 83 - lg:gap-28 84 lg:px-28; 85 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; 97 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 - } 111 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; 120 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 - } 131 132 - p { 133 - @apply max-w-[400px] 134 - text-base 135 - text-neutral-900 136 - md:text-lg 137 - dark:text-white; 138 } 139 140 - > div { 141 - &:nth-of-type(1) { 142 - @apply flex 143 - flex-col 144 - gap-4; 145 } 146 147 - &:nth-of-type(2) { 148 - @apply flex 149 - w-full 150 - flex-col 151 - gap-2 152 - xl:flex-row; 153 } 154 } 155 } 156 157 - &:nth-of-type(2) { 158 @apply flex 159 - max-w-md 160 - flex-col 161 - content-center 162 items-center 163 - gap-4 164 - md:max-w-2xl; 165 166 - p { 167 - @apply text-center 168 - text-sm 169 - text-neutral-800 170 - dark:text-neutral-200; 171 - } 172 } 173 } 174 }
··· 1 .baseLayout { 2 @apply grid 3 + size-full 4 grid-cols-[1fr] 5 grid-rows-[auto_1fr_auto]; 6 } ··· 24 } 25 26 > *:nth-child(2) { 27 + @apply overflow-y-auto 28 overflow-x-hidden 29 bg-gradient-subtle 30 p-12 ··· 59 } 60 61 .homeLayout { 62 + @apply flex 63 w-full 64 items-center 65 + justify-center 66 px-4 67 py-14 68 md:px-14 69 lg:px-28; 70 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; 79 80 + section { 81 + &:nth-of-type(1) { 82 + @apply flex 83 + max-w-[500px] 84 + flex-[1_0] 85 + flex-col 86 + gap-8; 87 88 + > div { 89 + @apply flex 90 + max-w-[400px] 91 + flex-col 92 + gap-4; 93 94 + p { 95 + @apply text-base 96 + md:text-lg; 97 + } 98 99 + small { 100 + @apply text-center 101 + text-sm 102 + text-neutral-800 103 + xs:text-xs 104 + dark:text-neutral-400; 105 + } 106 + } 107 } 108 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 + } 134 } 135 136 + > p { 137 + @apply text-center 138 + text-sm 139 + text-neutral-800 140 + dark:text-neutral-200; 141 } 142 } 143 } 144 + } 145 + } 146 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 { 207 @apply flex 208 + flex-row 209 items-center 210 + gap-4; 211 + } 212 213 + > div:nth-of-type(1) { 214 + @apply mb-4 215 + mt-2; 216 } 217 } 218 }
+10 -4
next-data/blogData.ts
··· 4 NEXT_DATA_URL, 5 VERCEL_ENV, 6 } from '@/next.constants.mjs'; 7 - import type { BlogDataRSC } from '@/types'; 8 9 - const getBlogData = (category: string): Promise<BlogDataRSC> => { 10 // When we're using Static Exports the Next.js Server is not running (during build-time) 11 // hence the self-ingestion APIs will not be available. In this case we want to load 12 // the data directly within the current thread, which will anyways be loaded only once 13 // We use lazy-imports to prevent `provideBlogData` from executing on import 14 if (ENABLE_STATIC_EXPORT || (!IS_DEVELOPMENT && !VERCEL_ENV)) { 15 return import('@/next-data/providers/blogData').then( 16 - ({ default: provideBlogData }) => provideBlogData(category) 17 ); 18 } 19 20 // When we're on RSC with Server capabilities we prefer using Next.js Data Fetching 21 // as this will load cached data from the server instead of generating data on the fly 22 // 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()); 24 }; 25 26 export default getBlogData;
··· 4 NEXT_DATA_URL, 5 VERCEL_ENV, 6 } from '@/next.constants.mjs'; 7 + import type { BlogPostsRSC } from '@/types'; 8 9 + const getBlogData = (cat: string, page?: number): Promise<BlogPostsRSC> => { 10 // When we're using Static Exports the Next.js Server is not running (during build-time) 11 // hence the self-ingestion APIs will not be available. In this case we want to load 12 // the data directly within the current thread, which will anyways be loaded only once 13 // We use lazy-imports to prevent `provideBlogData` from executing on import 14 if (ENABLE_STATIC_EXPORT || (!IS_DEVELOPMENT && !VERCEL_ENV)) { 15 return import('@/next-data/providers/blogData').then( 16 + ({ provideBlogPosts, providePaginatedBlogPosts }) => 17 + page ? providePaginatedBlogPosts(cat, page) : provideBlogPosts(cat) 18 ); 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 + 26 // When we're on RSC with Server capabilities we prefer using Next.js Data Fetching 27 // as this will load cached data from the server instead of generating data on the fly 28 // this is extremely useful for ISR and SSG as it will not generate this data on every request 29 + return fetch(fetchURL).then(r => r.json()); 30 }; 31 32 export default getBlogData;
+18 -25
next-data/generators/blogData.mjs
··· 12 const blogPath = join(process.cwd(), 'pages/en/blog'); 13 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>}} 19 */ 20 - const blogMetadata = { pagination: new Set(), categories: new Set() }; 21 22 /** 23 * This method parses the source (raw) Markdown content into Frontmatter ··· 34 category = 'uncategorized', 35 } = graymatter(source).data; 36 37 - // we add the year to the pagination set 38 - blogMetadata.pagination.add(new Date(date).getUTCFullYear()); 39 40 // we add the category to the categories set 41 - blogMetadata.categories.add(category); 42 43 // this is the url used for the blog post it based on the category and filename 44 const slug = `/blog/${category}/${basename(filename, extname(filename))}`; 45 46 - return { title, author, date: new Date(date), category, slug }; 47 }; 48 49 /** ··· 53 * @return {Promise<import('../../types').BlogData>} 54 */ 55 const generateBlogData = async () => { 56 - // we retrieve all the filenames of all blog posts 57 const filenames = await getMarkdownFiles(process.cwd(), 'pages/en/blog', [ 58 '**/index.md', 59 - '**/pagination.md', 60 ]); 61 62 return new Promise(resolve => { 63 - const blogPosts = []; 64 const rawFrontmatter = []; 65 66 filenames.forEach(filename => { ··· 95 // This allows us to only read the frontmatter part of each file 96 // and optimise the read-process as we have thousands of markdown files 97 _readLine.on('close', () => { 98 - const frontmatter = getFrontMatter( 99 - filename, 100 - rawFrontmatter[filename][1] 101 - ); 102 - 103 - blogPosts.push(frontmatter); 104 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 - }); 112 } 113 }); 114 });
··· 12 const blogPath = join(process.cwd(), 'pages/en/blog'); 13 14 /** 15 + * This contains the metadata of all available blog categories 16 */ 17 + const blogCategories = new Set(['all']); 18 19 /** 20 * This method parses the source (raw) Markdown content into Frontmatter ··· 31 category = 'uncategorized', 32 } = graymatter(source).data; 33 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}`); 43 44 // we add the category to the categories set 45 + blogCategories.add(category); 46 47 // this is the url used for the blog post it based on the category and filename 48 const slug = `/blog/${category}/${basename(filename, extname(filename))}`; 49 50 + return { title, author, date: new Date(date), categories, slug }; 51 }; 52 53 /** ··· 57 * @return {Promise<import('../../types').BlogData>} 58 */ 59 const generateBlogData = async () => { 60 + // We retrieve the full pathnames of all Blog Posts to read each file individually 61 const filenames = await getMarkdownFiles(process.cwd(), 'pages/en/blog', [ 62 '**/index.md', 63 ]); 64 65 return new Promise(resolve => { 66 + const posts = []; 67 const rawFrontmatter = []; 68 69 filenames.forEach(filename => { ··· 98 // This allows us to only read the frontmatter part of each file 99 // and optimise the read-process as we have thousands of markdown files 100 _readLine.on('close', () => { 101 + posts.push(getFrontMatter(filename, rawFrontmatter[filename][1])); 102 103 + if (posts.length === filenames.length) { 104 + resolve({ categories: [...blogCategories], posts }); 105 } 106 }); 107 });
+30 -32
next-data/generators/websiteFeeds.mjs
··· 13 * This method generates RSS website feeds based on the current website configuration 14 * and the current blog data that is available 15 * 16 - * @param {Promise<import('../../types').BlogDataRSC>} blogData 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 - }); 34 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 - })); 44 45 - blogFeedEntries.forEach(entry => feed.addItem(entry)); 46 47 - return [file, feed]; 48 - } 49 - ); 50 51 - return new Map(websiteFeeds); 52 - }); 53 }; 54 55 export default generateWebsiteFeeds;
··· 13 * This method generates RSS website feeds based on the current website configuration 14 * and the current blog data that is available 15 * 16 + * @param {import('../../types').BlogPostsRSC} blogData 17 */ 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 + }); 33 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 + })); 43 44 + blogFeedEntries.forEach(entry => feed.addItem(entry)); 45 46 + return [file, feed]; 47 + } 48 + ); 49 50 + return new Map(websiteFeeds); 51 }; 52 53 export default generateWebsiteFeeds;
+54 -33
next-data/providers/blogData.ts
··· 1 import { cache } from 'react'; 2 3 import generateBlogData from '@/next-data/generators/blogData.mjs'; 4 - import type { BlogDataRSC } from '@/types'; 5 6 - const blogData = generateBlogData(); 7 8 - const provideBlogData = cache( 9 - async (category?: string): Promise<BlogDataRSC> => { 10 - return blogData.then(({ posts, categories, pagination }) => { 11 - const meta = { categories, pagination }; 12 13 - if (category && categories.includes(category)) { 14 - return { 15 - posts: posts.filter(post => post.category === category), 16 - pagination: { next: null, prev: null }, 17 - meta, 18 - }; 19 - } 20 21 - if (category && category.startsWith('year-')) { 22 - const paramYear = Number(category.replace('year-', '')); 23 24 - const isEqualYear = (date: string) => 25 - new Date(date).getFullYear() === paramYear; 26 27 - return { 28 - posts: posts.filter(({ date }) => isEqualYear(date)), 29 - pagination: { 30 - next: pagination.includes(paramYear + 1) ? paramYear + 1 : null, 31 - prev: pagination.includes(paramYear - 1) ? paramYear - 1 : null, 32 - }, 33 - meta, 34 - }; 35 - } 36 37 - if (category && !categories.includes(category)) { 38 - return { posts: [], pagination: { next: null, prev: null }, meta }; 39 - } 40 41 - return { posts, pagination: { next: null, prev: null }, meta }; 42 - }); 43 } 44 ); 45 - 46 - export default provideBlogData;
··· 1 import { cache } from 'react'; 2 3 import generateBlogData from '@/next-data/generators/blogData.mjs'; 4 + import { BLOG_POSTS_PER_PAGE } from '@/next.constants.mjs'; 5 + import type { BlogPostsRSC } from '@/types'; 6 7 + const { categories, posts } = await generateBlogData(); 8 9 + export const provideBlogCategories = cache(() => categories); 10 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()); 15 16 + // Total amount of possible pages given the amount of blog posts 17 + const total = categoryPosts.length / BLOG_POSTS_PER_PAGE; 18 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 + }); 31 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; 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 + } 56 57 + return { 58 + posts: [], 59 + pagination: { 60 + prev: pagination.total, 61 + next: null, 62 + pages: pagination.pages, 63 + total: posts.length, 64 + }, 65 + }; 66 } 67 );
+2 -2
next-data/providers/releaseData.ts
··· 2 3 import generateReleaseData from '@/next-data/generators/releaseData.mjs'; 4 5 - const releaseData = generateReleaseData(); 6 7 - const provideReleaseData = cache(async () => releaseData); 8 9 export default provideReleaseData;
··· 2 3 import generateReleaseData from '@/next-data/generators/releaseData.mjs'; 4 5 + const releaseData = await generateReleaseData(); 6 7 + const provideReleaseData = cache(() => releaseData); 8 9 export default provideReleaseData;
+7 -9
next-data/providers/websiteFeeds.ts
··· 1 import { cache } from 'react'; 2 3 import generateWebsiteFeeds from '@/next-data/generators/websiteFeeds.mjs'; 4 - import provideBlogData from '@/next-data/providers/blogData'; 5 6 - const websiteFeeds = generateWebsiteFeeds(provideBlogData()); 7 8 - const provideWebsiteFeeds = cache(async (feed: string) => { 9 - return websiteFeeds.then(feeds => { 10 - if (feed.includes('.xml') && feeds.has(feed)) { 11 - return feeds.get(feed)?.rss2(); 12 - } 13 14 - return undefined; 15 - }); 16 }); 17 18 export default provideWebsiteFeeds;
··· 1 import { cache } from 'react'; 2 3 import generateWebsiteFeeds from '@/next-data/generators/websiteFeeds.mjs'; 4 + import { provideBlogPosts } from '@/next-data/providers/blogData'; 5 6 + const websiteFeeds = await generateWebsiteFeeds(provideBlogPosts('all')); 7 8 + const provideWebsiteFeeds = cache((feed: string) => { 9 + if (feed.includes('.xml') && websiteFeeds.has(feed)) { 10 + return websiteFeeds.get(feed)?.rss2(); 11 + } 12 13 + return undefined; 14 }); 15 16 export default provideWebsiteFeeds;
+4 -2
next.config.mjs
··· 9 BASE_PATH, 10 ENABLE_STATIC_EXPORT, 11 ENABLE_WEBSITE_REDESIGN, 12 SENTRY_DSN, 13 SENTRY_ENABLE, 14 SENTRY_EXTENSIONS, 15 SENTRY_TUNNEL, 16 - } from './next.constants.mjs'; 17 - import { redirects, rewrites } from './next.rewrites.mjs'; 18 19 /** @type {import('next').NextConfig} */ 20 const nextConfig = {
··· 9 BASE_PATH, 10 ENABLE_STATIC_EXPORT, 11 ENABLE_WEBSITE_REDESIGN, 12 + } from './next.constants.mjs'; 13 + import { redirects, rewrites } from './next.rewrites.mjs'; 14 + import { 15 SENTRY_DSN, 16 SENTRY_ENABLE, 17 SENTRY_EXTENSIONS, 18 SENTRY_TUNNEL, 19 + } from './sentry.constants.mjs'; 20 21 /** @type {import('next').NextConfig} */ 22 const nextConfig = {
+14 -58
next.constants.mjs
··· 1 'use strict'; 2 3 /** 4 - * This is used for the current Legacy Website Blog Pagination Generation 5 - * 6 - * @deperecated remove with website redesign 7 - */ 8 - export const CURRENT_YEAR = new Date().getFullYear(); 9 - 10 - /** 11 * This is used to verify if the current Website is running on a Development Environment 12 */ 13 export const IS_DEVELOPMENT = process.env.NODE_ENV === 'development'; ··· 115 */ 116 export const MD_EXTENSION_REGEX = /((\/)?(index))?\.mdx?$/i; 117 118 /*** 119 * This is a list of all external links that are used on website sitemap. 120 * @see https://github.com/nodejs/nodejs.org/issues/5813 for more context ··· 128 'https://trademark-list.openjsf.org/', 129 'https://www.linuxfoundation.org/cookies', 130 ]; 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 - };
··· 1 'use strict'; 2 3 /** 4 * This is used to verify if the current Website is running on a Development Environment 5 */ 6 export const IS_DEVELOPMENT = process.env.NODE_ENV === 'development'; ··· 108 */ 109 export const MD_EXTENSION_REGEX = /((\/)?(index))?\.mdx?$/i; 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 + 125 /*** 126 * This is a list of all external links that are used on website sitemap. 127 * @see https://github.com/nodejs/nodejs.org/issues/5813 for more context ··· 135 'https://trademark-list.openjsf.org/', 136 'https://www.linuxfoundation.org/cookies', 137 ];
+29 -49
next.dynamic.constants.mjs
··· 1 'use strict'; 2 3 - import { BASE_PATH, BASE_URL, CURRENT_YEAR } from './next.constants.mjs'; 4 import { siteConfig } from './next.json.mjs'; 5 import { defaultLocale } from './next.locales.mjs'; 6 ··· 10 * 11 * @type {((route: import('./types').RouteSegment) => boolean)[]} A list of Ignored Routes by Regular Expressions 12 */ 13 - export const STATIC_ROUTES_IGNORES = [ 14 - // Ignore the 404 route on Static Generation 15 ({ pathname }) => pathname === '404', 16 - // This is used to ignore is used to ignore all blog routes except for the English language 17 ({ locale, pathname }) => 18 locale !== defaultLocale.code && /^blog\//.test(pathname), 19 - // This is used to ignore the blog/pagination meta route 20 - // @deprecated remove with website redesign 21 - ({ pathname }) => /^blog\/pagination/.test(pathname), 22 - ]; 23 - 24 - /** 25 - * This is a list of all dynamic routes or pages from the Website that we do not 26 - * want to allow to be dynamically access by our Dynamic Route Engine 27 - * 28 - * @type {RegExp[]} A list of Ignored Routes by Regular Expressions 29 - * @deprecated remove with website redesign 30 - */ 31 - export const DYNAMIC_ROUTES_IGNORES = [ 32 - // This is used to ignore the blog/pagination route 33 - /^blog\/pagination/, 34 - ]; 35 - 36 - /** 37 - * This is a list of all static routes that we want to rewrite their pathnames 38 - * into something else. This is useful when you want to have the current pathname in the route 39 - * but replace the actual Markdown file that is being loaded by the Dynamic Route to something else 40 - * 41 - * @type {[RegexExp, (pathname: string) => string][]} 42 - * @deprecated remove with website redesign 43 - */ 44 - export const DYNAMIC_ROUTES_REWRITES = [ 45 - [/^blog\/year-/, () => 'blog/pagination'], 46 ]; 47 48 /** 49 - * This is a constant that should be used during runtime by (`getStaticPaths`) on `pages/[...path].tsx` 50 - * 51 - * This function is used to provide an extra set of routes that are not provided by `next.dynamic.mjs` 52 - * static route discovery. This can happen when we have dynamic routes that **must** be provided 53 - * within the static export (static build) of the website. This constant usually would be used along 54 - * with a matching pathname on `DYNAMIC_ROUTES_REWRITES`. 55 * 56 - * @type {string[]} A list of all the Dynamic Routes that are generated by the Website 57 - * @deprecated remove with website redesign 58 */ 59 - export const DYNAMIC_GENERATED_ROUTES = [ 60 - ...Array.from( 61 - // Statically generate a List of Years from Current Year 62 - // til 2011 which is the oldest year with blog posts 63 - { length: CURRENT_YEAR - 2011 }, 64 - (_, i) => CURRENT_YEAR - i 65 - ).map(year => `blog/year-${year}`), 66 - ]; 67 68 /** 69 * This is the default Next.js Page Metadata for all pages 70 * 71 * @type {import('next').Metadata} 72 */ 73 - export const DEFAULT_METADATA = { 74 metadataBase: new URL(`${BASE_URL}${BASE_PATH}`), 75 title: siteConfig.title, 76 description: siteConfig.description, ··· 100 * 101 * @return {import('next').Viewport} 102 */ 103 - export const DEFAULT_VIEWPORT = { 104 themeColor: siteConfig.accentColor, 105 width: 'device-width', 106 initialScale: 1,
··· 1 'use strict'; 2 3 + import { 4 + provideBlogCategories, 5 + provideBlogPosts, 6 + } from './next-data/providers/blogData'; 7 + import { BASE_PATH, BASE_URL } from './next.constants.mjs'; 8 import { siteConfig } from './next.json.mjs'; 9 import { defaultLocale } from './next.locales.mjs'; 10 ··· 14 * 15 * @type {((route: import('./types').RouteSegment) => boolean)[]} A list of Ignored Routes by Regular Expressions 16 */ 17 + export const IGNORED_ROUTES = [ 18 + // This is used to ignore the 404 route for the static generation (/404) 19 ({ pathname }) => pathname === '404', 20 + // This is used to ignore all blog routes except for the English language 21 ({ locale, pathname }) => 22 locale !== defaultLocale.code && /^blog\//.test(pathname), 23 ]; 24 25 /** 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 29 * 30 + * @type {Map<string, import('./types').Layouts>} A Map of pathname and Layout Name 31 */ 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 + ]); 47 48 /** 49 * This is the default Next.js Page Metadata for all pages 50 * 51 * @type {import('next').Metadata} 52 */ 53 + export const PAGE_METADATA = { 54 metadataBase: new URL(`${BASE_URL}${BASE_PATH}`), 55 title: siteConfig.title, 56 description: siteConfig.description, ··· 80 * 81 * @return {import('next').Viewport} 82 */ 83 + export const PAGE_VIEWPORT = { 84 themeColor: siteConfig.accentColor, 85 width: 'device-width', 86 initialScale: 1,
+11 -30
next.dynamic.mjs
··· 8 import { cache } from 'react'; 9 import { VFile } from 'vfile'; 10 11 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, 23 } from './next.dynamic.constants.mjs'; 24 import { getMarkdownFiles } from './next.helpers.mjs'; 25 import { siteConfig } from './next.json.mjs'; ··· 32 // This is a small utility that allows us to quickly separate locale from the remaning pathname 33 const getPathname = (path = []) => path.join('/'); 34 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 // This maps a pathname into an actual route object that can be used 48 // we use a platform-specific separator to split the pathname 49 // since we're using filepaths here and not URL paths ··· 79 ); 80 81 websitePages.forEach(filename => { 82 - let pathname = filename.replace(MD_EXTENSION_REGEX, ''); 83 84 if (pathname.length > 1 && pathname.endsWith(sep)) { 85 pathname = pathname.substring(0, pathname.length - 1); ··· 100 */ 101 const getRoutesByLanguage = async (locale = defaultLocale.code) => { 102 const shouldIgnoreStaticRoute = pathname => 103 - STATIC_ROUTES_IGNORES.every(e => !e({ pathname, locale })); 104 105 return [...pathnameToFilename.keys()] 106 .filter(shouldIgnoreStaticRoute) 107 - .concat(DYNAMIC_GENERATED_ROUTES); 108 }; 109 110 /** ··· 207 * @returns {import('next').Metadata} 208 */ 209 const _getPageMetadata = async (locale = defaultLocale.code, path = '') => { 210 - const pageMetadata = { ...DEFAULT_METADATA }; 211 212 const { source = '' } = await getMarkdownFile(locale, path); 213 214 const { data } = matter(source); 215 216 pageMetadata.title = data.title 217 - ? `${data.title} | ${siteConfig.title}` 218 : siteConfig.title; 219 220 pageMetadata.twitter.title = pageMetadata.title; ··· 246 247 return { 248 mapPathToRoute, 249 - shouldIgnoreRoute, 250 getPathname, 251 - getRouteRewrite, 252 getRoutesByLanguage, 253 getMDXContent, 254 getMarkdownFile,
··· 8 import { cache } from 'react'; 9 import { VFile } from 'vfile'; 10 11 + import { BASE_URL, BASE_PATH, IS_DEVELOPMENT } from './next.constants.mjs'; 12 import { 13 + IGNORED_ROUTES, 14 + DYNAMIC_ROUTES, 15 + PAGE_METADATA, 16 } from './next.dynamic.constants.mjs'; 17 import { getMarkdownFiles } from './next.helpers.mjs'; 18 import { siteConfig } from './next.json.mjs'; ··· 25 // This is a small utility that allows us to quickly separate locale from the remaning pathname 26 const getPathname = (path = []) => path.join('/'); 27 28 // This maps a pathname into an actual route object that can be used 29 // we use a platform-specific separator to split the pathname 30 // since we're using filepaths here and not URL paths ··· 60 ); 61 62 websitePages.forEach(filename => { 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, ''); 66 67 if (pathname.length > 1 && pathname.endsWith(sep)) { 68 pathname = pathname.substring(0, pathname.length - 1); ··· 83 */ 84 const getRoutesByLanguage = async (locale = defaultLocale.code) => { 85 const shouldIgnoreStaticRoute = pathname => 86 + IGNORED_ROUTES.every(e => !e({ pathname, locale })); 87 88 return [...pathnameToFilename.keys()] 89 .filter(shouldIgnoreStaticRoute) 90 + .concat([...DYNAMIC_ROUTES.keys()]); 91 }; 92 93 /** ··· 190 * @returns {import('next').Metadata} 191 */ 192 const _getPageMetadata = async (locale = defaultLocale.code, path = '') => { 193 + const pageMetadata = { ...PAGE_METADATA }; 194 195 const { source = '' } = await getMarkdownFile(locale, path); 196 197 const { data } = matter(source); 198 199 pageMetadata.title = data.title 200 + ? `${siteConfig.title} — ${data.title}` 201 : siteConfig.title; 202 203 pageMetadata.twitter.title = pageMetadata.title; ··· 229 230 return { 231 mapPathToRoute, 232 getPathname, 233 getRoutesByLanguage, 234 getMDXContent, 235 getMarkdownFile,
+2 -1
next.mdx.compiler.mjs
··· 19 * @returns {Promise<{ 20 * MDXContent: import('mdx/types').MDXContent; 21 * headings: import('@vcarl/remark-headings').Heading[]; 22 - * frontmatter: Record<string, any>, readingTime: import('reading-time').ReadTimeResults 23 * }>} 24 */ 25 export async function compileMDX(source, fileExtension) {
··· 19 * @returns {Promise<{ 20 * MDXContent: import('mdx/types').MDXContent; 21 * headings: import('@vcarl/remark-headings').Heading[]; 22 + * frontmatter: Record<string, any>; 23 + * readingTime: import('reading-time').ReadTimeResults; 24 * }>} 25 */ 26 export async function compileMDX(source, fileExtension) {
+50 -96
next.mdx.shiki.mjs
··· 48 * @return {boolean} - True when it is a valid code element, false otherwise. 49 */ 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 - })); 87 } 88 89 export default function rehypeShikiji() { 90 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 = []; 108 109 - let defaultTab = '0'; 110 111 - visit(slicedTree, 'element', node => { 112 - const codeElement = node.children[0]; 113 114 - const displayName = getMetaParameter( 115 - codeElement.data?.meta, 116 - 'displayName' 117 - ); 118 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>.*)/); 123 124 - languages.push(matches?.groups.language ?? 'text'); 125 - } 126 127 - // Map the display names of each variant for the CodeTab 128 - displayNames.push(displayName?.replaceAll('|', '') ?? ''); 129 - codeTabsChildren.push(node); 130 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 - ); 137 138 - if (specificActive === 'true') { 139 - defaultTab = String(codeTabsChildren.length - 1); 140 - } 141 142 - // Prevent visiting the code block children 143 - return SKIP; 144 - }); 145 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 - }; 153 154 - const deleteCount = compensatedRange.end - compensatedRange.start + 1; 155 156 - // Replace the sequential code boxes with a code tabs element 157 - children.splice(compensatedRange.start, deleteCount, { 158 type: 'element', 159 tagName: 'CodeTabs', 160 children: codeTabsChildren, ··· 163 displayNames: displayNames.join('|'), 164 defaultTab, 165 }, 166 - }); 167 - } 168 169 - // Update the tree with the transformed children 170 - Object.assign(tree, { children: children }); 171 - } 172 173 visit(tree, 'element', (node, index, parent) => { 174 // We only want to process <pre>...</pre> elements
··· 48 * @return {boolean} - True when it is a valid code element, false otherwise. 49 */ 50 function isCodeBlock(node) { 51 + return Boolean( 52 + node?.tagName === 'pre' && node?.children[0].tagName === 'code' 53 + ); 54 } 55 56 export default function rehypeShikiji() { 57 return async function (tree) { 58 + visit(tree, 'element', (_, index, parent) => { 59 + const languages = []; 60 + const displayNames = []; 61 + const codeTabsChildren = []; 62 63 + let defaultTab = '0'; 64 + let currentIndex = index; 65 66 + while (isCodeBlock(parent?.children[currentIndex])) { 67 + const codeElement = parent?.children[currentIndex].children[0]; 68 69 + const displayName = getMetaParameter( 70 + codeElement.data?.meta, 71 + 'displayName' 72 + ); 73 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>.*)/); 78 79 + languages.push(matches?.groups.language ?? 'text'); 80 + } 81 82 + // Map the display names of each variant for the CodeTab 83 + displayNames.push(displayName?.replaceAll('|', '') ?? ''); 84 85 + codeTabsChildren.push(parent?.children[currentIndex]); 86 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 + ); 93 94 + if (specificActive === 'true') { 95 + defaultTab = String(codeTabsChildren.length - 1); 96 + } 97 98 + const nextNode = parent?.children[currentIndex + 1]; 99 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 + } 104 105 + if (codeTabsChildren.length >= 2) { 106 + const codeTabElement = { 107 type: 'element', 108 tagName: 'CodeTabs', 109 children: codeTabsChildren, ··· 112 displayNames: displayNames.join('|'), 113 defaultTab, 114 }, 115 + }; 116 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 + }); 126 127 visit(tree, 'element', (node, index, parent) => { 128 // We only want to process <pre>...</pre> elements
+4 -3
next.mdx.use.mjs
··· 3 import Blockquote from './components/Common/Blockquote'; 4 import Button from './components/Common/Button'; 5 import DownloadButton from './components/Downloads/DownloadButton'; 6 import DownloadReleasesTable from './components/Downloads/DownloadReleasesTable'; 7 import HomeDownloadButton from './components/Home/HomeDownloadButton'; 8 import Link from './components/Link'; ··· 33 CodeTabs: MDXCodeTabs, 34 // Renders a Download Button 35 DownloadButton: DownloadButton, 36 // Renders a Button Component for `button` tags 37 Button: Button, 38 }; ··· 51 ? Blockquote 52 : ({ children }) => <div className="highlight-box">{children}</div>, 53 // Renders a CodeBox Component for `pre` tags 54 - pre: ({ children, ...props }) => ( 55 - <MDXCodeBox {...props}>{children}</MDXCodeBox> 56 - ), 57 };
··· 3 import Blockquote from './components/Common/Blockquote'; 4 import Button from './components/Common/Button'; 5 import DownloadButton from './components/Downloads/DownloadButton'; 6 + import DownloadLink from './components/Downloads/DownloadLink'; 7 import DownloadReleasesTable from './components/Downloads/DownloadReleasesTable'; 8 import HomeDownloadButton from './components/Home/HomeDownloadButton'; 9 import Link from './components/Link'; ··· 34 CodeTabs: MDXCodeTabs, 35 // Renders a Download Button 36 DownloadButton: DownloadButton, 37 + // Renders a Download Link 38 + DownloadLink: DownloadLink, 39 // Renders a Button Component for `button` tags 40 Button: Button, 41 }; ··· 54 ? Blockquote 55 : ({ children }) => <div className="highlight-box">{children}</div>, 56 // Renders a CodeBox Component for `pre` tags 57 + pre: MDXCodeBox, 58 };
+9
package-lock.json
··· 19 "@radix-ui/react-toast": "^1.1.5", 20 "@savvywombat/tailwindcss-grid-areas": "~3.1.0", 21 "@sentry/nextjs": "~7.86.0", 22 "@types/node": "20.10.6", 23 "@vcarl/remark-headings": "~0.1.0", 24 "@vercel/analytics": "~1.1.1", ··· 6957 "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", 6958 "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", 6959 "dev": true 6960 }, 6961 "node_modules/@testing-library/dom": { 6962 "version": "9.3.3",
··· 19 "@radix-ui/react-toast": "^1.1.5", 20 "@savvywombat/tailwindcss-grid-areas": "~3.1.0", 21 "@sentry/nextjs": "~7.86.0", 22 + "@tailwindcss/container-queries": "~0.1.1", 23 "@types/node": "20.10.6", 24 "@vcarl/remark-headings": "~0.1.0", 25 "@vercel/analytics": "~1.1.1", ··· 6958 "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", 6959 "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", 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 + } 6969 }, 6970 "node_modules/@testing-library/dom": { 6971 "version": "9.3.3",
+1 -5
package.json
··· 50 "@radix-ui/react-toast": "^1.1.5", 51 "@savvywombat/tailwindcss-grid-areas": "~3.1.0", 52 "@sentry/nextjs": "~7.86.0", 53 "@types/node": "20.10.6", 54 "@vcarl/remark-headings": "~0.1.0", 55 "@vercel/analytics": "~1.1.1", ··· 123 "stylelint-order": "6.0.4", 124 "stylelint-selector-bem-pattern": "3.0.1", 125 "user-agent-data-types": "0.4.2" 126 - }, 127 - "overrides": { 128 - "stylelint-selector-bem-pattern": { 129 - "stylelint": "16.1.0" 130 - } 131 } 132 }
··· 50 "@radix-ui/react-toast": "^1.1.5", 51 "@savvywombat/tailwindcss-grid-areas": "~3.1.0", 52 "@sentry/nextjs": "~7.86.0", 53 + "@tailwindcss/container-queries": "~0.1.1", 54 "@types/node": "20.10.6", 55 "@vcarl/remark-headings": "~0.1.0", 56 "@vercel/analytics": "~1.1.1", ··· 124 "stylelint-order": "6.0.4", 125 "stylelint-selector-bem-pattern": "3.0.1", 126 "user-agent-data-types": "0.4.2" 127 } 128 }
-4
pages/en/blog/advisory-board/index.md
··· 1 - --- 2 - title: Advisory Board 3 - layout: blog-category.hbs 4 - ---
···
-4
pages/en/blog/announcements/index.md
··· 1 - --- 2 - title: Announcements 3 - layout: blog-category.hbs 4 - ---
···
-4
pages/en/blog/community/index.md
··· 1 - --- 2 - title: Community 3 - layout: blog-category.hbs 4 - ---
···
-4
pages/en/blog/feature/index.md
··· 1 - --- 2 - title: Features 3 - layout: blog-category.hbs 4 - ---
···
-4
pages/en/blog/module/index.md
··· 1 - --- 2 - title: Modules 3 - layout: blog-category.hbs 4 - ---
···
-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.
···
-4
pages/en/blog/npm/index.md
··· 1 - --- 2 - title: NPM 3 - layout: blog-category.hbs 4 - ---
···
-5
pages/en/blog/pagination.md
··· 1 - --- 2 - title: Blog from 3 - layout: blog-category.hbs 4 - author: The Node.js Project 5 - ---
···
-4
pages/en/blog/release/index.md
··· 1 - --- 2 - title: Releases 3 - layout: blog-category.hbs 4 - ---
···
+1 -1
pages/en/blog/release/v0.6.3.md
··· 8 9 2011.11.25, Version 0.6.3 (stable) 10 11 - - #2083 Land NPM in Node. It is included in packages/installers and installed on `make install`. 12 - #2076 Add logos to windows installer. 13 - #1711 Correctly handle http requests without headers. (Ben Noordhuis, Felix Geisendörfer) 14 - TLS: expose more openssl SSL context options and constants. (Ben Noordhuis)
··· 8 9 2011.11.25, Version 0.6.3 (stable) 10 11 + - #2083 Land npm in Node. It is included in packages/installers and installed on `make install`. 12 - #2076 Add logos to windows installer. 13 - #1711 Correctly handle http requests without headers. (Ben Noordhuis, Felix Geisendörfer) 14 - TLS: expose more openssl SSL context options and constants. (Ben Noordhuis)
-4
pages/en/blog/uncategorized/index.md
··· 1 - --- 2 - title: Uncategorized 3 - layout: blog-category.hbs 4 - ---
···
-4
pages/en/blog/video/index.md
··· 1 - --- 2 - title: Videos 3 - layout: blog-category.hbs 4 - ---
···
-4
pages/en/blog/vulnerability/index.md
··· 1 - --- 2 - title: Vulnerabilities 3 - layout: blog-category.hbs 4 - ---
···
-4
pages/en/blog/weekly-updates/index.md
··· 1 - --- 2 - title: Weekly Updates 3 - layout: blog-category.hbs 4 - ---
···
+1 -1
pages/en/blog/weekly-updates/weekly-update.2015-04-17.md
··· 31 ## Community Updates 32 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/). 35 - Thoughts of Node.js Foundation on [Medium](https://medium.com/@programmer/thoughts-on-node-foundation-abcf86c72786). 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 - io.js mention on [Oracle's blog](https://blogs.oracle.com/java-platform-group/entry/node_js_and_io_js).
··· 31 ## Community Updates 32 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/). 35 - Thoughts of Node.js Foundation on [Medium](https://medium.com/@programmer/thoughts-on-node-foundation-abcf86c72786). 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 - 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
··· 37 38 - Fedor Indutny opened discussion about removing TLS `newSession` and `resumeSession` event. [iojs/io.js#1462](https://github.com/nodejs/node/issues/1462) 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) 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 43 ## Upcoming Events
··· 37 38 - Fedor Indutny opened discussion about removing TLS `newSession` and `resumeSession` event. [iojs/io.js#1462](https://github.com/nodejs/node/issues/1462) 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) 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 43 ## Upcoming Events
+1 -1
pages/en/download/package-manager.md
··· 1 --- 2 - layout: page.hbs 3 title: Installing Node.js via package manager 4 --- 5
··· 1 --- 2 + layout: docs.hbs 3 title: Installing Node.js via package manager 4 --- 5
+2 -2
pages/en/learn/getting-started/an-introduction-to-the-npm-package-manager.md
··· 1 --- 2 - title: An introduction to the NPM package manager 3 layout: learn.hbs 4 authors: flaviocopes, MylesBorins, LaRuaNa, jgb-solutions, amiller-gh, ahmadawais 5 --- 6 7 - # An introduction to the NPM package manager 8 9 ## Introduction to npm 10
··· 1 --- 2 + title: An introduction to the npm package manager 3 layout: learn.hbs 4 authors: flaviocopes, MylesBorins, LaRuaNa, jgb-solutions, amiller-gh, ahmadawais 5 --- 6 7 + # An introduction to the npm package manager 8 9 ## Introduction to npm 10
-1
pages/en/new-design/.gitkeep
··· 1 - !.gitignore
···
+82 -32
pages/en/new-design/index.mdx
··· 1 --- 2 layout: home.hbs 3 --- 4 ··· 6 <WithBadge section="index" /> 7 8 <div> 9 - # Run JavaScript Everywhere 10 11 - Node.js is a free, open-sourced, cross-platform JavaScript run-time 12 environment that lets developers write command line tools and server-side 13 scripts outside of a browser. 14 15 </div> 16 <div> 17 <WithNodeRelease status={['Active LTS', 'Maintenance LTS']}> 18 {({ release }) => ( 19 - <DownloadButton release={release}>Download Node.js (LTS)</DownloadButton> 20 )} 21 </WithNodeRelease> 22 - 23 - <Button kind="secondary" href="/learn">Get Started</Button> 24 - 25 </div> 26 </section> 27 28 <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 38 39 - # Install Node.js 40 - nvm install --lts 41 42 - # Check that Node is installed 43 - node -v 44 45 - # Check your NPM version 46 - npm -v 47 ``` 48 49 - ```bash 50 - # Install Chocolatey 51 - iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) 52 53 - # Install Node.js 54 - choco install nodejs-lts 55 56 - # Check that Node is installed 57 - node -v 58 59 - # Check your NPM version 60 - npm -v 61 ``` 62 63 - </CodeTabs> 64 - Copy and paste this snippet to install Node.js LTS via a Package Manager 65 </section>
··· 1 --- 2 + title: Run JavaScript Everywhere 3 layout: home.hbs 4 --- 5 ··· 7 <WithBadge section="index" /> 8 9 <div> 10 + <h1 className="special">Run JavaScript Everywhere</h1> 11 12 + Node.js is a free, open-source, cross-platform JavaScript runtime 13 environment that lets developers write command line tools and server-side 14 scripts outside of a browser. 15 16 </div> 17 + 18 <div> 19 <WithNodeRelease status={['Active LTS', 'Maintenance LTS']}> 20 {({ release }) => ( 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> 36 )} 37 </WithNodeRelease> 38 </div> 39 </section> 40 41 <section> 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 + }); 50 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 + ``` 56 57 + ```js displayName="Write Tests" 58 + import assert from 'node:assert'; 59 + import test from 'node:test'; 60 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 + }); 69 ``` 70 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'); 93 94 + readableStream.setEncoding('utf8'); 95 96 + readableStream.on('data', chunk => writableStream.write(chunk)); 97 + ``` 98 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)); 111 ``` 112 113 + </div> 114 + Learn more what Node.js is able to offer with our [Learning materials](/learn). 115 </section>
+13 -20
providers/matterProvider.tsx
··· 1 'use client'; 2 3 - import type { Heading } from '@vcarl/remark-headings'; 4 import { createContext } from 'react'; 5 import type { FC, PropsWithChildren } from 'react'; 6 - import type { ReadTimeResults } from 'reading-time'; 7 8 - import type { LegacyFrontMatter } from '@/types'; 9 10 - type MatterContext = { 11 - frontmatter: LegacyFrontMatter; 12 - pathname: string; 13 - headings: Array<Heading>; 14 - readingTime: ReadTimeResults; 15 - filename: string; 16 - }; 17 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>; 27 28 export const MatterProvider: FC<MatterProviderProps> = ({ 29 children, 30 ...data 31 - }) => <MatterContext.Provider value={data}>{children}</MatterContext.Provider>;
··· 1 'use client'; 2 3 import { createContext } from 'react'; 4 import type { FC, PropsWithChildren } from 'react'; 5 6 + import type { ClientSharedServerContext } from '@/types'; 7 + import { assignClientContext } from '@/util/assignClientContext'; 8 9 + export const MatterContext = createContext<ClientSharedServerContext>( 10 + assignClientContext({}) 11 + ); 12 13 + type MatterProviderProps = PropsWithChildren< 14 + Partial<ClientSharedServerContext> 15 + >; 16 17 export const MatterProvider: FC<MatterProviderProps> = ({ 18 children, 19 ...data 20 + }) => ( 21 + <MatterContext.Provider value={assignClientContext(data)}> 22 + {children} 23 + </MatterContext.Provider> 24 + );
+1 -1
sentry.client.config.ts
··· 14 SENTRY_ENABLE, 15 SENTRY_CAPTURE_RATE, 16 SENTRY_TUNNEL, 17 - } from '@/next.constants.mjs'; 18 19 // This creates a custom Sentry Client with minimal integrations 20 export const sentryClient = new BrowserClient({
··· 14 SENTRY_ENABLE, 15 SENTRY_CAPTURE_RATE, 16 SENTRY_TUNNEL, 17 + } from '@/sentry.constants.mjs'; 18 19 // This creates a custom Sentry Client with minimal integrations 20 export const sentryClient = new BrowserClient({
+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
··· 4 SENTRY_CAPTURE_RATE, 5 SENTRY_DSN, 6 SENTRY_ENABLE, 7 - } from '@/next.constants.mjs'; 8 9 init({ 10 // Only run Sentry on Vercel Environment
··· 4 SENTRY_CAPTURE_RATE, 5 SENTRY_DSN, 6 SENTRY_ENABLE, 7 + } from '@/sentry.constants.mjs'; 8 9 init({ 10 // Only run Sentry on Vercel Environment
+1 -1
sentry.server.config.ts
··· 4 SENTRY_CAPTURE_RATE, 5 SENTRY_DSN, 6 SENTRY_ENABLE, 7 - } from '@/next.constants.mjs'; 8 9 init({ 10 // Only run Sentry on Vercel Environment
··· 4 SENTRY_CAPTURE_RATE, 5 SENTRY_DSN, 6 SENTRY_ENABLE, 7 + } from '@/sentry.constants.mjs'; 8 9 init({ 10 // Only run Sentry on Vercel Environment
+2 -1
site.json
··· 12 "rssFeeds": [ 13 { 14 "title": "Node.js Blog", 15 - "file": "blog.xml" 16 }, 17 { 18 "title": "Node.js Blog: Releases",
··· 12 "rssFeeds": [ 13 { 14 "title": "Node.js Blog", 15 + "file": "blog.xml", 16 + "category": "all" 17 }, 18 { 19 "title": "Node.js Blog: Releases",
+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
··· 11 @import 'tailwindcss/utilities'; 12 @import './base.css'; 13 @import './markdown.css';
··· 11 @import 'tailwindcss/utilities'; 12 @import './base.css'; 13 @import './markdown.css'; 14 + @import './effects.css';
+16
styles/new/markdown.css
··· 1 main { 2 hr { 3 @apply w-full 4 border-t ··· 53 dark:text-white; 54 } 55 56 a { 57 @apply text-green-600 58 dark:text-green-400; 59 60 &:hover { 61 @apply text-green-900 62 dark:text-green-300; 63 } 64 } 65
··· 1 main { 2 + @apply flex 3 + w-full 4 + flex-col 5 + gap-6; 6 + 7 hr { 8 @apply w-full 9 border-t ··· 58 dark:text-white; 59 } 60 61 + p { 62 + @apply text-neutral-900 63 + dark:text-white; 64 + } 65 + 66 a { 67 @apply text-green-600 68 + xs:underline 69 dark:text-green-400; 70 71 &:hover { 72 @apply text-green-900 73 dark:text-green-300; 74 + } 75 + 76 + &:has(code) { 77 + @apply xs:decoration-neutral-800 78 + dark:xs:decoration-neutral-200; 79 } 80 } 81
+6 -3
tailwind.config.ts
··· 117 'ibm-plex-mono': ['var(--font-ibm-plex-mono)'], 118 }, 119 extend: { 120 - screens: { xs: { max: '670px' } }, 121 backgroundImage: { 122 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 123 'gradient-subtle': ··· 129 'gradient-subtle-white': 130 'linear-gradient(180deg, theme(colors.white) 0%, theme(colors.white / 80%) 100%)', 131 'gradient-glow-backdrop': 132 - 'radial-gradient(8em circle at calc(100% - 40px) 10px, theme(colors.green.500), transparent 30%)', 133 }, 134 boxShadow: { 135 xs: '0px 1px 2px 0px theme(colors.shadow / 5%)', ··· 141 }, 142 }, 143 darkMode: ['class', '[data-theme="dark"]'], 144 - plugins: [require('@savvywombat/tailwindcss-grid-areas')], 145 } satisfies Config;
··· 117 'ibm-plex-mono': ['var(--font-ibm-plex-mono)'], 118 }, 119 extend: { 120 + screens: { xs: { max: '670px', min: '0px' } }, 121 backgroundImage: { 122 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 123 'gradient-subtle': ··· 129 'gradient-subtle-white': 130 'linear-gradient(180deg, theme(colors.white) 0%, theme(colors.white / 80%) 100%)', 131 'gradient-glow-backdrop': 132 + 'radial-gradient(8em circle at calc(50%) 10px, theme(colors.green.500), transparent 30%)', 133 }, 134 boxShadow: { 135 xs: '0px 1px 2px 0px theme(colors.shadow / 5%)', ··· 141 }, 142 }, 143 darkMode: ['class', '[data-theme="dark"]'], 144 + plugins: [ 145 + require('@savvywombat/tailwindcss-grid-areas'), 146 + require('@tailwindcss/container-queries'), 147 + ], 148 } satisfies Config;
+13 -12
types/blog.ts
··· 1 export interface BlogPost { 2 title: string; 3 author: string; 4 - date: string; 5 - category: string; 6 slug: string; 7 } 8 9 export interface BlogData { 10 posts: Array<BlogPost>; 11 - pagination: Array<number>; 12 categories: Array<string>; 13 } 14 15 - export interface BlogDataRSC { 16 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 }
··· 1 + export type BlogPreviewType = 'announcements' | 'release' | 'vulnerability'; 2 + 3 export interface BlogPost { 4 title: string; 5 author: string; 6 + date: Date; 7 + categories: Array<string>; 8 slug: string; 9 } 10 11 export interface BlogData { 12 posts: Array<BlogPost>; 13 categories: Array<string>; 14 } 15 16 + export interface BlogPagination { 17 + next: number | null; 18 + prev: number | null; 19 + pages: number; 20 + total: number; 21 + } 22 + 23 + export interface BlogPostsRSC { 24 posts: Array<BlogPost>; 25 + pagination: BlogPagination; 26 }
+1 -1
types/features.ts
··· 1 export interface RSSFeed { 2 file: string; 3 title: string; 4 description?: string; 5 - blogCategory?: string; 6 } 7 8 interface WithRange {
··· 1 export interface RSSFeed { 2 file: string; 3 title: string; 4 + blogCategory: string; 5 description?: string; 6 } 7 8 interface WithRange {
+3 -1
types/layouts.ts
··· 4 | 'docs.hbs' 5 | 'home.hbs' 6 | 'learn.hbs' 7 - | 'page.hbs'; 8 9 // @TODO: These are legacy layouts that are going to be replaced with the `nodejs/nodejs.dev` Layouts in the future 10 export type LegacyLayouts =
··· 4 | 'docs.hbs' 5 | 'home.hbs' 6 | 'learn.hbs' 7 + | 'page.hbs' 8 + | 'blog-category.hbs' 9 + | 'blog-post.hbs'; 10 11 // @TODO: These are legacy layouts that are going to be replaced with the `nodejs/nodejs.dev` Layouts in the future 12 export type LegacyLayouts =
+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
···
··· 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 + };