diff --git a/app/[locale]/next-data/api-data/route.ts b/app/[locale]/next-data/api-data/route.ts new file mode 100644 index 0000000000000..11d48967bbe0c --- /dev/null +++ b/app/[locale]/next-data/api-data/route.ts @@ -0,0 +1,63 @@ +import { deflateSync } from 'node:zlib'; + +import { VERCEL_REVALIDATE } from '@/next.constants.mjs'; +import { defaultLocale } from '@/next.locales.mjs'; +import type { GitHubApiFile } from '@/types'; +import { getGitHubApiDocsUrl } from '@/util/gitHubUtils'; +import { parseRichTextIntoPlainText } from '@/util/stringUtils'; + +const getPathnameForApiFile = (name: string) => + `api/${name.replace('.md', '.html')}`; + +// This is the Route Handler for the `GET` method which handles the request +// for a digest and metadata of all API pages from the Node.js Website +// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers +export const GET = async () => { + const gitHubApiResponse = await fetch(getGitHubApiDocsUrl('main')); + + return gitHubApiResponse.json().then((apiDocsFiles: Array) => { + // maps over each api file and get the download_url, fetch the content and deflates it + const mappedApiFiles = apiDocsFiles.map( + async ({ name, path: filename, download_url }) => { + const apiFileResponse = await fetch(download_url); + + // Retrieves the content as a raw text string + const source = await apiFileResponse.text(); + + // Removes empty/blank lines or lines just with spaces and trims each line + // from leading and trailing paddings/spaces + const cleanedContent = parseRichTextIntoPlainText(source); + + const deflatedSource = deflateSync(cleanedContent).toString('base64'); + + return { + filename, + pathname: getPathnameForApiFile(name), + content: deflatedSource, + }; + } + ); + + return Promise.all(mappedApiFiles).then(Response.json); + }); +}; + +// This function generates the static paths that come from the dynamic segments +// `[locale]/next-data/api-data/` and returns an array of all available static paths +// This is used for ISR static validation and generation +export const generateStaticParams = async () => [ + { locale: defaultLocale.code }, +]; + +// Enforces 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 = 'force-static'; + +// Ensures that this endpoint is invalidated and re-executed every X minutes +// so that when new deployments happen, the data is refreshed +// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#revalidate +export const revalidate = VERCEL_REVALIDATE; diff --git a/app/[locale]/next-data/page-data/route.ts b/app/[locale]/next-data/page-data/route.ts index cb64fea5e3a29..2b21630b19398 100644 --- a/app/[locale]/next-data/page-data/route.ts +++ b/app/[locale]/next-data/page-data/route.ts @@ -37,14 +37,14 @@ export const GET = async () => { const deflatedSource = deflateSync(cleanedContent).toString('base64'); // Returns metadata of each page available on the Website - return { pathname, filename, title, description, content: deflatedSource }; + return { filename, pathname, title, description, content: deflatedSource }; }); return Response.json(await Promise.all(availablePagesMetadata)); }; // 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 +// `[locale]/next-data/page-data/` and returns an array of all available static paths // This is used for ISR static validation and generation export const generateStaticParams = async () => [ { locale: defaultLocale.code }, diff --git a/components/Common/AvatarGroup/Avatar/index.stories.tsx b/components/Common/AvatarGroup/Avatar/index.stories.tsx index 5b92b1cf20568..7f3a6430d87b8 100644 --- a/components/Common/AvatarGroup/Avatar/index.stories.tsx +++ b/components/Common/AvatarGroup/Avatar/index.stories.tsx @@ -1,14 +1,14 @@ import type { Meta as MetaObj, StoryObj } from '@storybook/react'; import Avatar from '@/components/Common/AvatarGroup/Avatar'; -import { githubProfileAvatarUrl } from '@/util/gitHubUtils'; +import { getGitHubAvatarUrl } from '@/util/gitHubUtils'; type Story = StoryObj; type Meta = MetaObj; export const Default: Story = { args: { - src: githubProfileAvatarUrl('ovflowd'), + src: getGitHubAvatarUrl('ovflowd'), alt: 'ovflowd', }, }; diff --git a/components/Common/AvatarGroup/index.stories.tsx b/components/Common/AvatarGroup/index.stories.tsx index 6dff66e2e20b7..6aef57bb904ed 100644 --- a/components/Common/AvatarGroup/index.stories.tsx +++ b/components/Common/AvatarGroup/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta as MetaObj, StoryObj } from '@storybook/react'; import AvatarGroup from '@/components/Common/AvatarGroup'; -import { githubProfileAvatarUrl } from '@/util/gitHubUtils'; +import { getGitHubAvatarUrl } from '@/util/gitHubUtils'; type Story = StoryObj; type Meta = MetaObj; @@ -31,7 +31,7 @@ const unknownAvatar = { const defaultProps = { avatars: [ unknownAvatar, - ...names.map(name => ({ src: githubProfileAvatarUrl(name), alt: name })), + ...names.map(name => ({ src: getGitHubAvatarUrl(name), alt: name })), ], }; diff --git a/components/Downloads/ChangelogModal/index.stories.tsx b/components/Downloads/ChangelogModal/index.stories.tsx index bbaf73c4897f4..8fc593f93954d 100644 --- a/components/Downloads/ChangelogModal/index.stories.tsx +++ b/components/Downloads/ChangelogModal/index.stories.tsx @@ -5,7 +5,7 @@ import Button from '@/components/Common/Button'; import ChangelogModal from '@/components/Downloads/ChangelogModal'; import { MDXRenderer } from '@/components/mdxRenderer'; import { compileMDX } from '@/next.mdx.compiler.mjs'; -import { githubProfileAvatarUrl } from '@/util/gitHubUtils'; +import { getGitHubAvatarUrl } from '@/util/gitHubUtils'; type Story = StoryObj; type Meta = MetaObj; @@ -182,7 +182,7 @@ export const Default: Story = { heading: 'Node v18.17.0', subheading: "2023-07-18, Version 18.17.0 'Hydrogen' (LTS), @danielleadams", avatars: names.map(name => ({ - src: githubProfileAvatarUrl(name), + src: getGitHubAvatarUrl(name), alt: name, })), children, diff --git a/components/withMetaBar.tsx b/components/withMetaBar.tsx index ec90dc09e736c..4ae130a3dfab1 100644 --- a/components/withMetaBar.tsx +++ b/components/withMetaBar.tsx @@ -5,7 +5,7 @@ import MetaBar from '@/components/Containers/MetaBar'; import GitHub from '@/components/Icons/Social/GitHub'; import Link from '@/components/Link'; import { useClientContext } from '@/hooks/server'; -import { getGitHubEditPageUrl } from '@/util/gitHubUtils'; +import { getGitHubBlobUrl } from '@/util/gitHubUtils'; const DATE_FORMAT = { month: 'short', @@ -29,7 +29,7 @@ const WithMetaBar: FC = () => { 'components.metabar.contribute': ( <> - Edit this page + Edit this page ), }} diff --git a/next.mdx.compiler.mjs b/next.mdx.compiler.mjs index b515548176b3b..29c672147f9a9 100644 --- a/next.mdx.compiler.mjs +++ b/next.mdx.compiler.mjs @@ -5,7 +5,7 @@ import { Fragment, jsx, jsxs } from 'react/jsx-runtime'; import { matter } from 'vfile-matter'; import { NEXT_REHYPE_PLUGINS, NEXT_REMARK_PLUGINS } from './next.mdx.mjs'; -import { createGitHubSlug } from './util/gitHubUtils'; +import { createGitHubSlugger } from './util/gitHubUtils'; // Defines the React Runtime Components const reactRuntime = { Fragment, jsx, jsxs }; @@ -28,7 +28,7 @@ export async function compileMDX(source, fileExtension) { // cleaning the frontmatter to the source that is going to be parsed by the MDX Compiler matter(source, { strip: true }); - const slugger = createGitHubSlug(); + const slugger = createGitHubSlugger(); // This is a minimal MDX Compiler that is lightweight and only parses the MDX const { default: MDXContent } = await evaluate(source, { diff --git a/types/github.ts b/types/github.ts new file mode 100644 index 0000000000000..d26ad6924ec83 --- /dev/null +++ b/types/github.ts @@ -0,0 +1,11 @@ +export interface GitHubApiFile { + name: string; + path: string; + sha: string; + size: number; + url: string; + html_url: string; + git_url: string; + download_url: string; + type: 'file' | 'dir'; +} diff --git a/types/index.ts b/types/index.ts index fad5ee10f6238..99dc26f939a17 100644 --- a/types/index.ts +++ b/types/index.ts @@ -8,3 +8,4 @@ export * from './navigation'; export * from './releases'; export * from './redirects'; export * from './server'; +export * from './github'; diff --git a/util/gitHubUtils.ts b/util/gitHubUtils.ts index 7327a2637094c..3abf386b88645 100644 --- a/util/gitHubUtils.ts +++ b/util/gitHubUtils.ts @@ -1,13 +1,16 @@ import GitHubSlugger from 'github-slugger'; -export const githubProfileAvatarUrl = (username: string): string => +export const getGitHubAvatarUrl = (username: string): string => `https://avatars.githubusercontent.com/${username}`; -export const createGitHubSlug = () => { +export const createGitHubSlugger = () => { const githubSlugger = new GitHubSlugger(); return (text: string) => githubSlugger.slug(text); }; -export const getGitHubEditPageUrl = (filename: string) => +export const getGitHubBlobUrl = (filename: string) => `https://github.com/nodejs/nodejs.org/blob/main/pages/en/${filename}`; + +export const getGitHubApiDocsUrl = (ref: string) => + `https://api.github.com/repos/nodejs/node/contents/doc/api?ref=${ref}`;