vercel / next.js

The React Framework
https://nextjs.org
MIT License
124.93k stars 26.68k forks source link

Feature Request: Building MDX Pages via `getStaticProps` #14718

Closed joe-bell closed 4 years ago

joe-bell commented 4 years ago

Feature request

Given that Next.js is investing efforts into static generation with the likes of getStaticProps and getStaticPaths, it would be great to leverage the full power of MDX without being tied to the Webpack loader's (@next/mdx) current constraints.

The current use of @next/mdx has its limitations:

next-mdx-enhanced does a great job of alleviating some of these concerns but still relies heavily on Webpack, rather than utilizing Next's static functions.

This PR suggests @next/mdx could be extended to offer users more flexibility (but hopefully still easy-to-follow) on how their pages are built. This suggestion aims to provide:

Describe the solution you'd like

Note: These examples would still rely on the consumer configuring their own MDXProvider (e.g. Theme-UI)

  1. An MDXRenderer that allows for full control of layout:

    (Importing an mdx file would return an object containing frontMatter and mdxContent)

    // e.g. src/pages/blog/[id].tsx
    import * as React from "react";
    import { GetStaticProps, GetStaticPaths, NextPage } from "next";
    import Error from "next/error";
    import LayoutPost from "../layouts/post";
    import { somePrivateFunctionToGlobMdxFileNames } from "./utils";
    // but maybe this could be a function added to `@next/mdx`
    
    // Proposed:
    import { MDXRenderer } from "@next/mdx";
    
    type BlogPostProps = { error?: boolean };
    
    export const getStaticPaths: GetStaticPaths = async () => ({
     paths: somePrivateFunctionToGlobMdxFileNames("../posts"),
    });
    
    export const getStaticProps: GetStaticProps<BlogPostProps> = async (
     context
    ) => {
     // this could also be a remote data source
     const res = await import(`../posts/${context.params.id}.mdx`);
     // const { frontMatter, mdxContent } = res
    
     return { props: res || { error: true } };
    };
    
    const BlogPost: React.FC<NextPage & BlogPostProps> = ({ error, ...props }) =>
     error ? (
       <Error statusCode={404} />
     ) : (
       <LayoutPost title={props.frontMatter.title}>
         <MDXRenderer {...props} />
       </LayoutPost>
     );
    
    export default BlogPostPage;
  2. An mdxToStaticMarkup function that allows the MDXRenderer to be converted to static HTML (for RSS feeds etc.).

    mdxToStaticMarkup(<MDXRenderer />);

    With this approach the consumer would have the freedom to transform content if necessary.

Describe alternatives you've considered

One approach I tried back in january was to use MDX's async API in the getStaticProps method directly, but I hit a snag with this issue: https://github.com/mdx-js/mdx/issues/382#issuecomment-523014913

More importantly though, given Next.js does such a great job of reducing set-up complexity, it would be great to have something that makes MDX more flexible but still easy to use.

Additional context

As of now my availability is open and I would be more than happy to collaborate with the Next.js core team into spiking this as an option (if desired).

timneutkens commented 4 years ago

Just keep in mind that what you're proposing would only allow for fully static html to be output, no client-side hydration would happen based on what you're proposing, meaning no dynamic components (no setState / useEffect etc)

joe-bell commented 4 years ago

Sorry @timneutkens I'm not sure I follow; do you mean both functions I've suggested or a specific one in particular? I'm suggesting providing both.

With the MDXRenderer I'm thinking an implementation of MDX's Async API

but the mdxToStaticMarkup is an optional method designed specifically for things like RSS feeds, API routes etc (where we of course wouldn't want client-side hydration)

timneutkens commented 4 years ago

With the MDXRenderer I'm thinking an implementation of MDX's Async API…

This ships a JS parser to the browser to make it work (increasing bundle sizes tremendously)

You'll likely want to use something like https://github.com/hashicorp/next-mdx-remote

joe-bell commented 4 years ago

Gotcha, thanks for the explanation

Do the Next.js core team plan on integrating something similar to next-mdx-remote in the future?

I do feel that something like this is more inline to what Next.js is achieving in other areas, it would be great to see this for MDX too

IanMitchell commented 4 years ago

Jumping in to say I think improvements similar to the ones proposed would be hugely welcome, and I'd be happy to help out! I've been working on redoing my personal website with MDX and Next.js, porting it from Gatsby. It would be cool to see MDX as a seamless option in Next.js SSG. There's been a couple of us working on these issues already too - I don't think it would take a lot of work to get some implemented and shipped.

I know previously it was decided frontmatter is outside the purview of Next.js but I think it helps reduce the barrier of entry significantly, especially for non-technical folks.

Hydrating MDX is a bit of a different beast (I'm running into the problems Tim mentioned), but would love to see if it's possible to create a system that makes it as seamless as it is in Gatsby.

daneden commented 4 years ago

@joe-bell I just added a with-mdx-remote example which might give you what you’re looking for: #16613

I use a more advanced configuration for my own site, which has nested directories and conditionally loaded components. This has been working well for me and feels more native-to-Next.js (versus adding boilerplate MDX-specific code!)

joe-bell commented 4 years ago

@daneden thanks for this, it's a huge help!

joe-bell commented 4 years ago

I'll go ahead and close this issue seeing as next-mdx-remote does a good enough job of solving this challenge

deadcoder0904 commented 3 years ago

Would love to see this implemented as well.

I tried using next-mdx-remote & next-mdx-enhanced but ran into its limitations (specifically having images in the same folder as the posts that it cannot solve)

I had to use hacks to solve MDX to HTML (for RSS) → https://github.com/vercel/next.js/discussions/17931

kripod commented 3 years ago

I came across this issue while trying to find a solution for hot reloading .md files without a webpack loader. Using a combination of getStaticPaths and getStaticProps, I'm not quite sure how Fast Refresh could work when a .md file changes.

Would preview mode be sufficient for this use-case? If so, how could it be set up to watch for file changes?

(I think this is relevant because it's a problem to be solved when using getStaticProps instead of a webpack plugin.)

lunelson commented 3 years ago

@kripod although this is a bit OT and this thread is old: the solution I've found for refreshing data from getStaticProps/getStaticPaths is to use router.replace(), which causes those functions to re-run. NOTE however, that it is not a "fast refresh" thing at all, because those functions have to go through their complete fetching and parsing

// at the top of the file
import { useRouter } from 'next/router';

// in your page component
const router = useRouter();

// somewhere in response to an update event - page's props should just update in place
router.replace(router.asPath, router.asPath, { shallow: false, scroll: false });

...the tricky part is how do you get that event: I've built a hook to get pushed updates from a remote CMS, but I'm not sure how it could be done for local files. It would be cool if Next provided a way to add watchers/listeners for this use-case, or maybe add on to the watcher(s) already run by webpack-dev-server

balazsorban44 commented 2 years ago

This issue has been automatically locked due to no recent activity. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.