vercel / next.js

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

"getStaticProps is not defined" with mdx files #12053

Closed laurentsenta closed 1 year ago

laurentsenta commented 4 years ago

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

    "@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!

silvenon commented 4 years ago

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"]
}
kud commented 4 years ago

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)

silvenon commented 4 years ago

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.

kud commented 4 years ago

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!

aptfx commented 4 years ago

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?

silvenon commented 4 years ago

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. 😕

kud commented 4 years ago

Super interesting! Thank you very much @silvenon

leerob commented 4 years ago

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

kud commented 4 years ago

For the moment:

silvenon commented 4 years ago

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?

kud commented 4 years ago

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.

kud commented 4 years ago

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

silvenon commented 4 years ago

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. 🤷

kud commented 4 years ago

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

juice49 commented 4 years ago

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.

aralroca commented 3 years ago

I get a similar issue, but with async, after writing:

export async function getStaticProps(){
   return { props: { data: await myData() } }
}

image

Rewriting to:

export const getStaticProps = async () => {
   return { props: { data: await myData() } }
}

Just the same as you:

image

souporserious commented 3 years ago

Spent some time trying to figure this out and can confirm that MDX V2 does fix this issue. If you're ok being on the next version, you just need to install "@mdx-js/loader": "next" and set the loader up yourself.

drew-codes commented 3 years ago

@souporserious Thanks for this! I installed the next version and I'm able to now export getStaticProps from my mdx pages, making life much easier. I'm fine with working around any stability issues as this is for a small personal blog anyway.

souporserious commented 3 years ago

No problem, glad it could help! In case you need extra markdown features like rendering tables, I ran into this issue switching to latest. You can fix it by adding remark-gfm:

{ loader: '@mdx-js/loader', options: { remarkPlugins: [gfm] }}
ericclemmons commented 3 years ago

I've noticed that with @mdx-js/loader (latest or next) and xdm, using getStaticProps in .mdx files isn't equivalent to a .tsx file.

For example, suppose I do something like what @leerob does with next-mdx-remote, but in my .mdx file:

https://github.com/leerob/leerob.io/blob/9992324086e07c1dfb31e8b0629a034da1810a03/pages/blog/%5Bslug%5D.js#L41-L42

---
title: My Test Page
---

import { getFiles, getFileBySlug } from '@/lib/mdx';

...Some content goes here...

export async function getStaticProps({ params }) {
  const post = await getFileBySlug('blog', params.slug);
  const tweets = await getTweets(post.tweetIDs);

  return { props: { ...post, tweets } };
}

I'll get this error, presumably because getStaticProps and it's imports aren't being stripped from the client-side bundle:

../node_modules/fs.realpath/index.js:8:0 Module not found: Can't resolve 'fs' null

For comparison, a vanilla test.tsx page can use the utilities just fine and Next.js seems to strip getStaticProps and its dependencies:

import { getFiles, getFileBySlug } from '@/lib/mdx';

import { getContentPaths } from "@/utils/getContentPaths";

export default function Test() {
  return <h1>Test</h1>;
}

export async function getStaticProps({ params }) {
  const post = await getFileBySlug('blog', params.slug);
  const tweets = await getTweets(post.tweetIDs);

  return { props: { ...post, tweets } };
}
github-actions[bot] commented 1 year ago

Please verify that your issue can be recreated with next@canary.

Why was this issue marked with the please verify canary label?

We noticed the provided reproduction was using an older version of Next.js, instead of canary.

The canary version of Next.js ships daily and includes all features and fixes that have not been released to the stable version yet. You can think of canary as a public beta. Some issues may already be fixed in the canary version, so please verify that your issue reproduces by running npm install next@canary and test it in your project, using your reproduction steps.

If the issue does not reproduce with the canary version, then it has already been fixed and this issue can be closed.

How can I quickly verify if my issue has been fixed in canary?

The safest way is to install next@canary in your project and test it, but you can also search through closed Next.js issues for duplicates or check the Next.js releases.

My issue has been open for a long time, why do I need to verify canary now?

Next.js does not backport bug fixes to older versions of Next.js. Instead, we are trying to introduce only a minimal amount of breaking changes between major releases.

What happens if I don't verify against the canary version of Next.js?

An issue with the please verify canary that receives no meaningful activity (e.g. new comments that acknowledge verification against canary) will be automatically closed and locked after 30 days.

If your issue has not been resolved in that time and it has been closed/locked, please open a new issue, with the required reproduction, using next@canary.

I did not open this issue, but it is relevant to me, what can I do to help?

Anyone experiencing the same issue is welcome to provide a minimal reproduction following the above steps. Furthermore, you can upvote the issue using the :+1: reaction on the topmost comment (please do not comment "I have the same issue" without repro steps). Then, we can sort issues by votes to prioritize.

I think my reproduction is good enough, why aren't you looking into it quicker?

We look into every Next.js issue and constantly monitor open issues for new comments.

However, sometimes we might miss one or two due to the popularity/high traffic of the repository. We apologize, and kindly ask you to refrain from tagging core maintainers, as that will usually not result in increased priority.

Upvoting issues to show your interest will help us prioritize and address them as quickly as possible. That said, every issue is important to us, and if an issue gets closed by accident, we encourage you to open a new one linking to the old issue and we will look into it.

Useful Resources

balazsorban44 commented 1 year ago

This issue has been automatically closed because it wasn't verified against next@canary. If you think it was closed by accident, please leave a comment. If you are running into a similar issue, please open a new issue with a reproduction. Thank you.

github-actions[bot] commented 1 year ago

This closed issue has been automatically locked because it had no new activity for a month. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.