Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 41 additions & 14 deletions apps/site/app/[locale]/[...path]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>; locale: string };
type DynamicParams = { params: Promise<DynamicStaticPaths> };

// This is the default Viewport Metadata
// @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function
export const generateViewport = basePage.generateViewport;
Expand All @@ -34,24 +38,47 @@ export const generateStaticParams = async () => {
return [];
}

// Helper function to fetch and map routes for a specific locale
const getRoutesForLocale = async (locale: string) => {
const routes = await dynamicRouter.getRoutesByLanguage(locale);
const routes = await dynamicRouter.getAllRoutes();

return routes.map(pathname =>
dynamicRouter.mapPathToRoute(locale, 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
? availableLocaleCodes
: [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 routesWithLocales.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<DynamicParams> = 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 routes.flat().sort();
return notFound();
};

// Enforces that this route is used as static rendering
Expand All @@ -64,4 +91,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;
87 changes: 87 additions & 0 deletions apps/site/app/[locale]/blog/[...path]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* 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 { ENABLE_STATIC_EXPORT } from '#site/next.constants.mjs';
import { BLOG_DYNAMIC_ROUTES } from '#site/next.dynamic.constants.mjs';
import { defaultLocale } from '#site/next.locales.mjs';

type DynamicStaticPaths = { path: Array<string>; locale: string };
type DynamicParams = { params: Promise<DynamicStaticPaths> };

// 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.map(pathname => ({
locale: defaultLocale.code,
path: pathname.split('/'),
}));
};

// 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<DynamicParams> = async props => {
// Gets the current full pathname for a given path
const [locale, pathname] = await basePage.getLocaleAndPath(props);

// 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
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;
4 changes: 1 addition & 3 deletions apps/site/app/[locale]/next-data/page-data/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
134 changes: 69 additions & 65 deletions apps/site/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>; locale: string };
type DynamicParams = { params: Promise<DynamicStaticPaths> };

type DynamicPageRender = {
content: ReactNode;
layout: Layouts;
context: Partial<ClientSharedServerContext>;
};

// This is the default Viewport Metadata
// @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function
export const generateViewport = () => ({ ...PAGE_VIEWPORT });
Expand Down Expand Up @@ -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<DynamicParams> = 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)) {
Expand All @@ -93,31 +93,11 @@ const getPage: FC<DynamicParams> = 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 (
<MatterProvider {...sharedContext}>
<WithLayout layout={staticGeneratedLayout} />
</MatterProvider>
);
}
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
Expand All @@ -126,33 +106,57 @@ const getPage: FC<DynamicParams> = 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 (
<MatterProvider {...sharedContext}>
<WithLayout layout={frontmatter.layout}>{content}</WithLayout>
</MatterProvider>
);
// 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<DynamicPageRender> = 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 (
<MatterProvider {...props.context}>
<WithLayout layout={props.layout}>{props.content}</WithLayout>
</MatterProvider>
);
};

// 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<DynamicParams> = 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();
Expand Down
Loading
Loading