Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

examples: update with-mdx-remote example to utilize the App Router #74067

Merged
merged 3 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 3 additions & 2 deletions examples/with-mdx-remote/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*.local
# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel
Expand Down
61 changes: 20 additions & 41 deletions examples/with-mdx-remote/README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
# MDX Remote Example

This example shows how a simple blog might be built using the [next-mdx-remote](https://github.com/hashicorp/next-mdx-remote) library, which allows mdx content to be loaded via `getStaticProps` or `getServerSideProps`. The mdx content is loaded from a local folder, but it could be loaded from a database or anywhere else.

The example also showcases [next-remote-watch](https://github.com/hashicorp/next-remote-watch), a library that allows next.js to watch files outside the `pages` folder that are not explicitly imported, which enables the mdx content here to trigger a live reload on change.

Since `next-remote-watch` uses undocumented Next.js APIs, it doesn't replace the default `dev` script for this example. To use it, run `npm run dev:watch` or `yarn dev:watch`.
This example shows how a simple blog might be built using the [next-mdx-remote](https://github.com/hashicorp/next-mdx-remote) library, which allows mdx content to be loaded via [`generateStaticParams`](https://nextjs.org/docs/app/api-reference/functions/generate-static-params). The mdx content is loaded from a local folder, but it could be loaded from a database or anywhere else.

## Deploy your own

Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example):
Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) or preview live with [StackBlitz](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/with-mdx-remote)

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-mdx-remote&project-name=with-mdx-remote&repository-name=with-mdx-remote)

## How to use

Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), [pnpm](https://pnpm.io), or [Bun](https://bun.sh/docs/cli/bunx) to bootstrap the example:

```bash
npx create-next-app --example with-mdx-remote with-mdx-remote-app
Expand All @@ -28,56 +24,39 @@ yarn create next-app --example with-mdx-remote with-mdx-remote-app
pnpm create next-app --example with-mdx-remote with-mdx-remote-app
```

Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
```bash
bunx create-next-app --example with-mdx-remote with-mdx-remote-app
```

Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/app/building-your-application/deploying)).

## Notes

### Conditional custom components

When using `next-mdx-remote`, you can pass custom components to the MDX renderer. However, some pages/MDX files might use components that are used infrequently, or only on a single page. To avoid loading those components on every MDX page, you can use `next/dynamic` to conditionally load them.
When using `next-mdx-remote`, you can pass custom components to the MDX renderer. However, some pages/MDX files might use components that are used infrequently, or only on a single page. To avoid loading those components on every MDX page, you can use [`next/dynamic`](https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading#nextdynamic) to conditionally load them.

For example, here's how you can change `getStaticProps` to pass a list of component names, checking the names in the page render function to see which components need to be dynamically loaded.

```js
```typescript
import dynamic from "next/dynamic";
import Test from "../components/test";
import Test from "@/components/test";
import { MDXRemote, type MDXRemoteProps } from 'next-mdx-remote/rsc'

const SomeHeavyComponent = dynamic(() => import("SomeHeavyComponent"));
const SomeHeavyComponent = dynamic(() => import("../component/SomeHeavyComponent"));

const defaultComponents = { Test };

export function SomePage({ mdxSource, componentNames }) {
export function CustomMDX(props: MDXRemoteProps) {
const componentNames = [
/<SomeHeavyComponent/.test(props.source as string) ? "SomeHeavyComponent" : "",
].filter(Boolean);

const components = {
...defaultComponents,
SomeHeavyComponent: componentNames.includes("SomeHeavyComponent")
? SomeHeavyComponent
: null,
: () => null,
};

return <MDXRemote {...mdxSource} components={components} />;
}

export async function getStaticProps() {
const source = `---
title: Conditional custom components
---

Some **mdx** text, with a default component <Test name={title}/> and a Heavy component <SomeHeavyComponent />
`;

const { content, data } = matter(source);

const componentNames = [
/<SomeHeavyComponent/.test(content) ? "SomeHeavyComponent" : null,
].filter(Boolean);

const mdxSource = await serialize(content);

return {
props: {
mdxSource,
componentNames,
},
};
return <MDXRemote {...props} components={components} />;
}
```
3 changes: 3 additions & 0 deletions examples/with-mdx-remote/app/[slug]/page.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.description {
opacity: 0.6;
}
45 changes: 45 additions & 0 deletions examples/with-mdx-remote/app/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { notFound } from "next/navigation";
import { CustomMDX } from "@/components/mdx";
import { getPosts } from "@/lib/utils";
import styles from "./page.module.css";
import Link from "next/link";

export function generateStaticParams() {
const posts = getPosts();

return posts.map((post) => ({
slug: post.slug,
}));
}

export default async function Blog({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const slug = (await params).slug;
const post = getPosts().find((post) => post.slug === slug);

if (!post) {
notFound();
}

return (
<>
<header>
<nav>
<Link href="/">👈 Go back home</Link>
</nav>
</header>
<main>
<h1 className={styles.postHeader}>{post.metadata.title}</h1>
{post.metadata.description && (
<p className={styles.description}>{post.metadata.description}</p>
)}
<article>
<CustomMDX source={post.content} />
</article>
</main>
</>
);
}
59 changes: 59 additions & 0 deletions examples/with-mdx-remote/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
:root {
--background: #ffffff;
--foreground: #171717;
}

@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}

html,
body {
font: 100%/1.5 system-ui;
max-width: 100vw;
overflow-x: hidden;
}

body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
max-width: 36rem;
margin: 0 auto;
padding: 1.5rem;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

* {
box-sizing: border-box;
padding: 0;
margin: 0;
}

a {
color: inherit;
text-decoration-thickness: 2px;
}

a:hover {
color: royalblue;
text-decoration-color: currentcolor;
}

p {
margin-bottom: 1.5rem;
}

code {
font-family: Menlo;
}

@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
32 changes: 32 additions & 0 deletions examples/with-mdx-remote/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});

const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});

export const metadata: Metadata = {
title: "MDX Remote App",
description: "Generated by create next app",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable}`}>
{children}
</body>
</html>
);
}
23 changes: 23 additions & 0 deletions examples/with-mdx-remote/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Link from "next/link";
import { getPosts } from "@/lib/utils";

export default function Home() {
const posts = getPosts();

return (
<main>
<h1>Home Page</h1>
<p>
Click the link below to navigate to a page generated by{" "}
<code>next-mdx-remote</code>.
</p>
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link href={`/${post.slug}`}>{post.metadata.title}</Link>
</li>
))}
</ul>
</main>
);
}
12 changes: 12 additions & 0 deletions examples/with-mdx-remote/app/posts/example-post.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: Example Post
description: This frontmatter description will appear below the title
---

This is an example post, with with a [link](https://nextjs.org) and a React component:

<Greet name="next-mdx-remote" />

Links are rendered using a custom component passed to `next-mdx-remote`.

Go back [home](/).
5 changes: 5 additions & 0 deletions examples/with-mdx-remote/app/posts/hello-world.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Hello World
---

This is an example post. There's another one [here](/example-post).
16 changes: 0 additions & 16 deletions examples/with-mdx-remote/components/CustomLink.js

This file was deleted.

49 changes: 0 additions & 49 deletions examples/with-mdx-remote/components/Layout.js

This file was deleted.

16 changes: 0 additions & 16 deletions examples/with-mdx-remote/components/TestComponent.js

This file was deleted.

7 changes: 7 additions & 0 deletions examples/with-mdx-remote/components/greet.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.div {
background-color: #111;
border-radius: 0.5em;
color: #fff;
margin-bottom: 1.5em;
padding: 0.5em 0.75em;
}
5 changes: 5 additions & 0 deletions examples/with-mdx-remote/components/greet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import styles from "./greet.module.css";

export function Greet({ name = "world" }: { name: string }) {
return <div className={styles.div}>Hello, {name}!</div>;
}
4 changes: 4 additions & 0 deletions examples/with-mdx-remote/components/mdx.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.a {
color: tomato;
text-decoration-color: rgba(0, 0, 0, 0.4);
}
Loading
Loading