Next.js: "getStaticProps is not defined" with mdx files

Created on 20 Apr 2020  路  15Comments  路  Source: vercel/next.js

Bug report

I'm trying to use nextjs and mdx to build a simple site. When I export getStaticProps, I get an "undefined" error. It looks like an issue that happens client side.

To Reproduce

I followed the "with-mdx" example to add mdx pages to my application.
https://github.com/zeit/next.js/tree/canary/examples/with-mdx

I try to generate static props from the mdx using exports (https://mdxjs.com/getting-started#exports)

// src/pages/index.mdx

# Helloworld

Content is here!

export function getStaticProps() {
    return {
        props: {hello: 'world'}
    }
}
// src/_app.tsx
...

export default function App(app: AppProps) {
  const { Component, pageProps } = app;

  return (<MDXProvider components={components}>
      <pre>{JSON.stringify(pageProps)}</pre>
      <Component {...pageProps} />
    </MDXProvider>
  );
}

I get an undefined error:

ReferenceError: getStaticProps is not defined
Module../src/pages/index.mdx
./src/pages/index.mdx:25
  22 | const layoutProps = {
  23 |   layout,
  24 | hello,
> 25 | getStaticProps
  26 | };
  27 | const MDXLayout = "wrapper"
  28 | export default function MDXContent({

The <pre>{hello: "world"}</pre> appears on my webpage. It looks like this error is client side only, and the code behave as expect on the server.

Screenshot of the full error below.

Expected behavior

I expect to see the props and the content.

Screenshots

Screenshot 2020-04-20 at 18 35 00

System information

  • OS: macOS
  • Version of Next.js: 9.3.4
  • Version of Node.js: 12.14.0
    "@mdx-js/loader": "^1.5.8",
    "@next/mdx": "^9.3.5",
    "cors": "^2.8.5",
    "dotenv": "^8.2.0",
    "isomorphic-unfetch": "^3.0.0",
    "next": "^9.3.4",
    "pg": "^8.0.2",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "swr": "^0.2.0",
    "unfetch": "^4.1.0"

Thanks for supporting this project!

bug needs investigation

Most helpful comment

You have to export getStaticProps from the MDX page itself, not layout.

There is one syntax that seems to circumvent this incompatibility between MDX and Next.js:

export { getStaticProps } from 'path/to/module'

# Title

Your MDX content...

But I'd say that it's somewhat of a bug in MDX that this works because all exports are meant to be passed as props to layout, e.g. export const meta = { ... } is a common pattern, and I'd expect export { meta } from './other-data to behave the same, but it doesn't.

All 15 comments

The problem is that MDX adds getStaticProps to layoutProps, but then next/babel removes the export, so now the reference is missing. I think you can patch the issue by creating this Babel plugin, which removes all data fetching functions from layoutProps:

/**
 * Currently it's not possible to export data fetching functions from MDX pages
 * because MDX includes them in `layoutProps`, and Next.js removes them at some
 * point, causing a `ReferenceError`.
 *
 * https://github.com/mdx-js/mdx/issues/742#issuecomment-612652071
 *
 * This plugin can be removed once MDX removes `layoutProps`, at least that
 * seems to be the current plan.
 */

// https://nextjs.org/docs/basic-features/data-fetching
const DATA_FETCH_FNS = ['getStaticPaths', 'getStaticProps', 'getServerProps']

module.exports = () => {
  return {
    visitor: {
      ObjectProperty(path) {
        if (
          DATA_FETCH_FNS.includes(path.node.value.name) &&
          path.findParent(
            (path) =>
              path.isVariableDeclarator() &&
              path.node.id.name === 'layoutProps',
          )
        ) {
          path.remove()
        }
      },
    },
  }
}

Then add it to your Babel configuration, let's say we call it babel-plugin-nextjs-mdx-patch.js:

{
  "presets": ["next/babel"],
  "plugins": ["./babel-plugin-nextjs-mdx-patch"]
}

Hey!

I think I'm trying to do something like that.

I defined a layout for my mdx files like this:

import PostLayout from "~/components/Layout/Post"

const App = ({ Component, pageProps }) => (
  <MDXProvider components={{ wrapper: PostLayout }}>
    <Component {...pageProps} />
  </MDXProvider>
)

however I need to do something first via getStaticProps before rendering the mdx via my layout.

I tried to add getStaticProps into my layout, but it's never called.

So I try a kind of hack to put my layout into a page as apparently only pages can have getStaticProps but it didn't change anything.

My question is: by using mdx, how I can intercept it before it's rendered by my component (which is PostLayout)

example:

export async function getStaticProps() {
  console.log("hello")

  return {
    props: {
      test: test,
    },
  }
}

const PostLayout = ({ children, meta: { title, cover }, test }) => ()

(nothing happened)

You have to export getStaticProps from the MDX page itself, not layout.

There is one syntax that seems to circumvent this incompatibility between MDX and Next.js:

export { getStaticProps } from 'path/to/module'

# Title

Your MDX content...

But I'd say that it's somewhat of a bug in MDX that this works because all exports are meant to be passed as props to layout, e.g. export const meta = { ... } is a common pattern, and I'd expect export { meta } from './other-data to behave the same, but it doesn't.

Oh I see. Thank you for your answer. The thing is I would like to do "lots of stuff" so it could be a bit annoying to put all this logic inside each mdx as my mdx files are especially posts for my blog.

Indeed, I only use export for metadata instead of using frontmatter. But for instance, knowing the path where I am, I don't want to put this logic inside this.

I found a trick by using the router in the layout to get this instead:

const PostLayout = ({ children, meta: { title, cover } }) => {
  const { pathname } = useRouter()

  const splitUri = pathname.split("/")
  const homeUrl = `/${splitUri[1]}/${splitUri[2]}`
  const date = dayjs(getDateFromPath(pathname)).format("DD MMMM YYYY")

Maybe I could do a kind of wrapper for metadata with getStaticProps as you said yes, thanks for this idea!

I wondered if this kind of global state stuff could be done on getInitialProps in a Custom App which puts the data an own Context Provider. Would that work with fully static sites?

The thing is I would like to do "lots of stuff" so it could be a bit annoying to put all this logic inside each mdx as my mdx files are especially posts for my blog.

Definitely annoying! This is why I went a little nuts in my configuration by building custom unified plugins for MDX, for example:

Depending on what you want you might find them useful, they are written to be reusable and the tests should help you figure out how they work. I was planning to publish them, but it seems like MDX v2 is moving towards this direction anyway.

I wondered if this kind of global state stuff could be done on getInitialProps in a Custom App which puts the data an own Context Provider. Would that work with fully static sites?

No, getInitialProps is run by the server. 馃槙

Super interesting! Thank you very much @silvenon

Could you expand on what you're trying to accomplish exactly with getStaticProps? That will help us understand what MDX solutions will work.

For the moment:

I would like to know the path of the current MDX parsed

For what? Unified plugins know the file path, but it depends on what you want to do with this path.

would also like to require the images I put in the MDX cf

I don't think that MDX supports interpolation in Markdown yet, but importing the image should get the src, then add it to an <img /> element:

import testGif from './test.gif`

<img src={testGif} />

I haven't tried it, but is there a downside to this approach?

About the current MDX, for the moment in my layout I do something like:

const PostLayout = ({ children, meta: { title, description, cover } }) => {
  const { pathname } = useRouter()

  const LANG = getLangFromPathname(pathname)

  const splitUri = pathname.split("/")
  const homeUrl = `/${splitUri[1]}/${splitUri[2]}`
  const date = dayjs(getDateFromPath(pathname))
    .locale(LANG)
    .format("DD MMMM YYYY")
  const fromNow = dayjs(getDateFromPath(pathname)).locale(LANG).fromNow()

and I'm not sure it's the best way, to use the router for this instead of the file path via the compiler/provider.


About the image, it means I can't use remark-images for instance. It also breaks some editors like markdown preview on VS Code 馃槵 (which can be fixed for sure).

And I don't like using two lines for one image.

My solution for the moment is <img src={require("./image.png")} />

I tried something like making a component called <Img /> where I require the image there instead in the MDX file, but I don't know the full path there so <Img src="./image.png" /> didn't work.

Maybe you're right, I should create my own remark plugin for that in fact. It's what I already thought about it. :)

This remark plugin could look something like this (adjust the logic accordingly because file.path is a full path):

const remarkMdxFromNow = () => (tree, file) => {
  const LANG = getLangFromPathname(file.path)

  const splitUri = file.path.split("/")
  const homeUrl = `/${splitUri[1]}/${splitUri[2]}`
  const date = dayjs(getDateFromPath(file.path))
    .locale(LANG)
    .format("DD MMMM YYYY")
  const fromNow = dayjs(getDateFromPath(file.path)).locale(LANG).fromNow()

  file.data.fromNow = fromNow
}

module.exports = remarkMdxFromNow

Now you saved it to file.data, and you could build a separate plugin that inserts an export statement into your MDX file, you can copy my remark-mdx-export-file-data.

Then you would apply these two plugins like this:

const fromNow = require('./etc/remark-mdx-from-now')
const exportFileData = require('./etc/remark-mdx-export-file-data')

// ...

{
  remarkPlugins: [
    fromNow,
    [exportFileData, ['fromNow']],
  ]
}

Now your MDX file will both export fromNow and provide it to the layout, if one is provided. Notice that you can use remark-mdx-export-file-data for exporting anything attached to file.data.

But this is obviously a temporary solution, MDX should provide a much better interface to do stuff like this. I haven't been following closely, but I think that's what MDX v2 will do.

About the image, it means I can't use remark-images for instance. It also breaks some editors like markdown preview on VS Code 馃槵 (which can be fixed for sure).

I didn't try to solve this problem before, so I don't know if there's a way. 馃し

Thank you so much for this explanation, and for your time. I'll give a try of this!

I have some work I want to do in getStaticProps for all of my mdx pages (much like @kud, I think).

Providing a custom renderer for @mdx-js/loader seems like a promising approach.

next.config.js

const mdxRenderer = `
  import React from 'react'
  import { mdx } from '@mdx-js/react'

  export async function getStaticProps () {
    return {
      props: {
        foo: 'bar'
      }
    }
  }
`

const withMdx = require('@next/mdx')({
  options: {
    renderer: mdxRenderer
  }
})

module.exports = withMdx({
  pageExtensions: ['tsx', 'mdx'],
})

layout.tsx

import React from 'react'

interface Props {
  foo: string
}

const Layout: React.FC<Props> = ({ children, foo }) => (
  <div>
    <p>The static prop value is: {foo}</p>
    {children}
  </div>
)

export default Layout

The static props seem to get passed into my layout just like I wanted, but I haven't thoroughly tested yet.

My next thought is whether I can export additional static prop getter functions from mdx pages to merge into the getStaticProps function defined in the mdx renderer.

Was this page helpful?
1 / 5 - 1 ratings