diff --git a/apps/site/app/[locale]/blog/[...path]/page.tsx b/apps/site/app/[locale]/blog/[...path]/page.tsx index 2e3546994465f..982a2b6eecfb8 100644 --- a/apps/site/app/[locale]/blog/[...path]/page.tsx +++ b/apps/site/app/[locale]/blog/[...path]/page.tsx @@ -15,7 +15,8 @@ export const generateViewport = basePage.generateViewport; // This generates each page's HTML Metadata // @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata -export const generateMetadata = basePage.generateMetadata; +export const generateMetadata = ({ params }: PageParams) => + basePage.generateMetadata({ params, prefix: 'blog' }); // Generates all possible static paths based on the locales and environment configuration // - Returns an empty array if static export is disabled (`ENABLE_STATIC_EXPORT` is false) diff --git a/apps/site/app/[locale]/download/archive/[version]/page.tsx b/apps/site/app/[locale]/download/archive/[version]/page.tsx index f8b627977bab3..b82a736c14d81 100644 --- a/apps/site/app/[locale]/download/archive/[version]/page.tsx +++ b/apps/site/app/[locale]/download/archive/[version]/page.tsx @@ -2,8 +2,8 @@ import { notFound, redirect } from 'next/navigation'; import type { FC } from 'react'; import provideReleaseData from '#site/next-data/providers/releaseData'; +import provideReleaseVersions from '#site/next-data/providers/releaseVersions'; import { ENABLE_STATIC_EXPORT } from '#site/next.constants.mjs'; -import { ARCHIVE_DYNAMIC_ROUTES } from '#site/next.dynamic.constants.mjs'; import * as basePage from '#site/next.dynamic.page.mjs'; import { defaultLocale } from '#site/next.locales.mjs'; import type { DynamicParams } from '#site/types'; @@ -29,7 +29,9 @@ export const generateStaticParams = async () => { return []; } - return ARCHIVE_DYNAMIC_ROUTES.map(version => ({ + const versions = await provideReleaseVersions(); + + return versions.map(version => ({ locale: defaultLocale.code, version, })); @@ -46,15 +48,17 @@ const getPage: FC = async props => { const [locale, pathname] = basePage.getLocaleAndPath(version, routeLocale); if (version === 'current') { - const releaseData = provideReleaseData(); + const releaseData = await provideReleaseData(); const release = releaseData.find(release => release.status === 'Current'); redirect(`/${locale}/download/archive/${release?.versionWithPrefix}`); } + const versions = await provideReleaseVersions(); + // Verifies if the current route is a dynamic route - const isDynamicRoute = ARCHIVE_DYNAMIC_ROUTES.some(r => r.includes(pathname)); + const isDynamicRoute = versions.some(r => r.includes(pathname)); // Gets the Markdown content and context for Download Archive pages const [content, context] = await basePage.getMarkdownContext({ diff --git a/apps/site/app/[locale]/not-found.tsx b/apps/site/app/[locale]/not-found.tsx index 0842df54d486e..bca476307ac22 100644 --- a/apps/site/app/[locale]/not-found.tsx +++ b/apps/site/app/[locale]/not-found.tsx @@ -1,15 +1,15 @@ -'use client'; +'use server'; import { ArrowRightIcon } from '@heroicons/react/24/solid'; -import { useTranslations } from 'next-intl'; +import { getTranslations } from 'next-intl/server'; import type { FC } from 'react'; import Button from '#site/components/Common/Button'; import Turtle from '#site/components/Common/Turtle'; import GlowingBackdropLayout from '#site/layouts/GlowingBackdrop'; -const NotFoundPage: FC = () => { - const t = useTranslations(); +const NotFoundPage: FC = async () => { + const t = await getTranslations(); return ( diff --git a/apps/site/components/EOL/EOLReleaseTable.tsx b/apps/site/components/EOL/EOLReleaseTable.tsx deleted file mode 100644 index 2119fefb8ef03..0000000000000 --- a/apps/site/components/EOL/EOLReleaseTable.tsx +++ /dev/null @@ -1,83 +0,0 @@ -'use client'; - -import { useTranslations } from 'next-intl'; -import { Fragment, useState } from 'react'; -import type { FC } from 'react'; - -import FormattedTime from '#site/components/Common/FormattedTime'; -import LinkWithArrow from '#site/components/Common/LinkWithArrow'; -import EOLModal from '#site/components/EOL/EOLModal'; -import VulnerabilityChips from '#site/components/EOL/VulnerabilityChips'; -import provideReleaseData from '#site/next-data/providers/releaseData'; -import provideVulnerabilities from '#site/next-data/providers/vulnerabilities'; -import { EOL_VERSION_IDENTIFIER } from '#site/next.constants.mjs'; - -const EOLReleaseTable: FC = () => { - const releaseData = provideReleaseData(); - const vulnerabilities = provideVulnerabilities(); - - const eolReleases = releaseData.filter( - release => release.status === EOL_VERSION_IDENTIFIER - ); - - const t = useTranslations(); - - const [currentModal, setCurrentModal] = useState(); - - return ( - - - - - - - - - - - - {eolReleases.map(release => ( - - - - - - - - - - - - open || setCurrentModal(undefined)} - /> - - ))} - -
- {t('components.eolTable.version')} ( - {t('components.eolTable.codename')}) - {t('components.eolTable.lastUpdated')}{t('components.eolTable.vulnerabilities')}{t('components.eolTable.details')}
- v{release.major}{' '} - {release.codename ? `(${release.codename})` : ''} - - - - - - setCurrentModal(release.version)} - > - {t('components.downloadReleasesTable.details')} - -
- ); -}; - -export default EOLReleaseTable; diff --git a/apps/site/components/EOL/EOLReleaseTable/TableBody.tsx b/apps/site/components/EOL/EOLReleaseTable/TableBody.tsx new file mode 100644 index 0000000000000..ee2fa061f37c7 --- /dev/null +++ b/apps/site/components/EOL/EOLReleaseTable/TableBody.tsx @@ -0,0 +1,68 @@ +'use client'; + +import { useTranslations } from 'next-intl'; +import type { FC } from 'react'; +import { Fragment, useState } from 'react'; + +import FormattedTime from '#site/components/Common/FormattedTime'; +import LinkWithArrow from '#site/components/Common/LinkWithArrow'; +import EOLModal from '#site/components/EOL/EOLModal'; +import VulnerabilityChips from '#site/components/EOL/VulnerabilityChips'; +import type { NodeRelease } from '#site/types/releases.js'; +import type { GroupedVulnerabilities } from '#site/types/vulnerabilities.js'; + +type EOLReleaseTableBodyProps = { + eolReleases: Array; + vulnerabilities: GroupedVulnerabilities; +}; + +const EOLReleaseTableBody: FC = ({ + eolReleases, + vulnerabilities, +}) => { + const t = useTranslations(); + + const [currentModal, setCurrentModal] = useState(); + + return ( + + {eolReleases.map(release => ( + + + + v{release.major} {release.codename ? `(${release.codename})` : ''} + + + + + + + + + + + + setCurrentModal(release.version)} + > + {t('components.downloadReleasesTable.details')} + + + + + open || setCurrentModal(undefined)} + /> + + ))} + + ); +}; + +export default EOLReleaseTableBody; diff --git a/apps/site/components/EOL/EOLReleaseTable/index.tsx b/apps/site/components/EOL/EOLReleaseTable/index.tsx new file mode 100644 index 0000000000000..d1e15f6dc37de --- /dev/null +++ b/apps/site/components/EOL/EOLReleaseTable/index.tsx @@ -0,0 +1,42 @@ +import { getTranslations } from 'next-intl/server'; +import type { FC } from 'react'; + +import provideReleaseData from '#site/next-data/providers/releaseData'; +import provideVulnerabilities from '#site/next-data/providers/vulnerabilities'; +import { EOL_VERSION_IDENTIFIER } from '#site/next.constants.mjs'; + +import EOLReleaseTableBody from './TableBody'; + +const EOLReleaseTable: FC = async () => { + const releaseData = await provideReleaseData(); + const vulnerabilities = await provideVulnerabilities(); + + const eolReleases = releaseData.filter( + release => release.status === EOL_VERSION_IDENTIFIER + ); + + const t = await getTranslations(); + + return ( + + + + + + + + + + + +
+ {t('components.eolTable.version')} ( + {t('components.eolTable.codename')}) + {t('components.eolTable.lastUpdated')}{t('components.eolTable.vulnerabilities')}{t('components.eolTable.details')}
+ ); +}; + +export default EOLReleaseTable; diff --git a/apps/site/components/Releases/PreviousReleasesTable.tsx b/apps/site/components/Releases/PreviousReleasesTable.tsx deleted file mode 100644 index 58f7846f80fd2..0000000000000 --- a/apps/site/components/Releases/PreviousReleasesTable.tsx +++ /dev/null @@ -1,98 +0,0 @@ -'use client'; - -import Badge from '@node-core/ui-components/Common/Badge'; -import { useTranslations } from 'next-intl'; -import type { FC } from 'react'; -import { Fragment, useState } from 'react'; - -import FormattedTime from '#site/components/Common/FormattedTime'; -import LinkWithArrow from '#site/components/Common/LinkWithArrow'; -import Link from '#site/components/Link'; -import provideReleaseData from '#site/next-data/providers/releaseData'; - -import ReleaseModal from './ReleaseModal'; - -const BADGE_KIND_MAP = { - 'End-of-life': 'warning', - 'Maintenance LTS': 'neutral', - 'Active LTS': 'info', - Current: 'default', - Pending: 'default', -} as const; - -const PreviousReleasesTable: FC = () => { - const releaseData = provideReleaseData(); - - const t = useTranslations(); - - const [currentModal, setCurrentModal] = useState(); - - return ( - - - - - - - - - - - - - - {releaseData.map(release => ( - - - - - - - - - - - - - - - - open || setCurrentModal(undefined)} - /> - - ))} - -
{t('components.downloadReleasesTable.version')}{t('components.downloadReleasesTable.codename')}{t('components.downloadReleasesTable.firstReleased')}{t('components.downloadReleasesTable.lastUpdated')}{t('components.downloadReleasesTable.status')}
- - v{release.major} - - - {release.codename || '-'} - - - - - - - {release.status} - {release.status === 'End-of-life' ? ' (EoL)' : ''} - - - setCurrentModal(release.version)} - > - {t('components.downloadReleasesTable.details')} - -
- ); -}; - -export default PreviousReleasesTable; diff --git a/apps/site/components/Releases/PreviousReleasesTable/TableBody.tsx b/apps/site/components/Releases/PreviousReleasesTable/TableBody.tsx new file mode 100644 index 0000000000000..f5011cac7290d --- /dev/null +++ b/apps/site/components/Releases/PreviousReleasesTable/TableBody.tsx @@ -0,0 +1,87 @@ +'use client'; + +import Badge from '@node-core/ui-components/Common/Badge'; +import { useTranslations } from 'next-intl'; +import type { FC } from 'react'; +import { Fragment, useState } from 'react'; + +import FormattedTime from '#site/components/Common/FormattedTime'; +import LinkWithArrow from '#site/components/Common/LinkWithArrow'; +import Link from '#site/components/Link'; +import type { NodeRelease } from '#site/types/releases.js'; + +import ReleaseModal from '../ReleaseModal'; + +const BADGE_KIND_MAP = { + 'End-of-life': 'warning', + 'Maintenance LTS': 'neutral', + 'Active LTS': 'info', + Current: 'default', + Pending: 'default', +} as const; + +type PreviousReleasesTableBodyProps = { + releaseData: Array; +}; + +const PreviousReleasesTableBody: FC = ({ + releaseData, +}) => { + const t = useTranslations(); + + const [currentModal, setCurrentModal] = useState(); + + return ( + + {releaseData.map(release => ( + + + + + v{release.major} + + + + + {release.codename || '-'} + + + + + + + + + + + + + {release.status} + {release.status === 'End-of-life' ? ' (EoL)' : ''} + + + + + setCurrentModal(release.version)} + > + {t('components.downloadReleasesTable.details')} + + + + + open || setCurrentModal(undefined)} + /> + + ))} + + ); +}; + +export default PreviousReleasesTableBody; diff --git a/apps/site/components/Releases/PreviousReleasesTable/index.tsx b/apps/site/components/Releases/PreviousReleasesTable/index.tsx new file mode 100644 index 0000000000000..342528ec62e32 --- /dev/null +++ b/apps/site/components/Releases/PreviousReleasesTable/index.tsx @@ -0,0 +1,31 @@ +import { getTranslations } from 'next-intl/server'; +import type { FC } from 'react'; + +import provideReleaseData from '#site/next-data/providers/releaseData'; + +import PreviousReleasesTableBody from './TableBody'; + +const PreviousReleasesTable: FC = async () => { + const releaseData = await provideReleaseData(); + + const t = await getTranslations(); + + return ( + + + + + + + + + + + + + +
{t('components.downloadReleasesTable.version')}{t('components.downloadReleasesTable.codename')}{t('components.downloadReleasesTable.firstReleased')}{t('components.downloadReleasesTable.lastUpdated')}{t('components.downloadReleasesTable.status')}
+ ); +}; + +export default PreviousReleasesTable; diff --git a/apps/site/components/withDownloadArchive.tsx b/apps/site/components/withDownloadArchive.tsx index 460150ead3948..df8ecdb70aea0 100644 --- a/apps/site/components/withDownloadArchive.tsx +++ b/apps/site/components/withDownloadArchive.tsx @@ -27,7 +27,7 @@ const WithDownloadArchive: FC = async ({ const version = extractVersionFromPath(pathname); // Find the release data for the given version - const releaseData = provideReleaseData(); + const releaseData = await provideReleaseData(); const release = releaseData.find(release => // Match major version only (e.g., v22.x.x for release.major v22) version.startsWith(`v${release.major}`) diff --git a/apps/site/components/withDownloadSection.tsx b/apps/site/components/withDownloadSection.tsx index 34d76d09d6991..3caa60d024598 100644 --- a/apps/site/components/withDownloadSection.tsx +++ b/apps/site/components/withDownloadSection.tsx @@ -4,20 +4,27 @@ import type { FC, PropsWithChildren } from 'react'; import { getClientContext } from '#site/client-context'; import WithNodeRelease from '#site/components/withNodeRelease'; import provideDownloadSnippets from '#site/next-data/providers/downloadSnippets'; -import provideReleaseData from '#site/next-data/providers/releaseData'; import { defaultLocale } from '#site/next.locales.mjs'; import { ReleaseProvider, ReleasesProvider, } from '#site/providers/releaseProvider'; +import type { NodeRelease } from '../types'; + // By default the translated languages do not contain all the download snippets // Hence we always merge any translated snippet with the fallbacks for missing snippets const fallbackSnippets = provideDownloadSnippets(defaultLocale.code); -const WithDownloadSection: FC = ({ children }) => { +type WithDownloadSectionProps = PropsWithChildren<{ + releases: Array; +}>; + +const WithDownloadSection: FC = ({ + releases, + children, +}) => { const locale = useLocale(); - const releases = provideReleaseData(); const snippets = provideDownloadSnippets(locale); const { pathname } = getClientContext(); diff --git a/apps/site/components/withNodeRelease.tsx b/apps/site/components/withNodeRelease.tsx index 49b7a38bd0fc0..078c0c8ceed76 100644 --- a/apps/site/components/withNodeRelease.tsx +++ b/apps/site/components/withNodeRelease.tsx @@ -1,3 +1,5 @@ +'use server'; + import type { FC } from 'react'; import provideReleaseData from '#site/next-data/providers/releaseData'; @@ -11,11 +13,11 @@ type 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. -const WithNodeRelease: FC = ({ +const WithNodeRelease: FC = async ({ status, children: Component, }) => { - const releaseData = provideReleaseData(); + const releaseData = await provideReleaseData(); const matchingRelease = releaseData.find(release => [status].flat().includes(release.status) diff --git a/apps/site/components/withReleaseSelect.tsx b/apps/site/components/withReleaseSelect.tsx index e48a94b19a98e..5dbd64c009d8c 100644 --- a/apps/site/components/withReleaseSelect.tsx +++ b/apps/site/components/withReleaseSelect.tsx @@ -1,10 +1,9 @@ -'use client'; +'use server'; -import WithNoScriptSelect from '@node-core/ui-components/Common/Select/NoScriptSelect'; +import StatelessSelect from '@node-core/ui-components/Common/Select/StatelessSelect'; import type { ComponentProps, FC } from 'react'; import Link from '#site/components/Link'; -import { useRouter } from '#site/navigation.mjs'; import provideReleaseData from '#site/next-data/providers/releaseData'; import type { NodeRelease } from '#site/types'; import { STATUS_ORDER } from '#site/util/download'; @@ -39,21 +38,19 @@ const groupReleasesByStatus = (releases: Array) => { }; type WithReleaseSelectProps = Omit< - ComponentProps, - 'values' | 'as' | 'onChange' + ComponentProps, + 'values' | 'as' >; -const WithReleaseSelect: FC = ({ ...props }) => { - const releaseData = provideReleaseData(); - const { push } = useRouter(); +const WithReleaseSelect: FC = async ({ ...props }) => { + const releaseData = await provideReleaseData(); const navigation = groupReleasesByStatus(releaseData); return ( - diff --git a/apps/site/components/withSupporters.tsx b/apps/site/components/withSupporters.tsx index 2227e653d6385..6ce69110ab217 100644 --- a/apps/site/components/withSupporters.tsx +++ b/apps/site/components/withSupporters.tsx @@ -2,12 +2,12 @@ import type { FC, PropsWithChildren } from 'react'; -import SupportersList from './Common/Supporters'; - import provideSupporters from '#site/next-data/providers/supportersData'; -const WithSupporters: FC = () => { - const supporters = provideSupporters(); +import SupportersList from './Common/Supporters'; + +const WithSupporters: FC = async () => { + const supporters = await provideSupporters(); return (
diff --git a/apps/site/layouts/Download.tsx b/apps/site/layouts/Download.tsx index 79f589bbf04f7..df5fc573474a9 100644 --- a/apps/site/layouts/Download.tsx +++ b/apps/site/layouts/Download.tsx @@ -4,12 +4,15 @@ import { getClientContext } from '#site/client-context'; import WithDownloadSection from '#site/components/withDownloadSection'; import WithFooter from '#site/components/withFooter'; import WithNavBar from '#site/components/withNavBar'; +import provideReleaseData from '#site/next-data/providers/releaseData'; import styles from './layouts.module.css'; -const DownloadLayout: FC = ({ children }) => { +const DownloadLayout: FC = async ({ children }) => { const { frontmatter } = getClientContext(); + const releases = await provideReleaseData(); + return ( <> @@ -18,7 +21,9 @@ const DownloadLayout: FC = ({ children }) => {

{frontmatter.title}

- {children} + + {children} +
diff --git a/apps/site/next-data/generators/supportersData.mjs b/apps/site/next-data/generators/supportersData.mjs index fd788a7c14074..1c1c19f15b77b 100644 --- a/apps/site/next-data/generators/supportersData.mjs +++ b/apps/site/next-data/generators/supportersData.mjs @@ -25,4 +25,4 @@ async function fetchOpenCollectiveData() { return members; } -export { fetchOpenCollectiveData }; +export default fetchOpenCollectiveData; diff --git a/apps/site/next-data/providers/releaseData.ts b/apps/site/next-data/providers/releaseData.ts index 04f37ac05eeda..c063dd8fd5ea5 100644 --- a/apps/site/next-data/providers/releaseData.ts +++ b/apps/site/next-data/providers/releaseData.ts @@ -1,9 +1,5 @@ -import { cache } from 'react'; +'use cache'; -import generateReleaseData from '#site/next-data/generators/releaseData.mjs'; - -const releaseData = await generateReleaseData(); - -const provideReleaseData = cache(() => releaseData); +import provideReleaseData from '#site/next-data/generators/releaseData.mjs'; export default provideReleaseData; diff --git a/apps/site/next-data/providers/releaseVersions.ts b/apps/site/next-data/providers/releaseVersions.ts index f7ac4d86d010d..d9aa33346dc41 100644 --- a/apps/site/next-data/providers/releaseVersions.ts +++ b/apps/site/next-data/providers/releaseVersions.ts @@ -1,9 +1,5 @@ -import { cache } from 'react'; +'use cache'; -import generateAllVersionsData from '#site/next-data/generators/releaseVersions.mjs'; - -const releaseVersions = await generateAllVersionsData(); - -const provideReleaseVersions = cache(() => releaseVersions); +import provideReleaseVersions from '#site/next-data/generators/releaseVersions.mjs'; export default provideReleaseVersions; diff --git a/apps/site/next-data/providers/supportersData.mjs b/apps/site/next-data/providers/supportersData.mjs deleted file mode 100644 index a5479b43f03e3..0000000000000 --- a/apps/site/next-data/providers/supportersData.mjs +++ /dev/null @@ -1,9 +0,0 @@ -import { cache } from 'react'; - -import { fetchOpenCollectiveData } from '#site/next-data/generators/supportersData.mjs'; - -const openCollectiveSupporters = await fetchOpenCollectiveData(); - -const provideSupporters = cache(() => openCollectiveSupporters); - -export default provideSupporters; diff --git a/apps/site/next-data/providers/supportersData.ts b/apps/site/next-data/providers/supportersData.ts new file mode 100644 index 0000000000000..e6e34a6877590 --- /dev/null +++ b/apps/site/next-data/providers/supportersData.ts @@ -0,0 +1,5 @@ +'use cache'; + +import provideSupporters from '#site/next-data/generators/supportersData.mjs'; + +export default provideSupporters; diff --git a/apps/site/next-data/providers/vulnerabilities.ts b/apps/site/next-data/providers/vulnerabilities.ts index cfc3cce008ba0..e360781eebd10 100644 --- a/apps/site/next-data/providers/vulnerabilities.ts +++ b/apps/site/next-data/providers/vulnerabilities.ts @@ -1,9 +1,5 @@ -import { cache } from 'react'; +'use cache'; -import generateVulnerabilities from '#site/next-data/generators/vulnerabilities.mjs'; - -const vulnerabilities = await generateVulnerabilities(); - -const provideVulnerabilities = cache(() => vulnerabilities); +import provideVulnerabilities from '#site/next-data/generators/vulnerabilities.mjs'; export default provideVulnerabilities; diff --git a/apps/site/next.config.mjs b/apps/site/next.config.mjs index 8eb650197844e..7396448b79b18 100644 --- a/apps/site/next.config.mjs +++ b/apps/site/next.config.mjs @@ -80,6 +80,7 @@ const nextConfig = { // we also configure ESLint to run its lint checking on all files eslint: { ignoreDuringBuilds: true }, experimental: { + useCache: true, // Ensure that server-side code is also minified serverMinification: true, // Use Workers and Threads for webpack compilation diff --git a/apps/site/next.dynamic.constants.mjs b/apps/site/next.dynamic.constants.mjs index 68860ee2e2a0f..ce9f3128e0077 100644 --- a/apps/site/next.dynamic.constants.mjs +++ b/apps/site/next.dynamic.constants.mjs @@ -1,7 +1,6 @@ 'use strict'; import { provideBlogPosts } from '#site/next-data/providers/blogData'; -import provideReleaseVersions from '#site/next-data/providers/releaseVersions'; import { blogData } from '#site/next.json.mjs'; import { BASE_PATH, BASE_URL } from './next.constants.mjs'; @@ -28,19 +27,6 @@ export const BLOG_DYNAMIC_ROUTES = [ .flat(), ]; -/** - * This constant is used to create static routes on-the-fly that do not have a file-system - * counterpart route. This is useful for providing routes with matching Layout Names - * but that do not have Markdown content and a matching file for the route - * - * @type {Array} A Map of pathname and Layout Name - */ -export const ARCHIVE_DYNAMIC_ROUTES = [ - // Creates dynamic routes for downloads archive pages for each version - // (e.g., /download/archive/v18.20.8, /download/archive/v20.19.2) - ...provideReleaseVersions(), -]; - /** * This is the default Next.js Page Metadata for all pages * diff --git a/apps/site/next.dynamic.page.mjs b/apps/site/next.dynamic.page.mjs index 3a35f371bc720..06150d6b8baa9 100644 --- a/apps/site/next.dynamic.page.mjs +++ b/apps/site/next.dynamic.page.mjs @@ -1,3 +1,5 @@ +import { join } from 'node:path'; + import { notFound, redirect } from 'next/navigation'; import { setRequestLocale } from 'next-intl/server'; @@ -23,15 +25,19 @@ export const generateViewport = () => ({ ...PAGE_VIEWPORT }); * * @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata * - * @param {{ params: Promise<{ path: Array; locale: string }> }} props + * @param {{ params: Promise<{ path: Array; locale: string }>, prefix?: string }} props * @returns {Promise} the metadata for the page */ -export const generateMetadata = async props => { - const { path = [], locale = defaultLocale.code } = await props.params; +export const generateMetadata = async ({ params, prefix }) => { + const { path = [], locale = defaultLocale.code } = await params; const pathname = dynamicRouter.getPathname(path); - return dynamicRouter.getPageMetadata(locale, pathname); + return dynamicRouter.getPageMetadata( + locale, + // If there's a prefix, `join` it with the pathname + prefix ? join(prefix, pathname) : pathname + ); }; /**