Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pull] main from nodejs:main #304

Merged
merged 1 commit into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@
{
"files": ["**/*.md?(x)"],
"extends": "plugin:mdx/recommended",
"rules": { "react/jsx-no-undef": "off" }
"rules": {
"react/jsx-no-undef": "off",
"@next/next/no-img-element": "off"
}
},
{
"files": ["**/*.{mdx,tsx}"],
Expand Down
43 changes: 22 additions & 21 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,44 @@
import NextImage from 'next/image';
import classNames from 'classnames';
import { NextIntlClientProvider } from 'next-intl';

import { withThemeByDataAttribute } from '@storybook/addon-themes';
import { SiteProvider } from '../providers/siteProvider';
import { LocaleProvider } from '../providers/localeProvider';
import { NotificationProvider } from '../providers/notificationProvider';
import * as constants from './constants';
import { NotificationProvider } from '@/providers/notificationProvider';
import {
OPEN_SANS_FONT,
IBM_PLEX_MONO_FONT,
STORYBOOK_MODES,
STORYBOOK_SIZES,
} from '@/.storybook/constants';
import type { Preview, ReactRenderer } from '@storybook/react';

import englishLocale from '@/i18n/locales/en.json';

import '../styles/new/index.css';

const rootClasses = classNames(
constants.OPEN_SANS_FONT.variable,
constants.IBM_PLEX_MONO_FONT.variable,
OPEN_SANS_FONT.variable,
IBM_PLEX_MONO_FONT.variable,
'font-open-sans'
);

const preview: Preview = {
parameters: {
nextjs: { router: { basePath: '' } },
chromatic: { modes: constants.STORYBOOK_MODES },
viewport: {
defaultViewport: 'large',
viewports: constants.STORYBOOK_SIZES,
},
chromatic: { modes: STORYBOOK_MODES },
viewport: { defaultViewport: 'large', viewports: STORYBOOK_SIZES },
},
// These are extra Storybook Decorators applied to all stories
// that introduce extra functionality such as Theme Switching
// and all the App's Providers (Site, Theme, Locale)
decorators: [
Story => (
<SiteProvider>
<LocaleProvider>
<NotificationProvider viewportClassName="absolute top-0 left-0 list-none">
<div className={rootClasses}>
<Story />
</div>
</NotificationProvider>
</LocaleProvider>
</SiteProvider>
<NextIntlClientProvider locale="en" messages={englishLocale}>
<NotificationProvider viewportClassName="absolute top-0 left-0 list-none">
<div className={rootClasses}>
<Story />
</div>
</NotificationProvider>
</NextIntlClientProvider>
),
withThemeByDataAttribute<ReactRenderer>({
themes: {
Expand Down
17 changes: 8 additions & 9 deletions COLLABORATOR_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ The Website also uses several other Open Source libraries (not limited to) liste
- [Tailwind][] is used as our CSS Framework and the Foundation of our Design System
- [Hero Icons](https://heroicons.com/) is an SVG Icon Library used within our Codebase
- [Radix UI][] is a collection of customizable UI components
- [Shiki][] is a Syntax Highlighter used for our Codeboxes
- A [Rehype Plugin](https://rehype-pretty-code.netlify.app/) is used here for transforming `pre` and `code` tags into Syntax Highlighted Codeboxes
- [Shikiji][] is a Syntax Highlighter used for our Codeboxes
- The syntax highlighting is done within the processing of the Markdown files with the MDX compiler as a Rehype plugin.
- [MDX][] and Markdown are used for structuring the Content of the Website
- [`react-intl`][] is the i18n Library adopted within the Website
- [`next-sitemap`](https://www.npmjs.com/package/next-sitemap) is used for Sitemap and `robots.txt` Generation
Expand Down Expand Up @@ -442,20 +442,19 @@ MDX is an extension on Markdown that allows us to add JSX Components within Mark
Besides that, MDX is also a pluggable parser built on top of `unified` which supports Rehype and Remark Plugins.
MDX is becoming the standard for parsing human-content on React/Next.js-based Applications.

Some of the plugins that we use include:
**Some of the plugins that we use include:**

- `remark-gfm`: Allows us to bring GitHub Flavoured Markdown within MDX
- `remark-headings`: Generates Metadata for Markdown Headings
- This allows us to build the Table of Contents for each Page, for example.
- `rehype-autolink-headings`: Allows us to add Anchor Links to Markdown Headings
- `rehype-slug`: Allows us to add IDs to Markdown Headings
- `rehype-pretty-code`: Allows us to transform `pre` and `code` tags into Syntax Highlighted Codeboxes by using [Shiki][]

#### Syntax Highlighting (Shiki) and Vercel
#### Syntax Highlighting (Shikiji) and Vercel

Since we use Incremental Static Rendering and Serverless Functions, Vercel attempts to simplify the bundled Node.js runtime by removing all unnecessary dependencies.
This means that Shiki's Themes and Languages are not bundled by default.
We use [Shikiji][] which is a refactor of the famous [Shiki](https://github.com/shikijs/shiki) syntax highlighter in ESM. We use it to support our native ESM-nature, and since Shiki is incompatible on serverless environments and Edge functions due of the need of Node's `fs`. Shikiji is definitely a nice port/rewrite of Shiki which supports our needs.

Hence the `shiki.config.mjs` file, where we define our custom set of supported Languages and we bundle them directly by using [Shiki's Grammar Property](https://github.com/shikijs/shiki/blob/main/docs/languages.md#supporting-your-own-languages-with-shiki) which allows us to embed the languages directly.
Shikiji is integrated on our workflow as a Reype Plugin, see the `next.mdx.shiki.mjs` file. We also use the `nord` theme for Shikiji and a subset of the supported languages as defined on the `shiki.config.mjs` file.

### Vercel

Expand Down Expand Up @@ -496,6 +495,6 @@ If you're unfamiliar or curious about something, we recommend opening a Discussi
[MDX]: https://mdxjs.com/
[PostCSS]: https://postcss.org/
[React]: https://react.dev/
[Shiki]: https://github.com/shikijs/shiki
[Shikiji]: https://github.com/antfu/shikiji
[Tailwind]: https://tailwindcss.com/
[Radix UI]: https://www.radix-ui.com/
119 changes: 119 additions & 0 deletions app/[locale]/[[...path]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { notFound } from 'next/navigation';
import { unstable_setRequestLocale } from 'next-intl/server';
import type { FC } from 'react';

import { setClientContext } from '@/client-context';
import { MDXRenderer } from '@/components/mdxRenderer';
import { WithLayout } from '@/components/withLayout';
import { DEFAULT_VIEWPORT, ENABLE_STATIC_EXPORT } from '@/next.constants.mjs';
import { dynamicRouter } from '@/next.dynamic.mjs';
import { availableLocaleCodes, defaultLocale } from '@/next.locales.mjs';
import { MatterProvider } from '@/providers/matterProvider';

type DynamicStaticPaths = { path: string[]; locale: string };
type DynamicParams = { params: DynamicStaticPaths };

// This is the default Viewport Metadata
// @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport
export const viewport = DEFAULT_VIEWPORT;

// This generates each page's HTML Metadata
// @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata
export const generateMetadata = async (c: DynamicParams) => {
const { path = [], locale = defaultLocale.code } = c.params;

const pathname = dynamicRouter.getPathname(path);

// Retrieves and rewriting rule if the pathname matches any rule
const [, rewriteRule] = dynamicRouter.getRouteRewrite(pathname);

return dynamicRouter.getPageMetadata(
locale,
rewriteRule ? rewriteRule(pathname) : pathname
);
};

// This provides all the possible paths that can be generated statically
// + provides all the paths that we support on the Node.js Website
export const generateStaticParams = async () => {
const paths: DynamicStaticPaths[] = [];

// We don't need to compute all possible paths on regular builds
// as we benefit from Next.js's ISR (Incremental Static Regeneration)
if (!ENABLE_STATIC_EXPORT) {
return [];
}

for (const locale of availableLocaleCodes) {
const routesForLanguage = await dynamicRouter.getRoutesByLanguage(locale);

const mappedRoutesWithLocale = routesForLanguage.map(pathname =>
dynamicRouter.mapPathToRoute(locale, pathname)
);

paths.push(...mappedRoutesWithLocale);
}

return paths.sort();
};

// This method parses the current pathname and does any sort of modifications needed on the route
// then it proceeds to retrieve the Markdown file and parse the MDX Content into a React Component
// finally it returns (if the locale and route are valid) the React Component with the relevant context
// and attached context providers for rendering the current page
const getPage: FC<DynamicParams> = async ({ params }) => {
const { path = [], locale = defaultLocale.code } = params;

if (!availableLocaleCodes.includes(locale)) {
// Forces the current locale to be the Default Locale
unstable_setRequestLocale(defaultLocale.code);

return notFound();
}

// Configures the current Locale to be the given Locale of the Request
unstable_setRequestLocale(locale);

const pathname = dynamicRouter.getPathname(path);

if (dynamicRouter.shouldIgnoreRoute(pathname)) {
return notFound();
}

// Retrieves and rewriting rule if the pathname matches any rule
const [, rewriteRule] = dynamicRouter.getRouteRewrite(pathname);

// We retrieve the source of the Markdown file by doing an educated guess
// of what possible files could be the source of the page, since the extension
// context is lost from `getStaticProps` as a limitation of Next.js itself
const { source, filename } = await dynamicRouter.getMarkdownFile(
locale,
rewriteRule ? rewriteRule(pathname) : pathname
);

if (source.length && filename.length) {
// This parses the source Markdown content and returns a React Component and
// relevant context from the Markdown File
const { MDXContent, frontmatter, headings } =
await dynamicRouter.getMDXContent(source, filename);

// Defines a shared Server Context for the Client-Side
// That is shared for all pages under the dynamic router
setClientContext({ frontmatter, headings, pathname });

return (
<MatterProvider matter={frontmatter} headings={headings}>
<WithLayout layout={frontmatter.layout}>
<MDXRenderer Component={MDXContent} />
</WithLayout>
</MatterProvider>
);
}

return notFound();
};

// Enforce that all these routes are compatible with SSR
export const dynamic = 'error';

export default getPage;
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';

import { generateWebsiteFeeds } from '@/next.data.mjs';
import { blogData } from '@/next.json.mjs';
import { defaultLocale } from '@/next.locales.mjs';

// loads all the data from the blog-posts-data.json file
const websiteFeeds = generateWebsiteFeeds(blogData);
Expand All @@ -27,7 +28,7 @@ export const GET = (_: Request, { params }: StaticParams) => {
// Note that differently from the App Router these don't get built at the build time
// only if the export is already set for static export
export const generateStaticParams = () =>
[...websiteFeeds.keys()].map(feed => ({ feed }));
[...websiteFeeds.keys()].map(feed => ({ feed, locale: defaultLocale.code }));

// Enforces that this route is used as static rendering
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
Expand Down
3 changes: 3 additions & 0 deletions app/[locale]/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import NotFound from '@/app/not-found';

export default NotFound;
42 changes: 42 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Analytics } from '@vercel/analytics/react';
import { Source_Sans_3 } from 'next/font/google';
import { useLocale } from 'next-intl';
import type { FC, PropsWithChildren } from 'react';

import BaseLayout from '@/layouts/BaseLayout';
import { VERCEL_ENV } from '@/next.constants.mjs';
import { availableLocalesMap, defaultLocale } from '@/next.locales.mjs';
import { LocaleProvider } from '@/providers/localeProvider';
import { ThemeProvider } from '@/providers/themeProvider';

import '@/styles/old/index.css';

const sourceSans = Source_Sans_3({
weight: ['400', '600'],
display: 'fallback',
subsets: ['latin'],
});

const RootLayout: FC<PropsWithChildren> = ({ children }) => {
const locale = useLocale();

const { langDir, hrefLang } = availableLocalesMap[locale] || defaultLocale;

return (
<html className={sourceSans.className} dir={langDir} lang={hrefLang}>
<body>
<LocaleProvider>
<ThemeProvider>
<BaseLayout>{children}</BaseLayout>
</ThemeProvider>
</LocaleProvider>

{VERCEL_ENV && <Analytics />}

<a rel="me" href="https://social.lfx.dev/@nodejs" />
</body>
</html>
);
};

export default RootLayout;
18 changes: 18 additions & 0 deletions app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useTranslations } from 'next-intl';
import type { FC } from 'react';

const NotFound: FC = () => {
const t = useTranslations();

return (
<div className="container">
<h2>{t('pages.404.title')}</h2>
<h3>{t('pages.404.description')}</h3>
</div>
);
};

// This is a fallback Not Found Page that in theory should never be requested
export const dynamic = 'force-dynamic';

export default NotFound;
26 changes: 10 additions & 16 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,31 @@
import type { MetadataRoute } from 'next';

import {
STATIC_ROUTES_IGNORES,
DYNAMIC_GENERATED_ROUTES,
BASE_PATH,
BASE_URL,
EXTERNAL_LINKS_SITEMAP,
} from '@/next.constants.mjs';
import { allPaths } from '@/next.dynamic.mjs';
import { defaultLocale } from '@/next.locales.mjs';
import { dynamicRouter } from '@/next.dynamic.mjs';
import { availableLocaleCodes } from '@/next.locales.mjs';

// This is the combination of the Application Base URL and Base PATH
const baseUrlAndPath = `${BASE_URL}${BASE_PATH}`;

// This allows us to generate a `sitemap.xml` file dynamically based on the needs of the Node.js Website
// Next.js Sitemap Generation doesn't support `alternate` refs yet
// @see https://github.com/vercel/next.js/discussions/55646
const sitemap = (): MetadataRoute.Sitemap => {
// Retrieves all the dynamic generated paths
const dynamicRoutes = DYNAMIC_GENERATED_ROUTES();
const sitemap = async (): Promise<MetadataRoute.Sitemap> => {
const paths: string[] = [];

// Retrieves all the static paths for the default locale (English)
// and filter out the routes that should be ignored
const staticPaths = [...allPaths.get(defaultLocale.code)!]
.filter(route => STATIC_ROUTES_IGNORES.every(e => !e(route)))
.map(route => route.routeWithLocale);
for (const locale of availableLocaleCodes) {
const routesForLanguage = await dynamicRouter.getRoutesByLanguage(locale);

paths.push(...routesForLanguage.map(route => `${locale}/${route}`));
}

// The current date of this request
const currentDate = new Date().toISOString();

const appRoutes = [...dynamicRoutes, ...staticPaths]
.sort()
.map(route => `${baseUrlAndPath}/${route}`);
const appRoutes = paths.sort().map(route => `${baseUrlAndPath}/${route}`);

return [...appRoutes, ...EXTERNAL_LINKS_SITEMAP].map(route => ({
url: route,
Expand Down
24 changes: 24 additions & 0 deletions client-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { cache } from 'react';

import type { ClientSharedServerContext } from './types';

// This allows us to have Server-Side Context's of the shared "contextual" data
// which includes the frontmatter, the current pathname from the dynamic segments
// and the current headings of the current markdown context
export const getClientContext = cache(() => {
const serverSharedContext: ClientSharedServerContext = {
frontmatter: {},
pathname: '',
headings: [],
};

return serverSharedContext;
});

// This is used by the dynamic router to define on the request
// the current set of information we use (shared)
export const setClientContext = (data: ClientSharedServerContext) => {
getClientContext().frontmatter = data.frontmatter;
getClientContext().pathname = data.pathname;
getClientContext().headings = data.headings;
};
Loading
Loading