Skip to content

Commit dd33d07

Browse files
committed
chore: get rid of hacky dynamic pages, use proper app router
1 parent 8ab9cea commit dd33d07

5 files changed

Lines changed: 213 additions & 129 deletions

File tree

apps/site/app/[locale]/[...path]/page.tsx

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@
77
* dynamic params, which will lead on static export errors and other sort of issues.
88
*/
99

10+
import { notFound } from 'next/navigation';
11+
import type { FC } from 'react';
12+
1013
import * as basePage from '#site/app/[locale]/page';
11-
import {
12-
ENABLE_STATIC_EXPORT_LOCALE,
13-
ENABLE_STATIC_EXPORT,
14-
} from '#site/next.constants.mjs';
14+
import { ENABLE_STATIC_EXPORT } from '#site/next.constants.mjs';
15+
import { ENABLE_STATIC_EXPORT_LOCALE } from '#site/next.constants.mjs';
1516
import { dynamicRouter } from '#site/next.dynamic.mjs';
1617
import { availableLocaleCodes, defaultLocale } from '#site/next.locales.mjs';
1718

19+
type DynamicStaticPaths = { path: Array<string>; locale: string };
20+
type DynamicParams = { params: Promise<DynamicStaticPaths> };
21+
1822
// This is the default Viewport Metadata
1923
// @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function
2024
export const generateViewport = basePage.generateViewport;
@@ -35,12 +39,10 @@ export const generateStaticParams = async () => {
3539
}
3640

3741
// Helper function to fetch and map routes for a specific locale
38-
const getRoutesForLocale = async (locale: string) => {
39-
const routes = await dynamicRouter.getRoutesByLanguage(locale);
42+
const getRoutesForLocale = async (l: string) => {
43+
const routes = await dynamicRouter.getRoutesByLanguage(l);
4044

41-
return routes.map(pathname =>
42-
dynamicRouter.mapPathToRoute(locale, pathname)
43-
);
45+
return routes.map(pathname => dynamicRouter.mapPathToRoute(l, pathname));
4446
};
4547

4648
// Determine which locales to include in the static export
@@ -54,6 +56,32 @@ export const generateStaticParams = async () => {
5456
return routes.flat().sort();
5557
};
5658

59+
// This method parses the current pathname and does any sort of modifications needed on the route
60+
// then it proceeds to retrieve the Markdown file and parse the MDX Content into a React Component
61+
// finally it returns (if the locale and route are valid) the React Component with the relevant context
62+
// and attached context providers for rendering the current page
63+
const getPage: FC<DynamicParams> = async props => {
64+
// Gets the current full pathname for a given path
65+
const [locale, pathname] = await basePage.getLocaleAndPath(props);
66+
67+
// Gets the Markdown content and context
68+
const [content, context] = await basePage.getMarkdownContext(
69+
locale,
70+
pathname
71+
);
72+
73+
// If we have a filename and layout then we have a page
74+
if (context.filename && context.frontmatter.layout) {
75+
return basePage.renderPage({
76+
content: content,
77+
layout: context.frontmatter.layout,
78+
context: context,
79+
});
80+
}
81+
82+
return notFound();
83+
};
84+
5785
// Enforces that this route is used as static rendering
5886
// Except whenever on the Development mode as we want instant-refresh when making changes
5987
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
@@ -64,4 +92,4 @@ export const dynamic = 'force-static';
6492
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#revalidate
6593
export const revalidate = 300;
6694

67-
export default basePage.default;
95+
export default getPage;
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* This file extends on the `page.tsx` file, which is the default file that is used to render
3+
* the entry points for each locale and then also reused within the [...path] route to render the
4+
* and contains all logic for rendering our dynamic and static routes within the Node.js Website.
5+
*
6+
* Note: that each `page.tsx` should have its own `generateStaticParams` to prevent clash of
7+
* dynamic params, which will lead on static export errors and other sort of issues.
8+
*/
9+
10+
import { notFound } from 'next/navigation';
11+
import type { FC } from 'react';
12+
13+
import * as basePage from '#site/app/[locale]/page';
14+
import { provideBlogPosts } from '#site/next-data/providers/blogData';
15+
import { ENABLE_STATIC_EXPORT } from '#site/next.constants.mjs';
16+
import { blogData } from '#site/next.json.mjs';
17+
import { defaultLocale } from '#site/next.locales.mjs';
18+
19+
type DynamicStaticPaths = { path: Array<string>; locale: string };
20+
type DynamicParams = { params: Promise<DynamicStaticPaths> };
21+
22+
/**
23+
* This is a list of all static routes or pages from the Website that we do not
24+
* want to allow to be statically built on our Static Export Build.
25+
*/
26+
const BLOG_DYNAMIC_ROUTES = blogData.categories.flatMap(category => {
27+
// Each category can have multiple pages, so we generate a route for each page
28+
const categoryPages = provideBlogPosts(category).pagination.pages;
29+
30+
const categoryRoutes = Array.from({ length: categoryPages }, (_, page) => ({
31+
locale: defaultLocale.code,
32+
path: [category, `${category}/page/${page + 1}`],
33+
}));
34+
35+
return [{ locale: defaultLocale.code, path: [category] }, ...categoryRoutes];
36+
});
37+
38+
// This is the default Viewport Metadata
39+
// @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function
40+
export const generateViewport = basePage.generateViewport;
41+
42+
// This generates each page's HTML Metadata
43+
// @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata
44+
export const generateMetadata = basePage.generateMetadata;
45+
46+
// Generates all possible static paths based on the locales and environment configuration
47+
// - Returns an empty array if static export is disabled (`ENABLE_STATIC_EXPORT` is false)
48+
// - If `ENABLE_STATIC_EXPORT_LOCALE` is true, generates paths for all available locales
49+
// - Otherwise, generates paths only for the default locale
50+
// @see https://nextjs.org/docs/app/api-reference/functions/generate-static-params
51+
export const generateStaticParams = async () => {
52+
// Return an empty array if static export is disabled
53+
if (!ENABLE_STATIC_EXPORT) {
54+
return [];
55+
}
56+
57+
return BLOG_DYNAMIC_ROUTES;
58+
};
59+
60+
// This method parses the current pathname and does any sort of modifications needed on the route
61+
// then it proceeds to retrieve the Markdown file and parse the MDX Content into a React Component
62+
// finally it returns (if the locale and route are valid) the React Component with the relevant context
63+
// and attached context providers for rendering the current page
64+
const getPage: FC<DynamicParams> = async props => {
65+
// Gets the current full pathname for a given path
66+
const [locale, pathname] = await basePage.getLocaleAndPath(props);
67+
68+
const isDynamicRoute = BLOG_DYNAMIC_ROUTES.some(route =>
69+
route.path.includes(pathname)
70+
);
71+
72+
// Gets the Markdown content and context for Blog pages
73+
// otherwise this is likely a blog-category or a blog post
74+
const [content, context] = await basePage.getMarkdownContext(
75+
locale,
76+
`blog/${pathname}`
77+
);
78+
79+
// If this isn't a valid dynamic route for blog post or there's no mardown file
80+
// for this, then we fail as not found as there's nothing we can do.
81+
if (isDynamicRoute || context.filename) {
82+
return basePage.renderPage({
83+
content: content,
84+
layout: context.frontmatter.layout ?? 'blog-category',
85+
context: { ...context, pathname: `/blog/${pathname}` },
86+
});
87+
}
88+
89+
return notFound();
90+
};
91+
92+
// Enforces that this route is used as static rendering
93+
// Except whenever on the Development mode as we want instant-refresh when making changes
94+
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
95+
export const dynamic = 'force-static';
96+
97+
// Ensures that this endpoint is invalidated and re-executed every X minutes
98+
// so that when new deployments happen, the data is refreshed
99+
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#revalidate
100+
export const revalidate = 300;
101+
102+
export default getPage;

apps/site/app/[locale]/page.tsx

Lines changed: 69 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,29 @@
99

1010
import { notFound, redirect } from 'next/navigation';
1111
import { setRequestLocale } from 'next-intl/server';
12-
import type { FC } from 'react';
12+
import type { FC, ReactNode } from 'react';
1313

1414
import { setClientContext } from '#site/client-context';
1515
import WithLayout from '#site/components/withLayout';
16-
import {
17-
ENABLE_STATIC_EXPORT_LOCALE,
18-
ENABLE_STATIC_EXPORT,
19-
} from '#site/next.constants.mjs';
20-
import {
21-
PAGE_VIEWPORT,
22-
DYNAMIC_ROUTES,
23-
} from '#site/next.dynamic.constants.mjs';
16+
import { ENABLE_STATIC_EXPORT } from '#site/next.constants.mjs';
17+
import { ENABLE_STATIC_EXPORT_LOCALE } from '#site/next.constants.mjs';
18+
import { PAGE_VIEWPORT } from '#site/next.dynamic.constants.mjs';
2419
import { dynamicRouter } from '#site/next.dynamic.mjs';
2520
import { allLocaleCodes, availableLocaleCodes } from '#site/next.locales.mjs';
2621
import { defaultLocale } from '#site/next.locales.mjs';
2722
import { MatterProvider } from '#site/providers/matterProvider';
23+
import type { Layouts } from '#site/types/layouts';
24+
import type { ClientSharedServerContext } from '#site/types/server';
2825

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

29+
type DynamicPageRender = {
30+
content: ReactNode;
31+
layout: Layouts;
32+
context: Partial<ClientSharedServerContext>;
33+
};
34+
3235
// This is the default Viewport Metadata
3336
// @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function
3437
export const generateViewport = () => ({ ...PAGE_VIEWPORT });
@@ -67,11 +70,8 @@ export const generateStaticParams = async () => {
6770
return routes.flat().sort();
6871
};
6972

70-
// This method parses the current pathname and does any sort of modifications needed on the route
71-
// then it proceeds to retrieve the Markdown file and parse the MDX Content into a React Component
72-
// finally it returns (if the locale and route are valid) the React Component with the relevant context
73-
// and attached context providers for rendering the current page
74-
const getPage: FC<DynamicParams> = async props => {
73+
// This method is used for retrieving the current locale and pathname from the request
74+
export const getLocaleAndPath = async (props: DynamicParams) => {
7575
const { path = [], locale = defaultLocale.code } = await props.params;
7676

7777
if (!availableLocaleCodes.includes(locale)) {
@@ -93,31 +93,11 @@ const getPage: FC<DynamicParams> = async props => {
9393
setRequestLocale(locale);
9494

9595
// Gets the current full pathname for a given path
96-
const pathname = dynamicRouter.getPathname(path);
97-
98-
const staticGeneratedLayout = DYNAMIC_ROUTES.get(pathname);
99-
100-
// If the current pathname is a statically generated route
101-
// it means it does not have a Markdown file nor exists under the filesystem
102-
// but it is a valid route with an assigned layout that should be rendered
103-
if (staticGeneratedLayout !== undefined) {
104-
// Metadata and shared Context to be available through the lifecycle of the page
105-
const sharedContext = { pathname: `/${pathname}` };
106-
107-
// Defines a shared Server Context for the Client-Side
108-
// That is shared for all pages under the dynamic router
109-
setClientContext(sharedContext);
110-
111-
// The Matter Provider allows Client-Side injection of the data
112-
// to a shared React Client Provider even though the page is rendered
113-
// within a server-side context
114-
return (
115-
<MatterProvider {...sharedContext}>
116-
<WithLayout layout={staticGeneratedLayout} />
117-
</MatterProvider>
118-
);
119-
}
96+
return [locale, dynamicRouter.getPathname(path)] as const;
97+
};
12098

99+
// This method is used for retrieving the Markdown content and context
100+
export const getMarkdownContext = async (locale: string, pathname: string) => {
121101
// We retrieve the source of the Markdown file by doing an educated guess
122102
// of what possible files could be the source of the page, since the extension
123103
// context is lost from `getStaticProps` as a limitation of Next.js itself
@@ -126,33 +106,57 @@ const getPage: FC<DynamicParams> = async props => {
126106
pathname
127107
);
128108

129-
if (source.length && filename.length) {
130-
// This parses the source Markdown content and returns a React Component and
131-
// relevant context from the Markdown File
132-
const { content, frontmatter, headings, readingTime } =
133-
await dynamicRouter.getMDXContent(source, filename);
134-
135-
// Metadata and shared Context to be available through the lifecycle of the page
136-
const sharedContext = {
137-
frontmatter: frontmatter,
138-
headings: headings,
139-
pathname: `/${pathname}`,
140-
readingTime: readingTime,
141-
filename: filename,
142-
};
143-
144-
// Defines a shared Server Context for the Client-Side
145-
// That is shared for all pages under the dynamic router
146-
setClientContext(sharedContext);
147-
148-
// The Matter Provider allows Client-Side injection of the data
149-
// to a shared React Client Provider even though the page is rendered
150-
// within a server-side context
151-
return (
152-
<MatterProvider {...sharedContext}>
153-
<WithLayout layout={frontmatter.layout}>{content}</WithLayout>
154-
</MatterProvider>
155-
);
109+
// This parses the source Markdown content and returns a React Component and
110+
// relevant context from the Markdown File
111+
const { content, frontmatter, headings, readingTime } =
112+
await dynamicRouter.getMDXContent(source, filename);
113+
114+
// Metadata and shared Context to be available through the lifecycle of the page
115+
const context = {
116+
frontmatter: frontmatter,
117+
headings: headings,
118+
pathname: `/${pathname}`,
119+
readingTime: readingTime,
120+
filename: filename,
121+
};
122+
123+
return [content, context] as const;
124+
};
125+
126+
// This method is used for rendering the actual page
127+
export const renderPage: FC<DynamicPageRender> = props => {
128+
// Defines a shared Server Context for the Client-Side
129+
// That is shared for all pages under the dynamic router
130+
setClientContext(props.context);
131+
132+
// The Matter Provider allows Client-Side injection of the data
133+
// to a shared React Client Provider even though the page is rendered
134+
// within a server-side context
135+
return (
136+
<MatterProvider {...props.context}>
137+
<WithLayout layout={props.layout}>{props.content}</WithLayout>
138+
</MatterProvider>
139+
);
140+
};
141+
142+
// This method parses the current pathname and does any sort of modifications needed on the route
143+
// then it proceeds to retrieve the Markdown file and parse the MDX Content into a React Component
144+
// finally it returns (if the locale and route are valid) the React Component with the relevant context
145+
// and attached context providers for rendering the current page
146+
const getPage: FC<DynamicParams> = async props => {
147+
// Gets the current full pathname for a given path
148+
const [locale, pathname] = await getLocaleAndPath(props);
149+
150+
// Gets the Markdown content and context
151+
const [content, context] = await getMarkdownContext(locale, pathname);
152+
153+
// If we have a filename and layout then we have a page
154+
if (context.filename && context.frontmatter.layout) {
155+
return renderPage({
156+
content: content,
157+
layout: context.frontmatter.layout,
158+
context: context,
159+
});
156160
}
157161

158162
return notFound();

0 commit comments

Comments
 (0)