From fa05082ff62e0398c3d24579d922067fc8bec9e2 Mon Sep 17 00:00:00 2001 From: Daniel Eden Date: Wed, 26 Aug 2020 20:51:03 +0100 Subject: [PATCH 01/15] Add with-mdx-remote example --- examples/with-mdx-remote/.gitignore | 34 +++++++++ examples/with-mdx-remote/README.md | 42 +++++++++++ .../with-mdx-remote/components/CustomLink.js | 16 +++++ examples/with-mdx-remote/next.config.js | 5 ++ examples/with-mdx-remote/package.json | 18 +++++ examples/with-mdx-remote/pages/index.jsx | 41 +++++++++++ .../with-mdx-remote/pages/posts/[slug].jsx | 69 +++++++++++++++++++ .../with-mdx-remote/posts/example-post.mdx | 7 ++ .../with-mdx-remote/posts/hello-world.mdx | 5 ++ examples/with-mdx-remote/utils/mdxUtils.js | 11 +++ 10 files changed, 248 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/next.config.js create mode 100644 examples/with-mdx-remote/package.json create mode 100644 examples/with-mdx-remote/pages/index.jsx create mode 100644 examples/with-mdx-remote/pages/posts/[slug].jsx 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 0000000000000..1437c53f70bc2 --- /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 0000000000000..63909ebd16828 --- /dev/null +++ b/examples/with-mdx-remote/README.md @@ -0,0 +1,42 @@ +# Example app with MDX + +This example shows using [next-mdx-remote](https://github.com/hashicorp/next-mdx-remote) to import MDX file content to statically-generated pages. + +## 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 + +### Using `create-next-app` + +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 with-mdx-remote-app +# or +yarn create next-app --example with-mdx with-mdx-remote-app +``` + +### Download manually + +Download the example: + +```bash +curl https://codeload.github.com/vercel/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-mdx-remote +cd with-mdx-remote +``` + +Install it and run: + +```bash +npm install +npm run dev +# or +yarn +yarn dev +``` + +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)). diff --git a/examples/with-mdx-remote/components/CustomLink.js b/examples/with-mdx-remote/components/CustomLink.js new file mode 100644 index 0000000000000..7419d6e54c7bc --- /dev/null +++ b/examples/with-mdx-remote/components/CustomLink.js @@ -0,0 +1,16 @@ +import Link from 'next/link' + +export default function CustomLink({ href, ...otherProps }) { + return ( + <> + + + + + + ) +} diff --git a/examples/with-mdx-remote/next.config.js b/examples/with-mdx-remote/next.config.js new file mode 100644 index 0000000000000..bf102761462ba --- /dev/null +++ b/examples/with-mdx-remote/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + env: { + mdxPath: 'posts', + }, +} diff --git a/examples/with-mdx-remote/package.json b/examples/with-mdx-remote/package.json new file mode 100644 index 0000000000000..00f04ce31a411 --- /dev/null +++ b/examples/with-mdx-remote/package.json @@ -0,0 +1,18 @@ +{ + "name": "with-mdx-remote", + "version": "1.0.0", + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "gray-matter": "^4.0.2", + "next": "latest", + "next-mdx-remote": "^1.0.0", + "react": "^16.8.6", + "react-dom": "^16.8.6" + }, + "devDependencies": {}, + "license": "ISC" +} diff --git a/examples/with-mdx-remote/pages/index.jsx b/examples/with-mdx-remote/pages/index.jsx new file mode 100644 index 0000000000000..0be69430f44ac --- /dev/null +++ b/examples/with-mdx-remote/pages/index.jsx @@ -0,0 +1,41 @@ +import fs from 'fs' +import matter from 'gray-matter' +import Link from 'next/link' +import path from 'path' +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].jsx b/examples/with-mdx-remote/pages/posts/[slug].jsx new file mode 100644 index 0000000000000..cc3d7d16dc548 --- /dev/null +++ b/examples/with-mdx-remote/pages/posts/[slug].jsx @@ -0,0 +1,69 @@ +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 Link from 'next/link' +import path from 'path' +import CustomLink from '../../components/CustomLink' +import { postFilePaths, POSTS_PATH } from '../../utils/mdxUtils' + +// Custom components/renderers to pass to MDX +const components = { + a: CustomLink, +} + +export default function PostPage({ source, frontMatter }) { + const content = hydrate(source, { components }) + return ( + <> +
+ +
+
+

{frontMatter.title}

+ {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 0000000000000..b545db4708ad5 --- /dev/null +++ b/examples/with-mdx-remote/posts/example-post.mdx @@ -0,0 +1,7 @@ +--- +title: Example Post +--- + +This is an example post, with a [link](https://nextjs.org). + +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 0000000000000..c4c1e23a77414 --- /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 0000000000000..f232f3c85fd91 --- /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(), process.env.mdxPath) + +// 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)) From ed2892abaee6c976bd023a07c3b3df73811853e0 Mon Sep 17 00:00:00 2001 From: Daniel Eden Date: Wed, 26 Aug 2020 20:58:57 +0100 Subject: [PATCH 02/15] Add note about conditional custom components --- examples/with-mdx-remote/README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/examples/with-mdx-remote/README.md b/examples/with-mdx-remote/README.md index 63909ebd16828..caefb191c4fe0 100644 --- a/examples/with-mdx-remote/README.md +++ b/examples/with-mdx-remote/README.md @@ -40,3 +40,32 @@ yarn dev ``` 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 `getInitialProps` to conditionally add certain components: + +```js +import dynamic from "next/dynamic" +... + +async function getInitialProps() { + const { content, data } = matter(source) + + const components = { + ...defaultComponents, + SomeHeavyComponent: / import("SomeHeavyComponent")) : null, + } + + const mdxSource = await renderToString(content, { + components, + ...otherOptions, + }) +} +``` + +If you do this, you'll need to also check in the page render function which components need to be dynamically loaded. You can pass a list of components names via `getInitialProps` to accomplish this. From 4679aa0c8d8b47061643b0ddde8771d61bd027f8 Mon Sep 17 00:00:00 2001 From: Daniel Eden Date: Wed, 26 Aug 2020 21:06:44 +0100 Subject: [PATCH 03/15] Add clarifying comment to components object --- examples/with-mdx-remote/pages/posts/[slug].jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/with-mdx-remote/pages/posts/[slug].jsx b/examples/with-mdx-remote/pages/posts/[slug].jsx index cc3d7d16dc548..06da0d2e5de95 100644 --- a/examples/with-mdx-remote/pages/posts/[slug].jsx +++ b/examples/with-mdx-remote/pages/posts/[slug].jsx @@ -7,7 +7,10 @@ import path from 'path' import CustomLink from '../../components/CustomLink' import { postFilePaths, POSTS_PATH } from '../../utils/mdxUtils' -// Custom components/renderers to pass to MDX +// 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, } From 466f862fb738f4f48b1ddbc5e25a3795c0ad8207 Mon Sep 17 00:00:00 2001 From: Daniel Eden Date: Thu, 27 Aug 2020 16:47:37 +0100 Subject: [PATCH 04/15] Add example React component --- examples/with-mdx-remote/components/TestComponent.jsx | 3 +++ examples/with-mdx-remote/pages/posts/[slug].jsx | 5 +++++ examples/with-mdx-remote/posts/example-post.mdx | 4 +++- 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 examples/with-mdx-remote/components/TestComponent.jsx diff --git a/examples/with-mdx-remote/components/TestComponent.jsx b/examples/with-mdx-remote/components/TestComponent.jsx new file mode 100644 index 0000000000000..7e56f5d99205b --- /dev/null +++ b/examples/with-mdx-remote/components/TestComponent.jsx @@ -0,0 +1,3 @@ +export default function TestComponent({ name = 'world' }) { + return
Hello, {name}!
+} diff --git a/examples/with-mdx-remote/pages/posts/[slug].jsx b/examples/with-mdx-remote/pages/posts/[slug].jsx index 06da0d2e5de95..cc0784d322d1a 100644 --- a/examples/with-mdx-remote/pages/posts/[slug].jsx +++ b/examples/with-mdx-remote/pages/posts/[slug].jsx @@ -2,6 +2,7 @@ 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 Link from 'next/link' import path from 'path' import CustomLink from '../../components/CustomLink' @@ -13,6 +14,10 @@ import { postFilePaths, POSTS_PATH } from '../../utils/mdxUtils' // 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')), } export default function PostPage({ source, frontMatter }) { diff --git a/examples/with-mdx-remote/posts/example-post.mdx b/examples/with-mdx-remote/posts/example-post.mdx index b545db4708ad5..8243eabae0228 100644 --- a/examples/with-mdx-remote/posts/example-post.mdx +++ b/examples/with-mdx-remote/posts/example-post.mdx @@ -2,6 +2,8 @@ title: Example Post --- -This is an example post, with a [link](https://nextjs.org). +This is an example post, with a [link](https://nextjs.org) and a React component: + + Go back [home](/). From e44d1d2017e225d13173cd4c9f70e5ee7ad347b0 Mon Sep 17 00:00:00 2001 From: Daniel Eden Date: Thu, 27 Aug 2020 16:47:57 +0100 Subject: [PATCH 05/15] Consistently use .jsx extension --- .../components/{CustomLink.js => CustomLink.jsx} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename examples/with-mdx-remote/components/{CustomLink.js => CustomLink.jsx} (65%) diff --git a/examples/with-mdx-remote/components/CustomLink.js b/examples/with-mdx-remote/components/CustomLink.jsx similarity index 65% rename from examples/with-mdx-remote/components/CustomLink.js rename to examples/with-mdx-remote/components/CustomLink.jsx index 7419d6e54c7bc..f59023073206b 100644 --- a/examples/with-mdx-remote/components/CustomLink.js +++ b/examples/with-mdx-remote/components/CustomLink.jsx @@ -1,9 +1,9 @@ import Link from 'next/link' -export default function CustomLink({ href, ...otherProps }) { +export default function CustomLink({ as, href, ...otherProps }) { return ( <> - + + + + ) +} diff --git a/examples/with-mdx-remote/pages/index.jsx b/examples/with-mdx-remote/pages/index.jsx index 001c3aefc0cba..de6074a182969 100644 --- a/examples/with-mdx-remote/pages/index.jsx +++ b/examples/with-mdx-remote/pages/index.jsx @@ -2,11 +2,12 @@ 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{' '} @@ -24,7 +25,7 @@ export default function Index({ posts }) { ))} - + ) } diff --git a/examples/with-mdx-remote/pages/posts/[slug].jsx b/examples/with-mdx-remote/pages/posts/[slug].jsx index cc0784d322d1a..d0596304f04aa 100644 --- a/examples/with-mdx-remote/pages/posts/[slug].jsx +++ b/examples/with-mdx-remote/pages/posts/[slug].jsx @@ -6,6 +6,7 @@ import dynamic from 'next/dynamic' 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. @@ -23,7 +24,7 @@ const components = { export default function PostPage({ source, frontMatter }) { const content = hydrate(source, { components }) return ( - <> +

-
+

{frontMatter.title}

- {content} + {frontMatter.description && ( +

{frontMatter.description}

+ )}
+
{content}
+ + ) } diff --git a/examples/with-mdx-remote/posts/example-post.mdx b/examples/with-mdx-remote/posts/example-post.mdx index 8243eabae0228..e1b6bad048b0b 100644 --- a/examples/with-mdx-remote/posts/example-post.mdx +++ b/examples/with-mdx-remote/posts/example-post.mdx @@ -1,9 +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](/). From 5e03d7f2fbc9c4c790d644930c7feb8b6a7f5e30 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Fri, 28 Aug 2020 18:25:41 -0500 Subject: [PATCH 14/15] Updated readme --- examples/with-mdx-remote/README.md | 47 +++++++++--------------------- 1 file changed, 14 insertions(+), 33 deletions(-) diff --git a/examples/with-mdx-remote/README.md b/examples/with-mdx-remote/README.md index d9e9b540ba0f6..bf5e18ce1e43b 100644 --- a/examples/with-mdx-remote/README.md +++ b/examples/with-mdx-remote/README.md @@ -1,8 +1,10 @@ # 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`. In this example, the mdx content is loaded from a local folder, but it could be loaded from a database or anywhere else. The demo 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. +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. -Since `next-remote-watch` uses undocumented Next.js APIs, it doesn't replace the default `dev` script for this example. To use it, run `yarn dev:watch` or `npm run dev:watch`. +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 @@ -12,33 +14,12 @@ Deploy the example using [Vercel](https://vercel.com): ## How to use -### Using `create-next-app` - 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 with-mdx-remote-app +npx create-next-app --example with-mdx-remote with-mdx-remote-app # or -yarn create next-app --example with-mdx with-mdx-remote-app -``` - -### Download manually - -Download the example: - -```bash -curl https://codeload.github.com/vercel/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-mdx-remote -cd with-mdx-remote -``` - -Install it and run: - -```bash -npm install -npm run dev -# or -yarn -yarn dev +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)). @@ -52,22 +33,22 @@ When using `next-mdx-remote`, you can pass custom components to the MDX renderer For example, here's how you can change `getStaticProps` to conditionally add certain components: ```js -import dynamic from "next/dynamic" -... +import dynamic from 'next/dynamic' + +// ... export async function getStaticProps() { const { content, data } = matter(source) const components = { ...defaultComponents, - SomeHeavyComponent: / import("SomeHeavyComponent")) : null, + SomeHeavyComponent: / import('SomeHeavyComponent')) + : null, } - const mdxSource = await renderToString(content, { - components, - ...otherOptions, - }) + const mdxSource = await renderToString(content, { components }) } ``` -If you do this, you'll need to also check in the page render function which components need to be dynamically loaded. You can pass a list of components names via `getStaticProps` to accomplish this. +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. From 7e80a845a68cb120f4458a19d8c2cb9194d30f40 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Fri, 28 Aug 2020 19:00:43 -0500 Subject: [PATCH 15/15] Updated package.json --- examples/with-mdx-remote/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/with-mdx-remote/package.json b/examples/with-mdx-remote/package.json index 4306b5391f2f9..5f2a4f1d60b1b 100644 --- a/examples/with-mdx-remote/package.json +++ b/examples/with-mdx-remote/package.json @@ -12,8 +12,8 @@ "next": "latest", "next-mdx-remote": "^1.0.0", "next-remote-watch": "0.2.0", - "react": "^16.8.6", - "react-dom": "^16.8.6" + "react": "^16.13.1", + "react-dom": "^16.13.1" }, - "license": "ISC" + "license": "MIT" }