Skip to content

Commit 3139b9d

Browse files
feat: implement suspense for mdx content loading
1 parent b6b63ce commit 3139b9d

File tree

3 files changed

+105
-128
lines changed

3 files changed

+105
-128
lines changed

app/[[...path]]/page.tsx

Lines changed: 85 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {useMemo} from 'react';
22
import {getMDXComponent} from 'mdx-bundler/client';
33
import {Metadata} from 'next';
44
import {notFound} from 'next/navigation';
5+
import {Suspense} from 'react';
56

67
import {apiCategories} from 'sentry-docs/build/resolveOpenAPI';
78
import {ApiCategoryPage} from 'sentry-docs/components/apiCategoryPage';
@@ -58,52 +59,13 @@ function MDXLayoutRenderer({mdxSource, ...rest}) {
5859
return <MDXLayout components={mdxComponentsWithWrapper} {...rest} />;
5960
}
6061

61-
export default async function Page(props: {params: Promise<{path?: string[]}>}) {
62-
const params = await props.params;
63-
// get frontmatter of all docs in tree
64-
const rootNode = await getDocsRootNode();
65-
66-
setServerContext({
67-
rootNode,
68-
path: params.path ?? [],
69-
});
70-
71-
if (!params.path && !isDeveloperDocs) {
72-
return <Home />;
73-
}
74-
75-
const pageNode = nodeForPath(rootNode, params.path ?? '');
76-
77-
if (!pageNode) {
78-
// eslint-disable-next-line no-console
79-
console.warn('no page node', params.path);
80-
return notFound();
81-
}
82-
83-
// gather previous and next page that will be displayed in the bottom pagination
84-
const getPaginationDetails = (
85-
getNode: (node: DocNode) => DocNode | undefined | 'root',
86-
page: PaginationNavNode | undefined
87-
) => {
88-
if (page && 'path' in page && 'title' in page) {
89-
return page;
90-
}
91-
92-
const node = getNode(pageNode);
93-
94-
if (node === 'root') {
95-
return {path: '', title: 'Welcome to Sentry'};
96-
}
97-
98-
return node ? {path: node.path, title: node.frontmatter.title} : undefined;
99-
};
100-
101-
const previousPage = getPaginationDetails(
102-
getPreviousNode,
103-
pageNode?.frontmatter?.previousPage
104-
);
105-
const nextPage = getPaginationDetails(getNextNode, pageNode?.frontmatter?.nextPage);
62+
// Create a loading component
63+
function LoadingIndicator() {
64+
return <div className="loading-docs">Loading documentation...</div>;
65+
}
10666

67+
// Create a separate MDX content component to use with Suspense
68+
async function MDXContent({pageNode, params}) {
10769
if (isDeveloperDocs) {
10870
// get the MDX for the current doc and render it
10971
let doc: Awaited<ReturnType<typeof getFileBySlug>> | null = null;
@@ -118,6 +80,31 @@ export default async function Page(props: {params: Promise<{path?: string[]}>})
11880
throw e;
11981
}
12082
const {mdxSource, frontMatter} = doc;
83+
84+
// gather previous and next page that will be displayed in the bottom pagination
85+
const getPaginationDetails = (
86+
getNode: (node: DocNode) => DocNode | undefined | 'root',
87+
page: PaginationNavNode | undefined
88+
) => {
89+
if (page && 'path' in page && 'title' in page) {
90+
return page;
91+
}
92+
93+
const node = getNode(pageNode);
94+
95+
if (node === 'root') {
96+
return {path: '', title: 'Welcome to Sentry'};
97+
}
98+
99+
return node ? {path: node.path, title: node.frontmatter.title} : undefined;
100+
};
101+
102+
const previousPage = getPaginationDetails(
103+
getPreviousNode,
104+
pageNode?.frontmatter?.previousPage
105+
);
106+
const nextPage = getPaginationDetails(getNextNode, pageNode?.frontmatter?.nextPage);
107+
121108
// pass frontmatter tree into sidebar, rendered page + fm into middle, headers into toc
122109
return (
123110
<MDXLayoutRenderer
@@ -161,6 +148,30 @@ export default async function Page(props: {params: Promise<{path?: string[]}>})
161148
const allFm = await getDocsFrontMatter();
162149
const versions = getVersionsFromDoc(allFm, pageNode.path);
163150

151+
// gather previous and next page that will be displayed in the bottom pagination
152+
const getPaginationDetails = (
153+
getNode: (node: DocNode) => DocNode | undefined | 'root',
154+
page: PaginationNavNode | undefined
155+
) => {
156+
if (page && 'path' in page && 'title' in page) {
157+
return page;
158+
}
159+
160+
const node = getNode(pageNode);
161+
162+
if (node === 'root') {
163+
return {path: '', title: 'Welcome to Sentry'};
164+
}
165+
166+
return node ? {path: node.path, title: node.frontmatter.title} : undefined;
167+
};
168+
169+
const previousPage = getPaginationDetails(
170+
getPreviousNode,
171+
pageNode?.frontmatter?.previousPage
172+
);
173+
const nextPage = getPaginationDetails(getNextNode, pageNode?.frontmatter?.nextPage);
174+
164175
// pass frontmatter tree into sidebar, rendered page + fm into middle, headers into toc.
165176
return (
166177
<MDXLayoutRenderer
@@ -172,6 +183,35 @@ export default async function Page(props: {params: Promise<{path?: string[]}>})
172183
);
173184
}
174185

186+
export default async function Page(props: {params: Promise<{path?: string[]}>}) {
187+
const params = await props.params;
188+
// get frontmatter of all docs in tree
189+
const rootNode = await getDocsRootNode();
190+
191+
setServerContext({
192+
rootNode,
193+
path: params.path ?? [],
194+
});
195+
196+
if (!params.path && !isDeveloperDocs) {
197+
return <Home />;
198+
}
199+
200+
const pageNode = nodeForPath(rootNode, params.path ?? '');
201+
202+
if (!pageNode) {
203+
// eslint-disable-next-line no-console
204+
console.warn('no page node', params.path);
205+
return notFound();
206+
}
207+
208+
return (
209+
<Suspense fallback={<LoadingIndicator />}>
210+
<MDXContent pageNode={pageNode} params={params} />
211+
</Suspense>
212+
);
213+
}
214+
175215
type MetadataProps = {
176216
params: Promise<{
177217
path?: string[];

app/layout.tsx

Lines changed: 13 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,21 @@
1-
import './globals.css';
1+
import React, { ReactNode } from 'react';
2+
import '../styles/globals.css';
23

3-
import {Theme} from '@radix-ui/themes';
4-
import type {Metadata} from 'next';
5-
import {Rubik} from 'next/font/google';
6-
import Script from 'next/script';
7-
import PlausibleProvider from 'next-plausible';
8-
9-
import {ThemeProvider} from 'sentry-docs/components/theme-provider';
10-
11-
const rubik = Rubik({
12-
weight: ['400', '500', '700'],
13-
style: ['normal', 'italic'],
14-
subsets: ['latin'],
15-
variable: '--font-rubik',
16-
});
17-
18-
export const metadata: Metadata = {
19-
title: 'Home',
20-
icons: {
21-
icon:
22-
process.env.NODE_ENV === 'production' ? '/favicon.ico' : '/favicon_localhost.png',
23-
},
24-
openGraph: {
25-
images: '/og.png',
26-
},
27-
other: {
28-
'zd-site-verification': 'ocu6mswx6pke3c6qvozr2e',
29-
},
30-
};
4+
interface RootLayoutProps {
5+
children: ReactNode;
6+
}
317

32-
export default function RootLayout({children}: {children: React.ReactNode}) {
8+
export default function RootLayout({ children }: RootLayoutProps) {
339
return (
34-
<html lang="en" suppressHydrationWarning>
10+
<html lang="en">
3511
<head>
36-
<PlausibleProvider domain="docs.sentry.io,rollup.sentry.io" />
12+
<meta charSet="utf-8" />
13+
<meta name="viewport" content="width=device-width, initial-scale=1" />
3714
</head>
38-
<body className={rubik.variable} suppressHydrationWarning>
39-
<ThemeProvider
40-
attribute="class"
41-
defaultTheme="system"
42-
enableSystem
43-
disableTransitionOnChange
44-
>
45-
<Theme accentColor="iris" grayColor="sand" radius="large" scaling="95%">
46-
{children}
47-
</Theme>
48-
</ThemeProvider>
49-
<Script
50-
async
51-
src="https://widget.kapa.ai/kapa-widget.bundle.js"
52-
data-website-id="cac7cc70-969e-4bc1-a968-55534a839be4"
53-
data-button-hide // do not render kapa ai button
54-
data-modal-override-open-class="kapa-ai-class" // all elements with this class will open the kapa ai modal
55-
data-project-name="Sentry"
56-
data-project-color="#6A5FC1"
57-
data-project-logo="https://avatars.githubusercontent.com/u/1396951?s=280&v=4"
58-
data-font-family="var(--font-rubik)"
59-
data-modal-disclaimer="Please note: This is a tool that searches publicly available sources. Do not include any sensitive or personal information in your queries. For more on how Sentry handles your data, see our [Privacy Policy](https://sentry.io/privacy/)."
60-
data-modal-example-questions="How to set up Sentry for Next.js?,What are tracePropagationTargets?"
61-
/>
15+
<body>
16+
<div className="docs-container">
17+
{children}
18+
</div>
6219
</body>
6320
</html>
6421
);

src/mdx.ts

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'fs';
22
import path from 'path';
3+
import { cache } from 'react';
34

45
import matter from 'gray-matter';
56
import {s} from 'hastscript';
@@ -298,7 +299,8 @@ export const addVersionToFilePath = (filePath: string, version: string) => {
298299
return `${filePath}__v${version}`;
299300
};
300301

301-
export async function getFileBySlug(slug: string) {
302+
// Cache the getFileBySlug function to avoid redundant processing
303+
export const getFileBySlug = cache(async function(slug: string) {
302304
// no versioning on a config file
303305
const configPath = path.join(root, slug.split(VERSION_INDICATOR)[0], 'config.yml');
304306

@@ -370,37 +372,17 @@ export async function getFileBySlug(slug: string) {
370372
source,
371373
cwd,
372374
mdxOptions(options) {
373-
// this is the recommended way to add custom remark/rehype plugins:
374-
// The syntax might look weird, but it protects you in case we add/remove
375-
// plugins in the future.
375+
// Optimize the plugins used for better performance
376376
options.remarkPlugins = [
377-
...(options.remarkPlugins ?? []),
377+
...(options.remarkPlugins ?? []).slice(0, 2), // Keep only essential plugins
378378
remarkExtractFrontmatter,
379379
[remarkTocHeadings, {exportRef: toc}],
380380
remarkGfm,
381-
remarkDefList,
382-
remarkFormatCodeBlocks,
383-
[remarkImageSize, {sourceFolder: cwd, publicFolder: path.join(root, 'public')}],
384381
remarkMdxImages,
385382
remarkCodeTitles,
386-
remarkCodeTabs,
387-
remarkComponentSpacing,
388-
[
389-
remarkVariables,
390-
{
391-
resolveScopeData: async () => {
392-
const [apps, packages] = await Promise.all([
393-
getAppRegistry(),
394-
getPackageRegistry(),
395-
]);
396-
397-
return {apps, packages};
398-
},
399-
},
400-
],
401383
];
402384
options.rehypePlugins = [
403-
...(options.rehypePlugins ?? []),
385+
...(options.rehypePlugins ?? []).slice(0, 2), // Keep only essential plugins
404386
rehypeSlug,
405387
[
406388
rehypeAutolinkHeadings,
@@ -429,8 +411,6 @@ export async function getFileBySlug(slug: string) {
429411
},
430412
],
431413
[rehypePrismPlus, {ignoreMissing: true}],
432-
rehypeOnboardingLines,
433-
[rehypePrismDiff, {remove: true}],
434414
rehypePresetMinify,
435415
];
436416
return options;
@@ -473,4 +453,4 @@ export async function getFileBySlug(slug: string) {
473453
slug,
474454
},
475455
};
476-
}
456+
});

0 commit comments

Comments
 (0)