From a2d83952c1c35fc83542bf0e5542b899aab0d6ef Mon Sep 17 00:00:00 2001 From: Daniel Eden Date: Sat, 29 Aug 2020 02:22:35 +0100 Subject: [PATCH] Add with-mdx-remote example (#16613) This change adds a new example, `with-mdx-remote`, which leverages [`next-mdx-remote`](https://github.com/hashicorp/next-mdx-remote) to use MDX files as content for a dynamic route. In addition to the basic functionality, the example adds a note about somewhat advanced usage that allows the use of conditionally-loaded custom MDX components. cc @jescalan --- examples/with-mdx-remote/.gitignore | 34 +++++++ examples/with-mdx-remote/README.md | 54 +++++++++++ .../with-mdx-remote/components/CustomLink.js | 16 ++++ examples/with-mdx-remote/components/Layout.js | 49 ++++++++++ .../components/TestComponent.js | 16 ++++ examples/with-mdx-remote/package.json | 19 ++++ examples/with-mdx-remote/pages/index.js | 45 +++++++++ .../with-mdx-remote/pages/posts/[slug].js | 96 +++++++++++++++++++ .../with-mdx-remote/posts/example-post.mdx | 12 +++ .../with-mdx-remote/posts/hello-world.mdx | 5 + examples/with-mdx-remote/utils/mdxUtils.js | 11 +++ 11 files changed, 357 insertions(+) create mode 100644 examples/with-mdx-remote/.gitignore create mode 100644 examples/with-mdx-remote/README.md create mode 100644 examples/with-mdx-remote/components/CustomLink.js create mode 100644 examples/with-mdx-remote/components/Layout.js create mode 100644 examples/with-mdx-remote/components/TestComponent.js create mode 100644 examples/with-mdx-remote/package.json create mode 100644 examples/with-mdx-remote/pages/index.js create mode 100644 examples/with-mdx-remote/pages/posts/[slug].js create mode 100644 examples/with-mdx-remote/posts/example-post.mdx create mode 100644 examples/with-mdx-remote/posts/hello-world.mdx create mode 100644 examples/with-mdx-remote/utils/mdxUtils.js diff --git a/examples/with-mdx-remote/.gitignore b/examples/with-mdx-remote/.gitignore new file mode 100644 index 0000000000..1437c53f70 --- /dev/null +++ b/examples/with-mdx-remote/.gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel diff --git a/examples/with-mdx-remote/README.md b/examples/with-mdx-remote/README.md new file mode 100644 index 0000000000..bf5e18ce1e --- /dev/null +++ b/examples/with-mdx-remote/README.md @@ -0,0 +1,54 @@ +# 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`. + +## Deploy your own + +Deploy the example using [Vercel](https://vercel.com): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/vercel/next.js/tree/canary/examples/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) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: + +```bash +npx create-next-app --example with-mdx-remote with-mdx-remote-app +# or +yarn create next-app --example with-mdx-remote with-mdx-remote-app +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). + +## 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. + +For example, here's how you can change `getStaticProps` to conditionally add certain components: + +```js +import dynamic from 'next/dynamic' + +// ... + +export async function getStaticProps() { + const { content, data } = matter(source) + + const components = { + ...defaultComponents, + SomeHeavyComponent: / import('SomeHeavyComponent')) + : null, + } + + const mdxSource = await renderToString(content, { components }) +} +``` + +If you do this, you'll also need to check in the page render function which components need to be dynamically loaded. You can pass a list of component names via `getStaticProps` to accomplish this. diff --git a/examples/with-mdx-remote/components/CustomLink.js b/examples/with-mdx-remote/components/CustomLink.js new file mode 100644 index 0000000000..f590230732 --- /dev/null +++ b/examples/with-mdx-remote/components/CustomLink.js @@ -0,0 +1,16 @@ +import Link from 'next/link' + +export default function CustomLink({ as, href, ...otherProps }) { + return ( + <> + + + + + + ) +} diff --git a/examples/with-mdx-remote/components/Layout.js b/examples/with-mdx-remote/components/Layout.js new file mode 100644 index 0000000000..26da6b3a1f --- /dev/null +++ b/examples/with-mdx-remote/components/Layout.js @@ -0,0 +1,49 @@ +export default function Layout({ children }) { + return ( + <> +
{children}
+ + + + ) +} diff --git a/examples/with-mdx-remote/components/TestComponent.js b/examples/with-mdx-remote/components/TestComponent.js new file mode 100644 index 0000000000..ca4dfc5dc2 --- /dev/null +++ b/examples/with-mdx-remote/components/TestComponent.js @@ -0,0 +1,16 @@ +export default function TestComponent({ name = 'world' }) { + return ( + <> +
Hello, {name}!
+ + + ) +} diff --git a/examples/with-mdx-remote/package.json b/examples/with-mdx-remote/package.json new file mode 100644 index 0000000000..5f2a4f1d60 --- /dev/null +++ b/examples/with-mdx-remote/package.json @@ -0,0 +1,19 @@ +{ + "name": "with-mdx-remote", + "version": "1.0.0", + "scripts": { + "dev": "next", + "dev:watch": "next-remote-watch ./posts", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "gray-matter": "^4.0.2", + "next": "latest", + "next-mdx-remote": "^1.0.0", + "next-remote-watch": "0.2.0", + "react": "^16.13.1", + "react-dom": "^16.13.1" + }, + "license": "MIT" +} diff --git a/examples/with-mdx-remote/pages/index.js b/examples/with-mdx-remote/pages/index.js new file mode 100644 index 0000000000..de6074a182 --- /dev/null +++ b/examples/with-mdx-remote/pages/index.js @@ -0,0 +1,45 @@ +import fs from 'fs' +import matter from 'gray-matter' +import Link from 'next/link' +import path from 'path' +import Layout from '../components/Layout' +import { postFilePaths, POSTS_PATH } from '../utils/mdxUtils' + +export default function Index({ posts }) { + return ( + +

Home Page

+

+ Click the link below to navigate to a page generated by{' '} + next-mdx-remote. +

+
+ + ) +} + +export function getStaticProps() { + const posts = postFilePaths.map((filePath) => { + const source = fs.readFileSync(path.join(POSTS_PATH, filePath)) + const { content, data } = matter(source) + + return { + content, + data, + filePath, + } + }) + + return { props: { posts } } +} diff --git a/examples/with-mdx-remote/pages/posts/[slug].js b/examples/with-mdx-remote/pages/posts/[slug].js new file mode 100644 index 0000000000..fc62ac6d51 --- /dev/null +++ b/examples/with-mdx-remote/pages/posts/[slug].js @@ -0,0 +1,96 @@ +import fs from 'fs' +import matter from 'gray-matter' +import hydrate from 'next-mdx-remote/hydrate' +import renderToString from 'next-mdx-remote/render-to-string' +import dynamic from 'next/dynamic' +import Head from 'next/head' +import Link from 'next/link' +import path from 'path' +import CustomLink from '../../components/CustomLink' +import Layout from '../../components/Layout' +import { postFilePaths, POSTS_PATH } from '../../utils/mdxUtils' + +// Custom components/renderers to pass to MDX. +// Since the MDX files aren't loaded by webpack, they have no knowledge of how +// to handle import statements. Instead, you must include components in scope +// here. +const components = { + a: CustomLink, + // It also works with dynamically-imported components, which is especially + // useful for conditionally loading components for certain routes. + // See the notes in README.md for more details. + TestComponent: dynamic(() => import('../../components/TestComponent')), + Head, +} + +export default function PostPage({ source, frontMatter }) { + const content = hydrate(source, { components }) + return ( + +
+ +
+
+

{frontMatter.title}

+ {frontMatter.description && ( +

{frontMatter.description}

+ )} +
+
{content}
+ + +
+ ) +} + +export const getStaticProps = async ({ params }) => { + const postFilePath = path.join(POSTS_PATH, `${params.slug}.mdx`) + const source = fs.readFileSync(postFilePath) + + const { content, data } = matter(source) + + const mdxSource = await renderToString(content, { + components, + // Optionally pass remark/rehype plugins + mdxOptions: { + remarkPlugins: [], + rehypePlugins: [], + }, + scope: data, + }) + + return { + props: { + source: mdxSource, + frontMatter: data, + }, + } +} + +export const getStaticPaths = async () => { + const paths = postFilePaths + // Remove file extensions for page paths + .map((path) => path.replace(/\.mdx?$/, '')) + // Map the path into the static paths object required by Next.js + .map((slug) => ({ params: { slug } })) + + return { + paths, + fallback: false, + } +} diff --git a/examples/with-mdx-remote/posts/example-post.mdx b/examples/with-mdx-remote/posts/example-post.mdx new file mode 100644 index 0000000000..e1b6bad048 --- /dev/null +++ b/examples/with-mdx-remote/posts/example-post.mdx @@ -0,0 +1,12 @@ +--- +title: Example Post +description: This frontmatter description will appear below the title +--- + +This is an example post, with a [link](https://nextjs.org) and a React component: + + + +The title and description are pulled from the MDX file and processed using `gray-matter`. Additionally, links are rendered using a custom component passed to `next-mdx-remote`. + +Go back [home](/). diff --git a/examples/with-mdx-remote/posts/hello-world.mdx b/examples/with-mdx-remote/posts/hello-world.mdx new file mode 100644 index 0000000000..c4c1e23a77 --- /dev/null +++ b/examples/with-mdx-remote/posts/hello-world.mdx @@ -0,0 +1,5 @@ +--- +title: Hello World +--- + +This is an example post. There's another one [here](/posts/example-post). diff --git a/examples/with-mdx-remote/utils/mdxUtils.js b/examples/with-mdx-remote/utils/mdxUtils.js new file mode 100644 index 0000000000..3b884f2bfc --- /dev/null +++ b/examples/with-mdx-remote/utils/mdxUtils.js @@ -0,0 +1,11 @@ +import fs from 'fs' +import path from 'path' + +// POSTS_PATH is useful when you want to get the path to a specific file +export const POSTS_PATH = path.join(process.cwd(), 'posts') + +// postFilePaths is the list of all mdx files inside the POSTS_PATH directory +export const postFilePaths = fs + .readdirSync(POSTS_PATH) + // Only include md(x) files + .filter((path) => /\.mdx?$/.test(path)) -- GitLab