Skip to content

Commit b511479

Browse files
araujoguiovflowd
andauthored
meta: adopt next-intl and app router (nodejs#6092)
* meta: adopt next-intl and app router code: kept working on adoption of i18n feat: app router transition meta: renamed sections to containers due to fs casing fix: force static fix: minor fixes of build and caching feat: metadata of each page fix: tests and stuff fix: tiny type fix chore: updated link to guide chore: intl on stories chore: no usage of assert refactor: some more cleanups on codebase meta: some improvements and fixes refactor: more code review changes refactor: more standardisation next-intl chore: minor changes to 404 refactor: code cleanup and reusability fix: fixed tests and imports chore: minor fixes and test updates fix: sitemap generation and 404 ignore chore: cleanup activelocalizedlink chore: remove legacy sto-top chore: more cleanups chore: more cleanups chore: use right import chore: optimise generation of blog meta chore: small cleanup chore: updated collaborator guide meta: provide server and client version of hooks fix: tests imports fix: styles of blogcard * fix: fix eslint rules --------- Co-authored-by: Claudio Wunder <[email protected]>
1 parent 10fb878 commit b511479

File tree

188 files changed

+4831
-3980
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

188 files changed

+4831
-3980
lines changed

.eslintrc.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@
5959
{
6060
"files": ["**/*.md?(x)"],
6161
"extends": "plugin:mdx/recommended",
62-
"rules": { "react/jsx-no-undef": "off" }
62+
"rules": {
63+
"react/jsx-no-undef": "off",
64+
"@next/next/no-img-element": "off"
65+
}
6366
},
6467
{
6568
"files": ["**/*.{mdx,tsx}"],

.storybook/preview.tsx

+22-21
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,44 @@
1-
import NextImage from 'next/image';
21
import classNames from 'classnames';
2+
import { NextIntlClientProvider } from 'next-intl';
3+
34
import { withThemeByDataAttribute } from '@storybook/addon-themes';
4-
import { SiteProvider } from '../providers/siteProvider';
5-
import { LocaleProvider } from '../providers/localeProvider';
6-
import { NotificationProvider } from '../providers/notificationProvider';
7-
import * as constants from './constants';
5+
import { NotificationProvider } from '@/providers/notificationProvider';
6+
import {
7+
OPEN_SANS_FONT,
8+
IBM_PLEX_MONO_FONT,
9+
STORYBOOK_MODES,
10+
STORYBOOK_SIZES,
11+
} from '@/.storybook/constants';
812
import type { Preview, ReactRenderer } from '@storybook/react';
913

14+
import englishLocale from '@/i18n/locales/en.json';
15+
1016
import '../styles/new/index.css';
1117

1218
const rootClasses = classNames(
13-
constants.OPEN_SANS_FONT.variable,
14-
constants.IBM_PLEX_MONO_FONT.variable,
19+
OPEN_SANS_FONT.variable,
20+
IBM_PLEX_MONO_FONT.variable,
1521
'font-open-sans'
1622
);
1723

1824
const preview: Preview = {
1925
parameters: {
2026
nextjs: { router: { basePath: '' } },
21-
chromatic: { modes: constants.STORYBOOK_MODES },
22-
viewport: {
23-
defaultViewport: 'large',
24-
viewports: constants.STORYBOOK_SIZES,
25-
},
27+
chromatic: { modes: STORYBOOK_MODES },
28+
viewport: { defaultViewport: 'large', viewports: STORYBOOK_SIZES },
2629
},
2730
// These are extra Storybook Decorators applied to all stories
2831
// that introduce extra functionality such as Theme Switching
2932
// and all the App's Providers (Site, Theme, Locale)
3033
decorators: [
3134
Story => (
32-
<SiteProvider>
33-
<LocaleProvider>
34-
<NotificationProvider viewportClassName="absolute top-0 left-0 list-none">
35-
<div className={rootClasses}>
36-
<Story />
37-
</div>
38-
</NotificationProvider>
39-
</LocaleProvider>
40-
</SiteProvider>
35+
<NextIntlClientProvider locale="en" messages={englishLocale}>
36+
<NotificationProvider viewportClassName="absolute top-0 left-0 list-none">
37+
<div className={rootClasses}>
38+
<Story />
39+
</div>
40+
</NotificationProvider>
41+
</NextIntlClientProvider>
4142
),
4243
withThemeByDataAttribute<ReactRenderer>({
4344
themes: {

COLLABORATOR_GUIDE.md

+8-9
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ The Website also uses several other Open Source libraries (not limited to) liste
8080
- [Tailwind][] is used as our CSS Framework and the Foundation of our Design System
8181
- [Hero Icons](https://heroicons.com/) is an SVG Icon Library used within our Codebase
8282
- [Radix UI][] is a collection of customizable UI components
83-
- [Shiki][] is a Syntax Highlighter used for our Codeboxes
84-
- A [Rehype Plugin](https://rehype-pretty-code.netlify.app/) is used here for transforming `pre` and `code` tags into Syntax Highlighted Codeboxes
83+
- [Shikiji][] is a Syntax Highlighter used for our Codeboxes
84+
- The syntax highlighting is done within the processing of the Markdown files with the MDX compiler as a Rehype plugin.
8585
- [MDX][] and Markdown are used for structuring the Content of the Website
8686
- [`react-intl`][] is the i18n Library adopted within the Website
8787
- [`next-sitemap`](https://www.npmjs.com/package/next-sitemap) is used for Sitemap and `robots.txt` Generation
@@ -442,20 +442,19 @@ MDX is an extension on Markdown that allows us to add JSX Components within Mark
442442
Besides that, MDX is also a pluggable parser built on top of `unified` which supports Rehype and Remark Plugins.
443443
MDX is becoming the standard for parsing human-content on React/Next.js-based Applications.
444444

445-
Some of the plugins that we use include:
445+
**Some of the plugins that we use include:**
446446

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

453-
#### Syntax Highlighting (Shiki) and Vercel
453+
#### Syntax Highlighting (Shikiji) and Vercel
454454

455-
Since we use Incremental Static Rendering and Serverless Functions, Vercel attempts to simplify the bundled Node.js runtime by removing all unnecessary dependencies.
456-
This means that Shiki's Themes and Languages are not bundled by default.
455+
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.
457456

458-
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.
457+
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.
459458

460459
### Vercel
461460

@@ -496,6 +495,6 @@ If you're unfamiliar or curious about something, we recommend opening a Discussi
496495
[MDX]: https://mdxjs.com/
497496
[PostCSS]: https://postcss.org/
498497
[React]: https://react.dev/
499-
[Shiki]: https://github.com/shikijs/shiki
498+
[Shikiji]: https://github.com/antfu/shikiji
500499
[Tailwind]: https://tailwindcss.com/
501500
[Radix UI]: https://www.radix-ui.com/

app/[locale]/[[...path]]/page.tsx

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { notFound } from 'next/navigation';
2+
import { unstable_setRequestLocale } from 'next-intl/server';
3+
import type { FC } from 'react';
4+
5+
import { setClientContext } from '@/client-context';
6+
import { MDXRenderer } from '@/components/mdxRenderer';
7+
import { WithLayout } from '@/components/withLayout';
8+
import { DEFAULT_VIEWPORT, ENABLE_STATIC_EXPORT } from '@/next.constants.mjs';
9+
import { dynamicRouter } from '@/next.dynamic.mjs';
10+
import { availableLocaleCodes, defaultLocale } from '@/next.locales.mjs';
11+
import { MatterProvider } from '@/providers/matterProvider';
12+
13+
type DynamicStaticPaths = { path: string[]; locale: string };
14+
type DynamicParams = { params: DynamicStaticPaths };
15+
16+
// This is the default Viewport Metadata
17+
// @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport
18+
export const viewport = DEFAULT_VIEWPORT;
19+
20+
// This generates each page's HTML Metadata
21+
// @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata
22+
export const generateMetadata = async (c: DynamicParams) => {
23+
const { path = [], locale = defaultLocale.code } = c.params;
24+
25+
const pathname = dynamicRouter.getPathname(path);
26+
27+
// Retrieves and rewriting rule if the pathname matches any rule
28+
const [, rewriteRule] = dynamicRouter.getRouteRewrite(pathname);
29+
30+
return dynamicRouter.getPageMetadata(
31+
locale,
32+
rewriteRule ? rewriteRule(pathname) : pathname
33+
);
34+
};
35+
36+
// This provides all the possible paths that can be generated statically
37+
// + provides all the paths that we support on the Node.js Website
38+
export const generateStaticParams = async () => {
39+
const paths: DynamicStaticPaths[] = [];
40+
41+
// We don't need to compute all possible paths on regular builds
42+
// as we benefit from Next.js's ISR (Incremental Static Regeneration)
43+
if (!ENABLE_STATIC_EXPORT) {
44+
return [];
45+
}
46+
47+
for (const locale of availableLocaleCodes) {
48+
const routesForLanguage = await dynamicRouter.getRoutesByLanguage(locale);
49+
50+
const mappedRoutesWithLocale = routesForLanguage.map(pathname =>
51+
dynamicRouter.mapPathToRoute(locale, pathname)
52+
);
53+
54+
paths.push(...mappedRoutesWithLocale);
55+
}
56+
57+
return paths.sort();
58+
};
59+
60+
// This method parses the current pathname and does any sort of modifications needed on the route
61+
// then it proceeds to retrieve the Markdown file and parse the MDX Content into a React Component
62+
// finally it returns (if the locale and route are valid) the React Component with the relevant context
63+
// and attached context providers for rendering the current page
64+
const getPage: FC<DynamicParams> = async ({ params }) => {
65+
const { path = [], locale = defaultLocale.code } = params;
66+
67+
if (!availableLocaleCodes.includes(locale)) {
68+
// Forces the current locale to be the Default Locale
69+
unstable_setRequestLocale(defaultLocale.code);
70+
71+
return notFound();
72+
}
73+
74+
// Configures the current Locale to be the given Locale of the Request
75+
unstable_setRequestLocale(locale);
76+
77+
const pathname = dynamicRouter.getPathname(path);
78+
79+
if (dynamicRouter.shouldIgnoreRoute(pathname)) {
80+
return notFound();
81+
}
82+
83+
// Retrieves and rewriting rule if the pathname matches any rule
84+
const [, rewriteRule] = dynamicRouter.getRouteRewrite(pathname);
85+
86+
// We retrieve the source of the Markdown file by doing an educated guess
87+
// of what possible files could be the source of the page, since the extension
88+
// context is lost from `getStaticProps` as a limitation of Next.js itself
89+
const { source, filename } = await dynamicRouter.getMarkdownFile(
90+
locale,
91+
rewriteRule ? rewriteRule(pathname) : pathname
92+
);
93+
94+
if (source.length && filename.length) {
95+
// This parses the source Markdown content and returns a React Component and
96+
// relevant context from the Markdown File
97+
const { MDXContent, frontmatter, headings } =
98+
await dynamicRouter.getMDXContent(source, filename);
99+
100+
// Defines a shared Server Context for the Client-Side
101+
// That is shared for all pages under the dynamic router
102+
setClientContext({ frontmatter, headings, pathname });
103+
104+
return (
105+
<MatterProvider matter={frontmatter} headings={headings}>
106+
<WithLayout layout={frontmatter.layout}>
107+
<MDXRenderer Component={MDXContent} />
108+
</WithLayout>
109+
</MatterProvider>
110+
);
111+
}
112+
113+
return notFound();
114+
};
115+
116+
// Enforce that all these routes are compatible with SSR
117+
export const dynamic = 'error';
118+
119+
export default getPage;

app/en/feed/[feed]/route.ts app/[locale]/feed/[feed]/route.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
22

33
import { generateWebsiteFeeds } from '@/next.data.mjs';
44
import { blogData } from '@/next.json.mjs';
5+
import { defaultLocale } from '@/next.locales.mjs';
56

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

3233
// Enforces that this route is used as static rendering
3334
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic

app/[locale]/not-found.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import NotFound from '@/app/not-found';
2+
3+
export default NotFound;

app/layout.tsx

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Analytics } from '@vercel/analytics/react';
2+
import { Source_Sans_3 } from 'next/font/google';
3+
import { useLocale } from 'next-intl';
4+
import type { FC, PropsWithChildren } from 'react';
5+
6+
import BaseLayout from '@/layouts/BaseLayout';
7+
import { VERCEL_ENV } from '@/next.constants.mjs';
8+
import { availableLocalesMap, defaultLocale } from '@/next.locales.mjs';
9+
import { LocaleProvider } from '@/providers/localeProvider';
10+
import { ThemeProvider } from '@/providers/themeProvider';
11+
12+
import '@/styles/old/index.css';
13+
14+
const sourceSans = Source_Sans_3({
15+
weight: ['400', '600'],
16+
display: 'fallback',
17+
subsets: ['latin'],
18+
});
19+
20+
const RootLayout: FC<PropsWithChildren> = ({ children }) => {
21+
const locale = useLocale();
22+
23+
const { langDir, hrefLang } = availableLocalesMap[locale] || defaultLocale;
24+
25+
return (
26+
<html className={sourceSans.className} dir={langDir} lang={hrefLang}>
27+
<body>
28+
<LocaleProvider>
29+
<ThemeProvider>
30+
<BaseLayout>{children}</BaseLayout>
31+
</ThemeProvider>
32+
</LocaleProvider>
33+
34+
{VERCEL_ENV && <Analytics />}
35+
36+
<a rel="me" href="https://social.lfx.dev/@nodejs" />
37+
</body>
38+
</html>
39+
);
40+
};
41+
42+
export default RootLayout;

app/not-found.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { useTranslations } from 'next-intl';
2+
import type { FC } from 'react';
3+
4+
const NotFound: FC = () => {
5+
const t = useTranslations();
6+
7+
return (
8+
<div className="container">
9+
<h2>{t('pages.404.title')}</h2>
10+
<h3>{t('pages.404.description')}</h3>
11+
</div>
12+
);
13+
};
14+
15+
// This is a fallback Not Found Page that in theory should never be requested
16+
export const dynamic = 'force-dynamic';
17+
18+
export default NotFound;

app/sitemap.ts

+10-16
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,31 @@
11
import type { MetadataRoute } from 'next';
22

33
import {
4-
STATIC_ROUTES_IGNORES,
5-
DYNAMIC_GENERATED_ROUTES,
64
BASE_PATH,
75
BASE_URL,
86
EXTERNAL_LINKS_SITEMAP,
97
} from '@/next.constants.mjs';
10-
import { allPaths } from '@/next.dynamic.mjs';
11-
import { defaultLocale } from '@/next.locales.mjs';
8+
import { dynamicRouter } from '@/next.dynamic.mjs';
9+
import { availableLocaleCodes } from '@/next.locales.mjs';
1210

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

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

23-
// Retrieves all the static paths for the default locale (English)
24-
// and filter out the routes that should be ignored
25-
const staticPaths = [...allPaths.get(defaultLocale.code)!]
26-
.filter(route => STATIC_ROUTES_IGNORES.every(e => !e(route)))
27-
.map(route => route.routeWithLocale);
20+
for (const locale of availableLocaleCodes) {
21+
const routesForLanguage = await dynamicRouter.getRoutesByLanguage(locale);
22+
23+
paths.push(...routesForLanguage.map(route => `${locale}/${route}`));
24+
}
2825

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

32-
const appRoutes = [...dynamicRoutes, ...staticPaths]
33-
.sort()
34-
.map(route => `${baseUrlAndPath}/${route}`);
28+
const appRoutes = paths.sort().map(route => `${baseUrlAndPath}/${route}`);
3529

3630
return [...appRoutes, ...EXTERNAL_LINKS_SITEMAP].map(route => ({
3731
url: route,

client-context.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { cache } from 'react';
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+
};
14+
15+
return serverSharedContext;
16+
});
17+
18+
// This is used by the dynamic router to define on the request
19+
// the current set of information we use (shared)
20+
export const setClientContext = (data: ClientSharedServerContext) => {
21+
getClientContext().frontmatter = data.frontmatter;
22+
getClientContext().pathname = data.pathname;
23+
getClientContext().headings = data.headings;
24+
};

0 commit comments

Comments
 (0)