diff --git a/examples/with-mdx-remote/.gitignore b/examples/with-mdx-remote/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..1437c53f70bc211ec65739ec4a8c2a4db5874c73 --- /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 0000000000000000000000000000000000000000..bf5e18ce1e43b254ab877980a26120c75c345fc7 --- /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 0000000000000000000000000000000000000000..f59023073206ba016459bb4da6299dfa9a8e5d67 --- /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 0000000000000000000000000000000000000000..26da6b3a1ffca1c84d088327f8caa8d54ff3caf4 --- /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 0000000000000000000000000000000000000000..ca4dfc5dc2b2266d8571837313f9b0ee8e8a51ec --- /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 0000000000000000000000000000000000000000..5f2a4f1d60b1bad032138c24757c19b6bb7bf213 --- /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 0000000000000000000000000000000000000000..de6074a182969e6031d36338de3d93b627c95589 --- /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 0000000000000000000000000000000000000000..fc62ac6d51db33221307fc165deba52a1ab963aa --- /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 0000000000000000000000000000000000000000..e1b6bad048b0b1aadc5fa0710f08223f75bc7c9c --- /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 0000000000000000000000000000000000000000..c4c1e23a774142eb6b6db5499df8c7007913cf87 --- /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 0000000000000000000000000000000000000000..3b884f2bfc60c75d5e0d6de9e64e5b80beed2090 --- /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))