Skip to content

Commit

Permalink
meta: refactor data generation to server-side only (#6137)
Browse files Browse the repository at this point in the history
* meta: refactor data generation to server-side only

* chore: use import for this small data

* meta: refactor to an inteligent cache system

* chore: some code review

* chore: final code review changes

* meta: updated certain packages
  • Loading branch information
ovflowd authored Nov 24, 2023
1 parent e51a529 commit c463a36
Show file tree
Hide file tree
Showing 47 changed files with 621 additions and 484 deletions.
4 changes: 0 additions & 4 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ node_modules
.swc
build

# Public Files
public/node-releases-data.json
public/blog-posts-data.json

# We don't want to lint/prettify the Coverage Results
coverage
junit.xml
Expand Down
4 changes: 0 additions & 4 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# if the generated files got tracked to this commit we revert them
git reset public/node-releases-data.json
git reset public/blog-posts-data.json

# lint and format staged files
npx lint-staged
2 changes: 0 additions & 2 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ build

# Next.js Generated Files
public/static/documents
public/node-releases-data.json
public/blog-posts-data.json

# Jest
coverage
Expand Down
15 changes: 11 additions & 4 deletions app/[locale]/[[...path]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ 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 { ENABLE_STATIC_EXPORT } from '@/next.constants.mjs';
import { DEFAULT_VIEWPORT } from '@/next.dynamic.constants.mjs';
import { dynamicRouter } from '@/next.dynamic.mjs';
import { availableLocaleCodes, defaultLocale } from '@/next.locales.mjs';
import { MatterProvider } from '@/providers/matterProvider';
Expand All @@ -14,8 +15,8 @@ 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;
// @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function
export const generateViewport = async () => ({ ...DEFAULT_VIEWPORT });

// This generates each page's HTML Metadata
// @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata
Expand Down Expand Up @@ -113,7 +114,13 @@ const getPage: FC<DynamicParams> = async ({ params }) => {
return notFound();
};

// Enforce that all these routes are compatible with SSR
// In this case we want to catch-all possible pages even to this page. This ensures that we use our 404
// and that all pages including existing ones are handled here and provide `next-intl` locale also
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams
export const dynamicParams = true;

// Enforces that this route is used as static rendering
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
export const dynamic = 'error';

export default getPage;
46 changes: 24 additions & 22 deletions app/[locale]/feed/[feed]/route.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,37 @@
import { NextResponse } from 'next/server';

import { generateWebsiteFeeds } from '@/next.data.mjs';
import { blogData } from '@/next.json.mjs';
import provideWebsiteFeeds from '@/next-data/providers/websiteFeeds';
import { siteConfig } 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);
// We only support fetching these pages from the /en/ locale code
const locale = defaultLocale.code;

type StaticParams = { params: { feed: string } };
type StaticParams = { params: { feed: string; locale: string } };

// This is the Route Handler for the `GET` method which handles the request
// for Blog Feeds within the Node.js Website
// for the Node.js Website Blog Feeds (RSS)
// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers
export const GET = (_: Request, { params }: StaticParams) => {
if (params.feed.includes('.xml') && websiteFeeds.has(params.feed)) {
return new NextResponse(websiteFeeds.get(params.feed)?.rss2(), {
headers: { 'Content-Type': 'application/xml' },
});
}

return new NextResponse(null, { status: 404 });
export const GET = async (_: Request, { params }: StaticParams) => {
// Generate the Feed for the given feed type (blog, releases, etc)
const websiteFeed = await provideWebsiteFeeds(params.feed);

return new NextResponse(websiteFeed, {
headers: { 'Content-Type': 'application/xml' },
status: websiteFeed ? 200 : 404,
});
};

// This function generates the static paths that come from the dynamic segments
// `en/feeds/[feed]` and returns an array of all available static paths
// this is useful for static exports, for example.
// 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, locale: defaultLocale.code }));

// Enforces that this route is used as static rendering
// `[locale]/feeds/[feed]` and returns an array of all available static paths
// This is used for ISR static validation and generation
export const generateStaticParams = async () =>
siteConfig.rssFeeds.map(feed => ({ feed: feed.file, locale }));

// Forces that only the paths from `generateStaticParams` are allowed, giving 404 on the contrary
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams
export const dynamicParams = false;

// Enforces that this route is cached and static as much as possible
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
export const dynamic = 'error';
42 changes: 42 additions & 0 deletions app/[locale]/next-data/blog-data/[category]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import provideBlogData from '@/next-data/providers/blogData';
import { defaultLocale } from '@/next.locales.mjs';

// We only support fetching these pages from the /en/ locale code
const locale = defaultLocale.code;

type StaticParams = { params: { category: string; locale: string } };

// This is the Route Handler for the `GET` method which handles the request
// for providing Blog Posts, Pagination for every supported Blog Category
// this includes the `year-XXXX` categories for yearly archives (pagination)
// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers
export const GET = async (_: Request, { params }: StaticParams) => {
const { posts, pagination } = await provideBlogData(params.category);

return Response.json(
{ posts, pagination },
{ status: posts.length ? 200 : 404 }
);
};

// This function generates the static paths that come from the dynamic segments
// `[locale]/next-data/blog-data/[category]` and returns an array of all available static paths
// This is used for ISR static validation and generation
export const generateStaticParams = async () => {
// This metadata is the original list of all available categories and all available years
// within the Node.js Website Blog Posts (2011, 2012...)
const { meta } = await provideBlogData();

return [
...meta.categories.map(category => ({ category, locale })),
...meta.pagination.map(year => ({ category: `year-${year}`, locale })),
];
};

// Forces that only the paths from `generateStaticParams` are allowed, giving 404 on the contrary
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams
export const dynamicParams = false;

// Enforces that this route is cached and static as much as possible
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
export const dynamic = 'error';
27 changes: 27 additions & 0 deletions app/[locale]/next-data/release-data/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import provideReleaseData from '@/next-data/providers/releaseData';
import { defaultLocale } from '@/next.locales.mjs';

// We only support fetching these pages from the /en/ locale code
const locale = defaultLocale.code;

// This is the Route Handler for the `GET` method which handles the request
// for generating static data related to the Node.js Release Data
// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers
export const GET = async () => {
const releaseData = await provideReleaseData();

return Response.json(releaseData);
};

// This function generates the static paths that come from the dynamic segments
// `[locale]/next-data/release-data/` and returns an array of all available static paths
// This is used for ISR static validation and generation
export const generateStaticParams = async () => [{ locale }];

// Forces that only the paths from `generateStaticParams` are allowed, giving 404 on the contrary
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams
export const dynamicParams = false;

// Enforces that this route is used as static rendering
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
export const dynamic = 'error';
7 changes: 3 additions & 4 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ const sitemap = async (): Promise<MetadataRoute.Sitemap> => {
const paths: string[] = [];

for (const locale of availableLocaleCodes) {
const routesForLanguage = await dynamicRouter.getRoutesByLanguage(locale);
paths.push(
...routesForLanguage.map(route => `${baseUrlAndPath}/${locale}/${route}`)
);
const routes = await dynamicRouter.getRoutesByLanguage(locale);

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

const currentDate = new Date().toISOString();
Expand Down
11 changes: 9 additions & 2 deletions components/Docs/NodeApiVersionLinks.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import type { FC } from 'react';

import getReleaseData from '@/next-data/releaseData';
import { DOCS_URL } from '@/next.constants.mjs';
import { releaseData } from '@/next.json.mjs';

const NodeApiVersionLinks = () => {
// This is a React Async Server Component
// Note that Hooks cannot be used in a RSC async component
// Async Components do not get re-rendered at all.
const NodeApiVersionLinks: FC = async () => {
const releaseData = await getReleaseData();

// Gets all major releases without the 0x release as those are divided on 0.12x and 0.10x
const mappedReleases = releaseData.slice(0, -1).map(({ major }) => (
<li key={major}>
Expand Down
13 changes: 9 additions & 4 deletions components/Downloads/DownloadReleasesTable.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { useTranslations } from 'next-intl';
import { getTranslations } from 'next-intl/server';
import type { FC } from 'react';

import { releaseData } from '@/next.json.mjs';
import getReleaseData from '@/next-data/releaseData';
import { getNodeApiLink } from '@/util/getNodeApiLink';
import { getNodejsChangelog } from '@/util/getNodeJsChangelog';

const DownloadReleasesTable: FC = () => {
const t = useTranslations();
// This is a React Async Server Component
// Note that Hooks cannot be used in a RSC async component
// Async Components do not get re-rendered at all.
const DownloadReleasesTable: FC = async () => {
const releaseData = await getReleaseData();

const t = await getTranslations();

return (
<table id="tbVersions" className="download-table full-width">
Expand Down
18 changes: 3 additions & 15 deletions components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import { useTheme } from 'next-themes';
import type { FC, PropsWithChildren } from 'react';
import { useState } from 'react';

import ActiveLink from '@/components/Common/ActiveLink';
import Link from '@/components/Link';
import { useSiteNavigation } from '@/hooks';
import { usePathname } from '@/navigation.mjs';
import { BASE_PATH } from '@/next.constants.mjs';
import { availableLocales } from '@/next.locales.mjs';

const Header = () => {
const { navigationItems } = useSiteNavigation();
const Header: FC<PropsWithChildren> = ({ children }) => {
const [showLangPicker, setShowLangPicker] = useState(false);
const { resolvedTheme, setTheme } = useTheme();

Expand All @@ -36,17 +34,7 @@ const Header = () => {
/>
</Link>

<nav aria-label="primary">
<ul className="list-divider-pipe">
{navigationItems.map((item, key) => (
<li key={key}>
<ActiveLink href={item.link} allowSubPath>
{item.text}
</ActiveLink>
</li>
))}
</ul>
</nav>
{children}

<div className="switchers">
<button
Expand Down
2 changes: 1 addition & 1 deletion components/Pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { FC } from 'react';

import Link from '@/components/Link';

type PaginationProps = { prev?: number; next?: number };
type PaginationProps = { prev?: number | null; next?: number | null };

const Pagination: FC<PaginationProps> = ({ next, prev }) => {
const t = useTranslations();
Expand Down
24 changes: 24 additions & 0 deletions components/TopNavigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { FC } from 'react';

import ActiveLink from '@/components/Common/ActiveLink';
import { useSiteNavigation } from '@/hooks/server';

const TopNavigation: FC = () => {
const { navigationItems } = useSiteNavigation();

return (
<nav aria-label="primary">
<ul className="list-divider-pipe">
{navigationItems.map(({ link, text }) => (
<li key={link}>
<ActiveLink href={link} allowSubPath>
{text}
</ActiveLink>
</li>
))}
</ul>
</nav>
);
};

export default TopNavigation;
9 changes: 7 additions & 2 deletions components/withNodeRelease.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import type { FC } from 'react';

import { releaseData } from '@/next.json.mjs';
import getReleaseData from '@/next-data/releaseData';
import type { NodeRelease, NodeReleaseStatus } from '@/types';

type WithNodeReleaseProps = {
status: NodeReleaseStatus[] | NodeReleaseStatus;
children: FC<{ release: NodeRelease }>;
};

export const WithNodeRelease: FC<WithNodeReleaseProps> = ({
// This is a React Async Server Component
// Note that Hooks cannot be used in a RSC async component
// Async Components do not get re-rendered at all.
export const WithNodeRelease: FC<WithNodeReleaseProps> = async ({
status,
children: Component,
}) => {
const releaseData = await getReleaseData();

const matchingRelease = releaseData.find(release =>
[status].flat().includes(release.status)
);
Expand Down
1 change: 0 additions & 1 deletion hooks/react-client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ export { default as useDetectOS } from './useDetectOS';
export { default as useMediaQuery } from './useMediaQuery';
export { default as useNotification } from './useNotification';
export { default as useClientContext } from './useClientContext';
export { default as useBlogData } from './useBlogData';
export { default as useSiteNavigation } from './useSiteNavigation';
13 changes: 0 additions & 13 deletions hooks/react-client/useBlogData.ts

This file was deleted.

6 changes: 4 additions & 2 deletions hooks/react-client/useSiteNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use client';

import useBaseSiteNavigation from '@/hooks/useBaseSiteNavigation';
const useSiteNavigation = () => {
throw new Error('Attempted to call useSiteNavigation from RCC');
};

export default useBaseSiteNavigation;
export default useSiteNavigation;
1 change: 0 additions & 1 deletion hooks/react-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ export { default as useDetectOS } from './useDetectOS';
export { default as useMediaQuery } from './useMediaQuery';
export { default as useNotification } from './useNotification';
export { default as useClientContext } from './useClientContext';
export { default as useBlogData } from './useBlogData';
export { default as useSiteNavigation } from './useSiteNavigation';
11 changes: 0 additions & 11 deletions hooks/react-server/useBlogData.ts

This file was deleted.

Loading

0 comments on commit c463a36

Please sign in to comment.