From dd33d07af52e7fc52cfbb9458f657af8f32de667 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sun, 17 Aug 2025 21:18:23 +0200 Subject: [PATCH 1/5] chore: get rid of hacky dynamic pages, use proper app router --- apps/site/app/[locale]/[...path]/page.tsx | 48 +++++-- .../site/app/[locale]/blog/[...path]/page.tsx | 102 +++++++++++++ apps/site/app/[locale]/page.tsx | 134 +++++++++--------- apps/site/next.dynamic.constants.mjs | 41 ------ apps/site/next.dynamic.mjs | 17 +-- 5 files changed, 213 insertions(+), 129 deletions(-) create mode 100644 apps/site/app/[locale]/blog/[...path]/page.tsx diff --git a/apps/site/app/[locale]/[...path]/page.tsx b/apps/site/app/[locale]/[...path]/page.tsx index 01edf259fe177..8bfe7f6094cfe 100644 --- a/apps/site/app/[locale]/[...path]/page.tsx +++ b/apps/site/app/[locale]/[...path]/page.tsx @@ -7,14 +7,18 @@ * dynamic params, which will lead on static export errors and other sort of issues. */ +import { notFound } from 'next/navigation'; +import type { FC } from 'react'; + import * as basePage from '#site/app/[locale]/page'; -import { - ENABLE_STATIC_EXPORT_LOCALE, - ENABLE_STATIC_EXPORT, -} from '#site/next.constants.mjs'; +import { ENABLE_STATIC_EXPORT } from '#site/next.constants.mjs'; +import { ENABLE_STATIC_EXPORT_LOCALE } from '#site/next.constants.mjs'; import { dynamicRouter } from '#site/next.dynamic.mjs'; import { availableLocaleCodes, defaultLocale } from '#site/next.locales.mjs'; +type DynamicStaticPaths = { path: Array; locale: string }; +type DynamicParams = { params: Promise }; + // This is the default Viewport Metadata // @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function export const generateViewport = basePage.generateViewport; @@ -35,12 +39,10 @@ export const generateStaticParams = async () => { } // Helper function to fetch and map routes for a specific locale - const getRoutesForLocale = async (locale: string) => { - const routes = await dynamicRouter.getRoutesByLanguage(locale); + const getRoutesForLocale = async (l: string) => { + const routes = await dynamicRouter.getRoutesByLanguage(l); - return routes.map(pathname => - dynamicRouter.mapPathToRoute(locale, pathname) - ); + return routes.map(pathname => dynamicRouter.mapPathToRoute(l, pathname)); }; // Determine which locales to include in the static export @@ -54,6 +56,32 @@ export const generateStaticParams = async () => { return routes.flat().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 = async props => { + // Gets the current full pathname for a given path + const [locale, pathname] = await basePage.getLocaleAndPath(props); + + // Gets the Markdown content and context + const [content, context] = await basePage.getMarkdownContext( + locale, + pathname + ); + + // If we have a filename and layout then we have a page + if (context.filename && context.frontmatter.layout) { + return basePage.renderPage({ + content: content, + layout: context.frontmatter.layout, + context: context, + }); + } + + return notFound(); +}; + // Enforces that this route is used as static rendering // Except whenever on the Development mode as we want instant-refresh when making changes // @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic @@ -64,4 +92,4 @@ export const dynamic = 'force-static'; // @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#revalidate export const revalidate = 300; -export default basePage.default; +export default getPage; diff --git a/apps/site/app/[locale]/blog/[...path]/page.tsx b/apps/site/app/[locale]/blog/[...path]/page.tsx new file mode 100644 index 0000000000000..dc0ee721d567a --- /dev/null +++ b/apps/site/app/[locale]/blog/[...path]/page.tsx @@ -0,0 +1,102 @@ +/** + * This file extends on the `page.tsx` file, which is the default file that is used to render + * the entry points for each locale and then also reused within the [...path] route to render the + * and contains all logic for rendering our dynamic and static routes within the Node.js Website. + * + * Note: that each `page.tsx` should have its own `generateStaticParams` to prevent clash of + * dynamic params, which will lead on static export errors and other sort of issues. + */ + +import { notFound } from 'next/navigation'; +import type { FC } from 'react'; + +import * as basePage from '#site/app/[locale]/page'; +import { provideBlogPosts } from '#site/next-data/providers/blogData'; +import { ENABLE_STATIC_EXPORT } from '#site/next.constants.mjs'; +import { blogData } from '#site/next.json.mjs'; +import { defaultLocale } from '#site/next.locales.mjs'; + +type DynamicStaticPaths = { path: Array; locale: string }; +type DynamicParams = { params: Promise }; + +/** + * This is a list of all static routes or pages from the Website that we do not + * want to allow to be statically built on our Static Export Build. + */ +const BLOG_DYNAMIC_ROUTES = blogData.categories.flatMap(category => { + // Each category can have multiple pages, so we generate a route for each page + const categoryPages = provideBlogPosts(category).pagination.pages; + + const categoryRoutes = Array.from({ length: categoryPages }, (_, page) => ({ + locale: defaultLocale.code, + path: [category, `${category}/page/${page + 1}`], + })); + + return [{ locale: defaultLocale.code, path: [category] }, ...categoryRoutes]; +}); + +// This is the default Viewport Metadata +// @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function +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; + +// 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) +// - If `ENABLE_STATIC_EXPORT_LOCALE` is true, generates paths for all available locales +// - Otherwise, generates paths only for the default locale +// @see https://nextjs.org/docs/app/api-reference/functions/generate-static-params +export const generateStaticParams = async () => { + // Return an empty array if static export is disabled + if (!ENABLE_STATIC_EXPORT) { + return []; + } + + return BLOG_DYNAMIC_ROUTES; +}; + +// 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 = async props => { + // Gets the current full pathname for a given path + const [locale, pathname] = await basePage.getLocaleAndPath(props); + + const isDynamicRoute = BLOG_DYNAMIC_ROUTES.some(route => + route.path.includes(pathname) + ); + + // Gets the Markdown content and context for Blog pages + // otherwise this is likely a blog-category or a blog post + const [content, context] = await basePage.getMarkdownContext( + locale, + `blog/${pathname}` + ); + + // If this isn't a valid dynamic route for blog post or there's no mardown file + // for this, then we fail as not found as there's nothing we can do. + if (isDynamicRoute || context.filename) { + return basePage.renderPage({ + content: content, + layout: context.frontmatter.layout ?? 'blog-category', + context: { ...context, pathname: `/blog/${pathname}` }, + }); + } + + return notFound(); +}; + +// Enforces that this route is used as static rendering +// Except whenever on the Development mode as we want instant-refresh when making changes +// @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 = 300; + +export default getPage; diff --git a/apps/site/app/[locale]/page.tsx b/apps/site/app/[locale]/page.tsx index cf9d20c1de3b3..f7ae657ec9cfa 100644 --- a/apps/site/app/[locale]/page.tsx +++ b/apps/site/app/[locale]/page.tsx @@ -9,26 +9,29 @@ import { notFound, redirect } from 'next/navigation'; import { setRequestLocale } from 'next-intl/server'; -import type { FC } from 'react'; +import type { FC, ReactNode } from 'react'; import { setClientContext } from '#site/client-context'; import WithLayout from '#site/components/withLayout'; -import { - ENABLE_STATIC_EXPORT_LOCALE, - ENABLE_STATIC_EXPORT, -} from '#site/next.constants.mjs'; -import { - PAGE_VIEWPORT, - DYNAMIC_ROUTES, -} from '#site/next.dynamic.constants.mjs'; +import { ENABLE_STATIC_EXPORT } from '#site/next.constants.mjs'; +import { ENABLE_STATIC_EXPORT_LOCALE } from '#site/next.constants.mjs'; +import { PAGE_VIEWPORT } from '#site/next.dynamic.constants.mjs'; import { dynamicRouter } from '#site/next.dynamic.mjs'; import { allLocaleCodes, availableLocaleCodes } from '#site/next.locales.mjs'; import { defaultLocale } from '#site/next.locales.mjs'; import { MatterProvider } from '#site/providers/matterProvider'; +import type { Layouts } from '#site/types/layouts'; +import type { ClientSharedServerContext } from '#site/types/server'; type DynamicStaticPaths = { path: Array; locale: string }; type DynamicParams = { params: Promise }; +type DynamicPageRender = { + content: ReactNode; + layout: Layouts; + context: Partial; +}; + // This is the default Viewport Metadata // @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function export const generateViewport = () => ({ ...PAGE_VIEWPORT }); @@ -67,11 +70,8 @@ export const generateStaticParams = async () => { return routes.flat().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 = async props => { +// This method is used for retrieving the current locale and pathname from the request +export const getLocaleAndPath = async (props: DynamicParams) => { const { path = [], locale = defaultLocale.code } = await props.params; if (!availableLocaleCodes.includes(locale)) { @@ -93,31 +93,11 @@ const getPage: FC = async props => { setRequestLocale(locale); // Gets the current full pathname for a given path - const pathname = dynamicRouter.getPathname(path); - - const staticGeneratedLayout = DYNAMIC_ROUTES.get(pathname); - - // If the current pathname is a statically generated route - // it means it does not have a Markdown file nor exists under the filesystem - // but it is a valid route with an assigned layout that should be rendered - if (staticGeneratedLayout !== undefined) { - // Metadata and shared Context to be available through the lifecycle of the page - const sharedContext = { pathname: `/${pathname}` }; - - // Defines a shared Server Context for the Client-Side - // That is shared for all pages under the dynamic router - setClientContext(sharedContext); - - // The Matter Provider allows Client-Side injection of the data - // to a shared React Client Provider even though the page is rendered - // within a server-side context - return ( - - - - ); - } + return [locale, dynamicRouter.getPathname(path)] as const; +}; +// This method is used for retrieving the Markdown content and context +export const getMarkdownContext = async (locale: string, pathname: string) => { // 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 @@ -126,33 +106,57 @@ const getPage: FC = async props => { 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 { content, frontmatter, headings, readingTime } = - await dynamicRouter.getMDXContent(source, filename); - - // Metadata and shared Context to be available through the lifecycle of the page - const sharedContext = { - frontmatter: frontmatter, - headings: headings, - pathname: `/${pathname}`, - readingTime: readingTime, - filename: filename, - }; - - // Defines a shared Server Context for the Client-Side - // That is shared for all pages under the dynamic router - setClientContext(sharedContext); - - // The Matter Provider allows Client-Side injection of the data - // to a shared React Client Provider even though the page is rendered - // within a server-side context - return ( - - {content} - - ); + // This parses the source Markdown content and returns a React Component and + // relevant context from the Markdown File + const { content, frontmatter, headings, readingTime } = + await dynamicRouter.getMDXContent(source, filename); + + // Metadata and shared Context to be available through the lifecycle of the page + const context = { + frontmatter: frontmatter, + headings: headings, + pathname: `/${pathname}`, + readingTime: readingTime, + filename: filename, + }; + + return [content, context] as const; +}; + +// This method is used for rendering the actual page +export const renderPage: FC = props => { + // Defines a shared Server Context for the Client-Side + // That is shared for all pages under the dynamic router + setClientContext(props.context); + + // The Matter Provider allows Client-Side injection of the data + // to a shared React Client Provider even though the page is rendered + // within a server-side context + return ( + + {props.content} + + ); +}; + +// 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 = async props => { + // Gets the current full pathname for a given path + const [locale, pathname] = await getLocaleAndPath(props); + + // Gets the Markdown content and context + const [content, context] = await getMarkdownContext(locale, pathname); + + // If we have a filename and layout then we have a page + if (context.filename && context.frontmatter.layout) { + return renderPage({ + content: content, + layout: context.frontmatter.layout, + context: context, + }); } return notFound(); diff --git a/apps/site/next.dynamic.constants.mjs b/apps/site/next.dynamic.constants.mjs index 294344b09ea15..b96fa7b0cea1d 100644 --- a/apps/site/next.dynamic.constants.mjs +++ b/apps/site/next.dynamic.constants.mjs @@ -1,48 +1,7 @@ 'use strict'; -import { blogData } from '#site/next.json.mjs'; - -import { provideBlogPosts } from './next-data/providers/blogData'; import { BASE_PATH, BASE_URL } from './next.constants.mjs'; import { siteConfig } from './next.json.mjs'; -import { defaultLocale } from './next.locales.mjs'; - -/** - * This is a list of all static routes or pages from the Website that we do not - * want to allow to be statically built on our Static Export Build. - * - * @type {Array<((route: import('./types').RouteSegment) => boolean)>} A list of Ignored Routes by Regular Expressions - */ -export const IGNORED_ROUTES = [ - // This is used to ignore all blog routes except for the English language - ({ locale, pathname }) => - locale !== defaultLocale.code && /^blog/.test(pathname), - // This is used to ignore all pathnames that are empty - ({ locale, pathname }) => locale.length && !pathname.length, -]; - -/** - * 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 {Map} A Map of pathname and Layout Name - */ -export const DYNAMIC_ROUTES = new Map([ - // Provides Routes for all Blog Categories - ...blogData.categories.map(c => [`blog/${c}`, 'blog-category']), - // Provides Routes for all Blog Categories w/ Pagination - ...blogData.categories - // retrieves the amount of pages for each blog category - .map(c => [c, provideBlogPosts(c).pagination.pages]) - // creates a numeric array for each page and define a pathname for - // each page for a category (i.e. blog/all/page/1) - .map(([c, t]) => [...Array(t).keys()].map(p => `blog/${c}/page/${p + 1}`)) - // creates a tuple of each pathname and layout for the route - .map(paths => paths.map(path => [path, 'blog-category'])) - // flattens the array since we have a .map inside another .map - .flat(), -]); /** * This is the default Next.js Page Metadata for all pages diff --git a/apps/site/next.dynamic.mjs b/apps/site/next.dynamic.mjs index eddacdaaeaebc..95ff9b9604af7 100644 --- a/apps/site/next.dynamic.mjs +++ b/apps/site/next.dynamic.mjs @@ -14,11 +14,7 @@ import { ENABLE_STATIC_EXPORT, IS_DEV_ENV, } from './next.constants.mjs'; -import { - DYNAMIC_ROUTES, - IGNORED_ROUTES, - PAGE_METADATA, -} from './next.dynamic.constants.mjs'; +import { PAGE_METADATA } from './next.dynamic.constants.mjs'; import { getMarkdownFiles } from './next.helpers.mjs'; import { siteConfig } from './next.json.mjs'; import { availableLocaleCodes, defaultLocale } from './next.locales.mjs'; @@ -83,18 +79,13 @@ const getDynamicRouter = async () => { /** * This method returns a list of all routes that exist for a given locale + * Note: It will only match routes that have at least one pathname. * * @param {string} locale * @returns {Promise>} */ - const getRoutesByLanguage = async (locale = defaultLocale.code) => { - const shouldIgnoreStaticRoute = pathname => - IGNORED_ROUTES.every(e => !e({ pathname, locale })); - - return [...pathnameToFilename.keys()] - .filter(shouldIgnoreStaticRoute) - .concat([...DYNAMIC_ROUTES.keys()]); - }; + const getRoutesByLanguage = async (locale = defaultLocale.code) => + [...pathnameToFilename.keys()].filter(p => locale.length && p.length); /** * This method attempts to retrieve either a localized Markdown file From 6c8782fdd192bc479f59a2c31b3dad44ca2eb1ce Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sun, 17 Aug 2025 21:48:35 +0200 Subject: [PATCH 2/5] fix: fixed route generation --- .../site/app/[locale]/blog/[...path]/page.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/site/app/[locale]/blog/[...path]/page.tsx b/apps/site/app/[locale]/blog/[...path]/page.tsx index dc0ee721d567a..7655a5f62f780 100644 --- a/apps/site/app/[locale]/blog/[...path]/page.tsx +++ b/apps/site/app/[locale]/blog/[...path]/page.tsx @@ -27,12 +27,22 @@ const BLOG_DYNAMIC_ROUTES = blogData.categories.flatMap(category => { // Each category can have multiple pages, so we generate a route for each page const categoryPages = provideBlogPosts(category).pagination.pages; - const categoryRoutes = Array.from({ length: categoryPages }, (_, page) => ({ + const categoryRoute = { locale: defaultLocale.code, - path: [category, `${category}/page/${page + 1}`], - })); + path: [category], + pathname: `${category}`, + }; + + const categoryPaginationRoutes = Array.from( + { length: categoryPages }, + (_, page) => ({ + locale: defaultLocale.code, + path: [category, 'page', `${page + 1}`], + pathname: `${category}/page/${page + 1}`, + }) + ); - return [{ locale: defaultLocale.code, path: [category] }, ...categoryRoutes]; + return [categoryRoute, ...categoryPaginationRoutes]; }); // This is the default Viewport Metadata @@ -66,7 +76,7 @@ const getPage: FC = async props => { const [locale, pathname] = await basePage.getLocaleAndPath(props); const isDynamicRoute = BLOG_DYNAMIC_ROUTES.some(route => - route.path.includes(pathname) + route.pathname.includes(pathname) ); // Gets the Markdown content and context for Blog pages From f02b01c747af00a08d773f0d722d4fdb8516f1ed Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Sun, 17 Aug 2025 22:21:13 +0200 Subject: [PATCH 3/5] chore: self review changes --- apps/site/app/[locale]/[...path]/page.tsx | 13 ++-- .../site/app/[locale]/blog/[...path]/page.tsx | 39 +++--------- .../app/[locale]/next-data/page-data/route.ts | 4 +- apps/site/app/sitemap.ts | 62 +++++++------------ apps/site/next.dynamic.constants.mjs | 24 +++++++ apps/site/next.dynamic.mjs | 11 ++-- 6 files changed, 64 insertions(+), 89 deletions(-) diff --git a/apps/site/app/[locale]/[...path]/page.tsx b/apps/site/app/[locale]/[...path]/page.tsx index 8bfe7f6094cfe..01d082fc1d1f3 100644 --- a/apps/site/app/[locale]/[...path]/page.tsx +++ b/apps/site/app/[locale]/[...path]/page.tsx @@ -38,12 +38,11 @@ export const generateStaticParams = async () => { return []; } - // Helper function to fetch and map routes for a specific locale - const getRoutesForLocale = async (l: string) => { - const routes = await dynamicRouter.getRoutesByLanguage(l); + const routes = await dynamicRouter.getAllRoutes(); - return routes.map(pathname => dynamicRouter.mapPathToRoute(l, pathname)); - }; + // Helper function to fetch and map routes for a specific locale + const getRoutesForLocale = async (l: string) => + routes.map(pathname => dynamicRouter.mapPathToRoute(l, pathname)); // Determine which locales to include in the static export const locales = ENABLE_STATIC_EXPORT_LOCALE @@ -51,9 +50,9 @@ export const generateStaticParams = async () => { : [defaultLocale.code]; // Generates all possible routes for all available locales - const routes = await Promise.all(locales.map(getRoutesForLocale)); + const routesWithLocales = await Promise.all(locales.map(getRoutesForLocale)); - return routes.flat().sort(); + return routesWithLocales.flat().sort(); }; // This method parses the current pathname and does any sort of modifications needed on the route diff --git a/apps/site/app/[locale]/blog/[...path]/page.tsx b/apps/site/app/[locale]/blog/[...path]/page.tsx index 7655a5f62f780..a94918bd489bd 100644 --- a/apps/site/app/[locale]/blog/[...path]/page.tsx +++ b/apps/site/app/[locale]/blog/[...path]/page.tsx @@ -11,40 +11,13 @@ import { notFound } from 'next/navigation'; import type { FC } from 'react'; import * as basePage from '#site/app/[locale]/page'; -import { provideBlogPosts } from '#site/next-data/providers/blogData'; import { ENABLE_STATIC_EXPORT } from '#site/next.constants.mjs'; -import { blogData } from '#site/next.json.mjs'; +import { BLOG_DYNAMIC_ROUTES } from '#site/next.dynamic.constants.mjs'; import { defaultLocale } from '#site/next.locales.mjs'; type DynamicStaticPaths = { path: Array; locale: string }; type DynamicParams = { params: Promise }; -/** - * This is a list of all static routes or pages from the Website that we do not - * want to allow to be statically built on our Static Export Build. - */ -const BLOG_DYNAMIC_ROUTES = blogData.categories.flatMap(category => { - // Each category can have multiple pages, so we generate a route for each page - const categoryPages = provideBlogPosts(category).pagination.pages; - - const categoryRoute = { - locale: defaultLocale.code, - path: [category], - pathname: `${category}`, - }; - - const categoryPaginationRoutes = Array.from( - { length: categoryPages }, - (_, page) => ({ - locale: defaultLocale.code, - path: [category, 'page', `${page + 1}`], - pathname: `${category}/page/${page + 1}`, - }) - ); - - return [categoryRoute, ...categoryPaginationRoutes]; -}); - // This is the default Viewport Metadata // @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function export const generateViewport = basePage.generateViewport; @@ -64,7 +37,10 @@ export const generateStaticParams = async () => { return []; } - return BLOG_DYNAMIC_ROUTES; + return BLOG_DYNAMIC_ROUTES.map(pathname => ({ + locale: defaultLocale.code, + path: pathname.split('/'), + })); }; // This method parses the current pathname and does any sort of modifications needed on the route @@ -75,9 +51,8 @@ const getPage: FC = async props => { // Gets the current full pathname for a given path const [locale, pathname] = await basePage.getLocaleAndPath(props); - const isDynamicRoute = BLOG_DYNAMIC_ROUTES.some(route => - route.pathname.includes(pathname) - ); + // Verifies if the current route is a dynamic route + const isDynamicRoute = BLOG_DYNAMIC_ROUTES.some(r => r.includes(pathname)); // Gets the Markdown content and context for Blog pages // otherwise this is likely a blog-category or a blog post diff --git a/apps/site/app/[locale]/next-data/page-data/route.ts b/apps/site/app/[locale]/next-data/page-data/route.ts index a9ad5a7e4416c..58d82a27e5993 100644 --- a/apps/site/app/[locale]/next-data/page-data/route.ts +++ b/apps/site/app/[locale]/next-data/page-data/route.ts @@ -11,9 +11,7 @@ import { parseRichTextIntoPlainText } from '#site/util/string'; // @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers export const GET = async () => { // Retrieves all available routes for the default locale - const allAvailbleRoutes = await dynamicRouter.getRoutesByLanguage( - defaultLocale.code - ); + const allAvailbleRoutes = await dynamicRouter.getAllRoutes(); // We exclude the blog routes from the available pages metadata // as they are generated separately and are not part of the static pages diff --git a/apps/site/app/sitemap.ts b/apps/site/app/sitemap.ts index 86b8c337a7290..5fc9257f041f5 100644 --- a/apps/site/app/sitemap.ts +++ b/apps/site/app/sitemap.ts @@ -5,60 +5,40 @@ import { BASE_URL, EXTERNAL_LINKS_SITEMAP, } from '#site/next.constants.mjs'; +import { BLOG_DYNAMIC_ROUTES } from '#site/next.dynamic.constants.mjs'; import { dynamicRouter } from '#site/next.dynamic.mjs'; import { availableLocaleCodes, defaultLocale } from '#site/next.locales.mjs'; // This is the combination of the Application Base URL and Base PATH const baseUrlAndPath = `${BASE_URL}${BASE_PATH}`; +// All available alternate locales +const nonDefaultLocales = availableLocaleCodes.filter( + l => l !== defaultLocale.code +); + +const getAlternatePath = (r: string, locales: Array) => + Object.fromEntries(locales.map(l => [l, `${baseUrlAndPath}/${l}/${r}`])); + // This allows us to generate a `sitemap.xml` file dynamically based on the needs of the Node.js Website const sitemap = async (): Promise => { - const routes = await dynamicRouter.getRoutesByLanguage(defaultLocale.code); - const paths = []; + // Gets a list of all statically available routes + const routes = await dynamicRouter.getAllRoutes(); const currentDate = new Date().toISOString(); - for (const route of routes) { - const availableLocalesForRoute = []; - - for (const locale of availableLocaleCodes.filter( - locale => locale !== defaultLocale.code - )) { - const markdownFile = await dynamicRouter.getMarkdownFile(locale, route); - const isAvailable = markdownFile.filename !== ''; - if (isAvailable) { - availableLocalesForRoute.push(locale); - } - } - - const alternatesPaths = availableLocalesForRoute.reduce( - (acc, locale) => ({ - ...acc, - [locale]: `${baseUrlAndPath}/${locale}/${route}`, - }), - {} - ); + const getSitemapEntry = (r: string, locales: Array = []) => ({ + url: `${baseUrlAndPath}/${defaultLocale.code}/${r}`, + lastModified: currentDate, + changeFrequency: 'always' as const, + alternates: { languages: getAlternatePath(r, locales) }, + }); - paths.push({ - url: `${baseUrlAndPath}/${defaultLocale.code}/${route}`, - lastModified: currentDate, - changeFrequency: 'always' as const, - alternates: { - languages: { - ...alternatesPaths, - }, - }, - }); - } + const staticPaths = routes.map(r => getSitemapEntry(r, nonDefaultLocales)); + const blogPaths = BLOG_DYNAMIC_ROUTES.map(r => getSitemapEntry(r)); + const externalPaths = EXTERNAL_LINKS_SITEMAP.map(r => getSitemapEntry(r)); - return [ - ...paths, - ...EXTERNAL_LINKS_SITEMAP.map(route => ({ - url: route, - lastModified: currentDate, - changeFrequency: 'always' as const, - })), - ]; + return [...staticPaths, ...blogPaths, ...externalPaths]; }; export default sitemap; diff --git a/apps/site/next.dynamic.constants.mjs b/apps/site/next.dynamic.constants.mjs index b96fa7b0cea1d..0eaa0969301ef 100644 --- a/apps/site/next.dynamic.constants.mjs +++ b/apps/site/next.dynamic.constants.mjs @@ -1,8 +1,32 @@ 'use strict'; +import { provideBlogPosts } from '#site/next-data/providers/blogData'; +import { blogData } from '#site/next.json.mjs'; + import { BASE_PATH, BASE_URL } from './next.constants.mjs'; import { siteConfig } from './next.json.mjs'; +/** + * 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 BLOG_DYNAMIC_ROUTES = [ + // Provides Routes for all Blog Categories + ...blogData.categories.map(c => `blog/${c}`), + // Provides Routes for all Blog Categories w/ Pagination + ...blogData.categories + // retrieves the amount of pages for each blog category + .map(c => [c, provideBlogPosts(c).pagination.pages]) + // creates a numeric array for each page and define a pathname for + // each page for a category (i.e. blog/all/page/1) + .map(([c, t]) => [...Array(t).keys()].map(p => `blog/${c}/page/${p + 1}`)) + // flattens the array since we have a .map inside another .map + .flat(), +]; + /** * This is the default Next.js Page Metadata for all pages * diff --git a/apps/site/next.dynamic.mjs b/apps/site/next.dynamic.mjs index 95ff9b9604af7..d63e249f2f1b1 100644 --- a/apps/site/next.dynamic.mjs +++ b/apps/site/next.dynamic.mjs @@ -78,14 +78,13 @@ const getDynamicRouter = async () => { }); /** - * This method returns a list of all routes that exist for a given locale + * This method returns a list of all routes that exist * Note: It will only match routes that have at least one pathname. - * - * @param {string} locale + * @returns {Promise>} */ - const getRoutesByLanguage = async (locale = defaultLocale.code) => - [...pathnameToFilename.keys()].filter(p => locale.length && p.length); + const getAllRoutes = async () => + [...pathnameToFilename.keys()].filter(pathname => pathname.length); /** * This method attempts to retrieve either a localized Markdown file @@ -262,7 +261,7 @@ const getDynamicRouter = async () => { return { mapPathToRoute, getPathname, - getRoutesByLanguage, + getAllRoutes, getMDXContent, getMarkdownFile, getPageMetadata, From 5b3014a41f9d4a855202041a2f94d1fa73905253 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Tue, 19 Aug 2025 00:21:03 +0200 Subject: [PATCH 4/5] fix: fixed static deployment --- .../site/app/[locale]/blog/[...path]/page.tsx | 24 ++++++++++++++++++- apps/site/app/sitemap.ts | 12 ++++------ apps/site/next.dynamic.constants.mjs | 24 ------------------- apps/site/next.dynamic.mjs | 12 ++++------ 4 files changed, 33 insertions(+), 39 deletions(-) diff --git a/apps/site/app/[locale]/blog/[...path]/page.tsx b/apps/site/app/[locale]/blog/[...path]/page.tsx index a94918bd489bd..e4be0336f0034 100644 --- a/apps/site/app/[locale]/blog/[...path]/page.tsx +++ b/apps/site/app/[locale]/blog/[...path]/page.tsx @@ -11,13 +11,35 @@ import { notFound } from 'next/navigation'; import type { FC } from 'react'; import * as basePage from '#site/app/[locale]/page'; +import { provideBlogPosts } from '#site/next-data/providers/blogData'; import { ENABLE_STATIC_EXPORT } from '#site/next.constants.mjs'; -import { BLOG_DYNAMIC_ROUTES } from '#site/next.dynamic.constants.mjs'; +import { blogData } from '#site/next.json.mjs'; import { defaultLocale } from '#site/next.locales.mjs'; type DynamicStaticPaths = { path: Array; locale: string }; type DynamicParams = { params: Promise }; +/** + * 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 BLOG_DYNAMIC_ROUTES = [ + // Provides Routes for all Blog Categories + ...blogData.categories, + // Provides Routes for all Blog Categories w/ Pagination + ...blogData.categories + // retrieves the amount of pages for each blog category + .map(c => [c, provideBlogPosts(c).pagination.pages]) + // creates a numeric array for each page and define a pathname for + // each page for a category (i.e. blog/all/page/1) + .map(([c, t]) => [...Array(t).keys()].map(p => `${c}/page/${p + 1}`)) + // flattens the array since we have a .map inside another .map + .flat(), +]; + // This is the default Viewport Metadata // @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function export const generateViewport = basePage.generateViewport; diff --git a/apps/site/app/sitemap.ts b/apps/site/app/sitemap.ts index 5fc9257f041f5..167b8b316876f 100644 --- a/apps/site/app/sitemap.ts +++ b/apps/site/app/sitemap.ts @@ -1,11 +1,9 @@ import type { MetadataRoute } from 'next'; -import { - BASE_PATH, - BASE_URL, - EXTERNAL_LINKS_SITEMAP, -} from '#site/next.constants.mjs'; -import { BLOG_DYNAMIC_ROUTES } from '#site/next.dynamic.constants.mjs'; +import { BLOG_DYNAMIC_ROUTES } from '#site/app/[locale]/blog/[...path]/page'; +import { BASE_PATH } from '#site/next.constants.mjs'; +import { BASE_URL } from '#site/next.constants.mjs'; +import { EXTERNAL_LINKS_SITEMAP } from '#site/next.constants.mjs'; import { dynamicRouter } from '#site/next.dynamic.mjs'; import { availableLocaleCodes, defaultLocale } from '#site/next.locales.mjs'; @@ -35,7 +33,7 @@ const sitemap = async (): Promise => { }); const staticPaths = routes.map(r => getSitemapEntry(r, nonDefaultLocales)); - const blogPaths = BLOG_DYNAMIC_ROUTES.map(r => getSitemapEntry(r)); + const blogPaths = BLOG_DYNAMIC_ROUTES.map(r => getSitemapEntry(`blog/${r}`)); const externalPaths = EXTERNAL_LINKS_SITEMAP.map(r => getSitemapEntry(r)); return [...staticPaths, ...blogPaths, ...externalPaths]; diff --git a/apps/site/next.dynamic.constants.mjs b/apps/site/next.dynamic.constants.mjs index 0eaa0969301ef..b96fa7b0cea1d 100644 --- a/apps/site/next.dynamic.constants.mjs +++ b/apps/site/next.dynamic.constants.mjs @@ -1,32 +1,8 @@ 'use strict'; -import { provideBlogPosts } from '#site/next-data/providers/blogData'; -import { blogData } from '#site/next.json.mjs'; - import { BASE_PATH, BASE_URL } from './next.constants.mjs'; import { siteConfig } from './next.json.mjs'; -/** - * 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 BLOG_DYNAMIC_ROUTES = [ - // Provides Routes for all Blog Categories - ...blogData.categories.map(c => `blog/${c}`), - // Provides Routes for all Blog Categories w/ Pagination - ...blogData.categories - // retrieves the amount of pages for each blog category - .map(c => [c, provideBlogPosts(c).pagination.pages]) - // creates a numeric array for each page and define a pathname for - // each page for a category (i.e. blog/all/page/1) - .map(([c, t]) => [...Array(t).keys()].map(p => `blog/${c}/page/${p + 1}`)) - // flattens the array since we have a .map inside another .map - .flat(), -]; - /** * This is the default Next.js Page Metadata for all pages * diff --git a/apps/site/next.dynamic.mjs b/apps/site/next.dynamic.mjs index d63e249f2f1b1..4b80852e12d9b 100644 --- a/apps/site/next.dynamic.mjs +++ b/apps/site/next.dynamic.mjs @@ -7,13 +7,11 @@ import matter from 'gray-matter'; import { cache } from 'react'; import { VFile } from 'vfile'; -import { - BASE_PATH, - BASE_URL, - DEFAULT_CATEGORY_OG_TYPE, - ENABLE_STATIC_EXPORT, - IS_DEV_ENV, -} from './next.constants.mjs'; +import { BASE_PATH } from './next.constants.mjs'; +import { BASE_URL } from './next.constants.mjs'; +import { DEFAULT_CATEGORY_OG_TYPE } from './next.constants.mjs'; +import { ENABLE_STATIC_EXPORT } from './next.constants.mjs'; +import { IS_DEV_ENV } from './next.constants.mjs'; import { PAGE_METADATA } from './next.dynamic.constants.mjs'; import { getMarkdownFiles } from './next.helpers.mjs'; import { siteConfig } from './next.json.mjs'; From 254de27e46b815532a96feb2ebd7c441c308349e Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Tue, 19 Aug 2025 00:42:04 +0200 Subject: [PATCH 5/5] chore: more cleanup and improvements --- apps/site/app/[locale]/[...path]/page.tsx | 8 +- .../site/app/[locale]/blog/[...path]/page.tsx | 43 +----- apps/site/app/[locale]/page.tsx | 133 +++--------------- apps/site/app/sitemap.ts | 2 +- apps/site/next.dynamic.constants.mjs | 24 ++++ apps/site/next.dynamic.page.mjs | 119 ++++++++++++++++ 6 files changed, 175 insertions(+), 154 deletions(-) create mode 100644 apps/site/next.dynamic.page.mjs diff --git a/apps/site/app/[locale]/[...path]/page.tsx b/apps/site/app/[locale]/[...path]/page.tsx index 01d082fc1d1f3..68c835fad09b6 100644 --- a/apps/site/app/[locale]/[...path]/page.tsx +++ b/apps/site/app/[locale]/[...path]/page.tsx @@ -10,10 +10,10 @@ import { notFound } from 'next/navigation'; import type { FC } from 'react'; -import * as basePage from '#site/app/[locale]/page'; import { ENABLE_STATIC_EXPORT } from '#site/next.constants.mjs'; import { ENABLE_STATIC_EXPORT_LOCALE } from '#site/next.constants.mjs'; import { dynamicRouter } from '#site/next.dynamic.mjs'; +import * as basePage from '#site/next.dynamic.page.mjs'; import { availableLocaleCodes, defaultLocale } from '#site/next.locales.mjs'; type DynamicStaticPaths = { path: Array; locale: string }; @@ -64,10 +64,10 @@ const getPage: FC = async props => { const [locale, pathname] = await basePage.getLocaleAndPath(props); // Gets the Markdown content and context - const [content, context] = await basePage.getMarkdownContext( + const [content, context] = await basePage.getMarkdownContext({ locale, - pathname - ); + pathname, + }); // If we have a filename and layout then we have a page if (context.filename && context.frontmatter.layout) { diff --git a/apps/site/app/[locale]/blog/[...path]/page.tsx b/apps/site/app/[locale]/blog/[...path]/page.tsx index e4be0336f0034..f8fd5ea520fb5 100644 --- a/apps/site/app/[locale]/blog/[...path]/page.tsx +++ b/apps/site/app/[locale]/blog/[...path]/page.tsx @@ -1,45 +1,14 @@ -/** - * This file extends on the `page.tsx` file, which is the default file that is used to render - * the entry points for each locale and then also reused within the [...path] route to render the - * and contains all logic for rendering our dynamic and static routes within the Node.js Website. - * - * Note: that each `page.tsx` should have its own `generateStaticParams` to prevent clash of - * dynamic params, which will lead on static export errors and other sort of issues. - */ - import { notFound } from 'next/navigation'; import type { FC } from 'react'; -import * as basePage from '#site/app/[locale]/page'; -import { provideBlogPosts } from '#site/next-data/providers/blogData'; import { ENABLE_STATIC_EXPORT } from '#site/next.constants.mjs'; -import { blogData } from '#site/next.json.mjs'; +import { BLOG_DYNAMIC_ROUTES } from '#site/next.dynamic.constants.mjs'; +import * as basePage from '#site/next.dynamic.page.mjs'; import { defaultLocale } from '#site/next.locales.mjs'; type DynamicStaticPaths = { path: Array; locale: string }; type DynamicParams = { params: Promise }; -/** - * 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 BLOG_DYNAMIC_ROUTES = [ - // Provides Routes for all Blog Categories - ...blogData.categories, - // Provides Routes for all Blog Categories w/ Pagination - ...blogData.categories - // retrieves the amount of pages for each blog category - .map(c => [c, provideBlogPosts(c).pagination.pages]) - // creates a numeric array for each page and define a pathname for - // each page for a category (i.e. blog/all/page/1) - .map(([c, t]) => [...Array(t).keys()].map(p => `${c}/page/${p + 1}`)) - // flattens the array since we have a .map inside another .map - .flat(), -]; - // This is the default Viewport Metadata // @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function export const generateViewport = basePage.generateViewport; @@ -78,10 +47,10 @@ const getPage: FC = async props => { // Gets the Markdown content and context for Blog pages // otherwise this is likely a blog-category or a blog post - const [content, context] = await basePage.getMarkdownContext( - locale, - `blog/${pathname}` - ); + const [content, context] = await basePage.getMarkdownContext({ + locale: locale, + pathname: `blog/${pathname}`, + }); // If this isn't a valid dynamic route for blog post or there's no mardown file // for this, then we fail as not found as there's nothing we can do. diff --git a/apps/site/app/[locale]/page.tsx b/apps/site/app/[locale]/page.tsx index f7ae657ec9cfa..482d917122c3e 100644 --- a/apps/site/app/[locale]/page.tsx +++ b/apps/site/app/[locale]/page.tsx @@ -1,56 +1,31 @@ -/** - * This file contains the logic for rendering our dynamic and static routes within the Node.js Website - * this page route template is used to render the entry points for each locale and then also reused within - * the [...path] route to render the individual pages under each locale of the Website. - * - * Note: that each `page.tsx` should have its own `generateStaticParams` to prevent clash of - * dynamic params, which will lead on static export errors and other sort of issues. - */ +import { notFound } from 'next/navigation'; +import type { FC } from 'react'; -import { notFound, redirect } from 'next/navigation'; -import { setRequestLocale } from 'next-intl/server'; -import type { FC, ReactNode } from 'react'; - -import { setClientContext } from '#site/client-context'; -import WithLayout from '#site/components/withLayout'; import { ENABLE_STATIC_EXPORT } from '#site/next.constants.mjs'; import { ENABLE_STATIC_EXPORT_LOCALE } from '#site/next.constants.mjs'; -import { PAGE_VIEWPORT } from '#site/next.dynamic.constants.mjs'; -import { dynamicRouter } from '#site/next.dynamic.mjs'; -import { allLocaleCodes, availableLocaleCodes } from '#site/next.locales.mjs'; +import * as basePage from '#site/next.dynamic.page.mjs'; +import { availableLocaleCodes } from '#site/next.locales.mjs'; import { defaultLocale } from '#site/next.locales.mjs'; -import { MatterProvider } from '#site/providers/matterProvider'; -import type { Layouts } from '#site/types/layouts'; -import type { ClientSharedServerContext } from '#site/types/server'; type DynamicStaticPaths = { path: Array; locale: string }; type DynamicParams = { params: Promise }; -type DynamicPageRender = { - content: ReactNode; - layout: Layouts; - context: Partial; -}; - // This is the default Viewport Metadata // @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function -export const generateViewport = () => ({ ...PAGE_VIEWPORT }); +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 = async (props: DynamicParams) => { - const { path = [], locale = defaultLocale.code } = await props.params; - - const pathname = dynamicRouter.getPathname(path); - - return dynamicRouter.getPageMetadata(locale, pathname); -}; +export const generateMetadata = basePage.generateMetadata; -// 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) -// - If `ENABLE_STATIC_EXPORT_LOCALE` is true, generates paths for all available locales -// - Otherwise, generates paths only for the default locale -// @see https://nextjs.org/docs/app/api-reference/functions/generate-static-params +/** + * 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) + * - If `ENABLE_STATIC_EXPORT_LOCALE` is true, generates paths for all available locales + * - Otherwise, generates paths only for the default locale + * + * @see https://nextjs.org/docs/app/api-reference/functions/generate-static-params + */ export const generateStaticParams = async () => { // Return an empty array if static export is disabled if (!ENABLE_STATIC_EXPORT) { @@ -64,95 +39,29 @@ export const generateStaticParams = async () => { const routes = await Promise.all( // Gets all mapped routes to the Next.js Routing Engine by Locale - locales.map((locale: string) => ({ locale })) + locales.map(locale => ({ locale })) ); return routes.flat().sort(); }; -// This method is used for retrieving the current locale and pathname from the request -export const getLocaleAndPath = async (props: DynamicParams) => { - const { path = [], locale = defaultLocale.code } = await props.params; - - if (!availableLocaleCodes.includes(locale)) { - // Forces the current locale to be the Default Locale - setRequestLocale(defaultLocale.code); - - if (!allLocaleCodes.includes(locale)) { - // when the locale is not listed in the locales, return NotFound - return notFound(); - } - - // Redirect to the default locale path - const pathname = dynamicRouter.getPathname(path); - - return redirect(`/${defaultLocale.code}/${pathname}`); - } - - // Configures the current Locale to be the given Locale of the Request - setRequestLocale(locale); - - // Gets the current full pathname for a given path - return [locale, dynamicRouter.getPathname(path)] as const; -}; - -// This method is used for retrieving the Markdown content and context -export const getMarkdownContext = async (locale: string, pathname: string) => { - // 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, - pathname - ); - - // This parses the source Markdown content and returns a React Component and - // relevant context from the Markdown File - const { content, frontmatter, headings, readingTime } = - await dynamicRouter.getMDXContent(source, filename); - - // Metadata and shared Context to be available through the lifecycle of the page - const context = { - frontmatter: frontmatter, - headings: headings, - pathname: `/${pathname}`, - readingTime: readingTime, - filename: filename, - }; - - return [content, context] as const; -}; - -// This method is used for rendering the actual page -export const renderPage: FC = props => { - // Defines a shared Server Context for the Client-Side - // That is shared for all pages under the dynamic router - setClientContext(props.context); - - // The Matter Provider allows Client-Side injection of the data - // to a shared React Client Provider even though the page is rendered - // within a server-side context - return ( - - {props.content} - - ); -}; - // 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 = async props => { // Gets the current full pathname for a given path - const [locale, pathname] = await getLocaleAndPath(props); + const [locale, pathname] = await basePage.getLocaleAndPath(props); // Gets the Markdown content and context - const [content, context] = await getMarkdownContext(locale, pathname); + const [content, context] = await basePage.getMarkdownContext({ + locale, + pathname, + }); // If we have a filename and layout then we have a page if (context.filename && context.frontmatter.layout) { - return renderPage({ + return basePage.renderPage({ content: content, layout: context.frontmatter.layout, context: context, diff --git a/apps/site/app/sitemap.ts b/apps/site/app/sitemap.ts index 167b8b316876f..f53d2c65d9569 100644 --- a/apps/site/app/sitemap.ts +++ b/apps/site/app/sitemap.ts @@ -1,9 +1,9 @@ import type { MetadataRoute } from 'next'; -import { BLOG_DYNAMIC_ROUTES } from '#site/app/[locale]/blog/[...path]/page'; import { BASE_PATH } from '#site/next.constants.mjs'; import { BASE_URL } from '#site/next.constants.mjs'; import { EXTERNAL_LINKS_SITEMAP } from '#site/next.constants.mjs'; +import { BLOG_DYNAMIC_ROUTES } from '#site/next.dynamic.constants.mjs'; import { dynamicRouter } from '#site/next.dynamic.mjs'; import { availableLocaleCodes, defaultLocale } from '#site/next.locales.mjs'; diff --git a/apps/site/next.dynamic.constants.mjs b/apps/site/next.dynamic.constants.mjs index b96fa7b0cea1d..ce9f3128e0077 100644 --- a/apps/site/next.dynamic.constants.mjs +++ b/apps/site/next.dynamic.constants.mjs @@ -1,8 +1,32 @@ 'use strict'; +import { provideBlogPosts } from '#site/next-data/providers/blogData'; +import { blogData } from '#site/next.json.mjs'; + import { BASE_PATH, BASE_URL } from './next.constants.mjs'; import { siteConfig } from './next.json.mjs'; +/** + * 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 BLOG_DYNAMIC_ROUTES = [ + // Provides Routes for all Blog Categories + ...blogData.categories, + // Provides Routes for all Blog Categories w/ Pagination + ...blogData.categories + // retrieves the amount of pages for each blog category + .map(c => [c, provideBlogPosts(c).pagination.pages]) + // creates a numeric array for each page and define a pathname for + // each page for a category (i.e. blog/all/page/1) + .map(([c, t]) => [...Array(t).keys()].map(p => `${c}/page/${p + 1}`)) + // flattens the array since we have a .map inside another .map + .flat(), +]; + /** * 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 new file mode 100644 index 0000000000000..53c583ea29030 --- /dev/null +++ b/apps/site/next.dynamic.page.mjs @@ -0,0 +1,119 @@ +import { notFound, redirect } from 'next/navigation'; +import { setRequestLocale } from 'next-intl/server'; + +import { setClientContext } from '#site/client-context'; +import WithLayout from '#site/components/withLayout'; +import { PAGE_VIEWPORT } from '#site/next.dynamic.constants.mjs'; +import { dynamicRouter } from '#site/next.dynamic.mjs'; +import { allLocaleCodes, availableLocaleCodes } from '#site/next.locales.mjs'; +import { defaultLocale } from '#site/next.locales.mjs'; +import { MatterProvider } from '#site/providers/matterProvider'; + +/** + * This is the default Viewport Metadata + * + * @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function + * + * @returns {import('next').Viewport} the default viewport metadata + */ +export const generateViewport = () => ({ ...PAGE_VIEWPORT }); + +/** + * This generates each page's HTML Metadata + * + * @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata + * + * @param {{ params: Promise<{ path: Array; locale: string }> }} props + * @returns {Promise} the metadata for the page + */ +export const generateMetadata = async props => { + const { path = [], locale = defaultLocale.code } = await props.params; + + const pathname = dynamicRouter.getPathname(path); + + return dynamicRouter.getPageMetadata(locale, pathname); +}; + +/** + * This method is used for retrieving the current locale and pathname from the request + * + * @param {{ params: Promise<{ path: Array; locale: string }> }} props + * @returns {Promise<[string, string]>} the locale and pathname for the request + */ +export const getLocaleAndPath = async props => { + const { path = [], locale = defaultLocale.code } = await props.params; + + if (!availableLocaleCodes.includes(locale)) { + // Forces the current locale to be the Default Locale + setRequestLocale(defaultLocale.code); + + if (!allLocaleCodes.includes(locale)) { + // when the locale is not listed in the locales, return NotFound + return notFound(); + } + + // Redirect to the default locale path + const pathname = dynamicRouter.getPathname(path); + + return redirect(`/${defaultLocale.code}/${pathname}`); + } + + // Configures the current Locale to be the given Locale of the Request + setRequestLocale(locale); + + // Gets the current full pathname for a given path + return [locale, dynamicRouter.getPathname(path)]; +}; + +/** + * This method is used for retrieving the Markdown content and context + * + * @param {{ locale: string; pathname: string }} props + * @returns {Promise<[import('react').ReactNode, import('#site/types/server').ClientSharedServerContext]>} + */ +export const getMarkdownContext = async props => { + // 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( + props.locale, + props.pathname + ); + + // This parses the source Markdown content and returns a React Component and + // relevant context from the Markdown File + const { content, frontmatter, headings, readingTime } = + await dynamicRouter.getMDXContent(source, filename); + + // Metadata and shared Context to be available through the lifecycle of the page + const context = { + frontmatter: frontmatter, + headings: headings, + pathname: `/${props.pathname}`, + readingTime: readingTime, + filename: filename, + }; + + return [content, context]; +}; + +/** + * This method is used for rendering the actual page + * + * @param {{ content: import('react').ReactNode; layout: import('#site/types/layouts').Layouts; context: Partial; }} props + * @returns {import('react').ReactElement} + */ +export const renderPage = props => { + // Defines a shared Server Context for the Client-Side + // That is shared for all pages under the dynamic router + setClientContext(props.context); + + // The Matter Provider allows Client-Side injection of the data + // to a shared React Client Provider even though the page is rendered + // within a server-side context + return ( + + {props.content} + + ); +};