Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
48 changes: 38 additions & 10 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 @@ -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
Expand All @@ -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<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 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
Expand All @@ -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;
112 changes: 112 additions & 0 deletions apps/site/app/[locale]/blog/[...path]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* 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<string>; locale: string };
type DynamicParams = { params: Promise<DynamicStaticPaths> };

/**
* 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;

// 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<DynamicParams> = 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)
);

// 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;
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