3

I am making a blog with MDX & Next.js but I am unable to render Markdown content. The blog post just shows markdown content as string.

Here's my complete source code → https://github.com/deadcoder0904/blog-mdx-next/

I have the following folder structure:

.
|-- README.md
|-- components
|   `-- Image.js
|-- next.config.js
|-- package-lock.json
|-- package.json
|-- pages
|   |-- _app.js
|   |-- blog
|   |   `-- [slug].js
|   |-- dark.css
|   |-- index.js
|   `-- new.css
|-- posts
|   |-- blog
|   |   |-- hello-world
|   |   |   |-- Rustin_Cohle.jpg
|   |   |   `-- index.mdx
|   |   `-- shit-world
|   |       `-- index.mdx
|   `-- tutorials
|       `-- console-log-in-javascript
|           `-- index.mdx
|-- prettier.config.js
`-- utils
    `-- mdxUtils.js

I have all my content in posts/ folder.

I have 2 folders in it: blog/ & tutorials/

Each post is in their own folder inside of blog/ or tutorials/. It looks like:

blog archives

I want to render my posts/blog/hello-world/index.mdx post at blog/hello-world location so I made blog/[slug].js file.

The contents of it are as follows:

pages/blog/[slug].js

import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import { MDXProvider } from '@mdx-js/react'

import { BLOG_PATH, blogFilePaths } from '../../utils/mdxUtils'
import { Image } from '../../components/Image'

const MDXComponents = { Image }

const Blog = ({ source, frontMatter }) => {
    return (
        <div>
            <h1>{frontMatter.title}</h1>
            <MDXProvider components={MDXComponents}>{source}</MDXProvider>
        </div>
    )
}

export async function getStaticPaths() {
    const paths = blogFilePaths.map((path) => {
        const split = path.split('/')
        const slug = split[split.length - 2]
        return {
            params: {
                slug,
            },
        }
    })

    return {
        paths,
        fallback: false,
    }
}

export const getStaticProps = async ({ params }) => {
    const { slug } = params
    const blogFilePath = path.join(BLOG_PATH, `/blog/${slug}/index.mdx`)

    const source = fs.readFileSync(blogFilePath)
    const { content: mdx, data } = matter(source)

    if (!blogFilePath) {
        console.warn('No MDX file found for slug')
    }

    return {
        props: {
            source: mdx,
            frontMatter: data,
        },
    }
}

export default Blog

The important part is:

<MDXProvider components={MDXComponents}>{source}</MDXProvider>

I thought this would render markdown content but it only displays markdown content as a string.

You can check the output at https://codesandbox.io/s/github/deadcoder0904/blog-mdx-next?file=/pages/blog/%5Bslug%5D.js by clicking any blog post.

It displays the following when I click on Hello World post:

hello world

How do I actually render the content?

I looked at other Github repos like Tailwind CSS Blog & it works fine for them but I am not sure how it works sadly :(

I do know that I have to convert the source prop in Blog or mdx in getStaticProps but I don't see Tailwind doing it either.

deadcoder0904
  • 7,232
  • 12
  • 66
  • 163

1 Answers1

3

I think this is what you're looking for https://github.com/vercel/next.js/discussions/13901

You'll need to install this package npm i next-mdx-remote and make these changes to your code:

import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import renderToString from 'next-mdx-remote/render-to-string'
import hydrate from 'next-mdx-remote/hydrate'

import { BLOG_PATH, blogFilePaths } from '../../utils/mdxUtils'
import { Image } from '../../components/Image'

const components = { Image }

const Blog = ({ source, frontMatter }) => {
    const content = hydrate(source, { components })

    return (
        <div>
            <h1>{frontMatter.title}</h1>
            {content}
        </div>
    )
}

...

export const getStaticProps = async ({ params }) => {
    const { slug } = params
    const blogFilePath = path.join(BLOG_PATH, `/blog/${slug}/index.mdx`)

    const source = fs.readFileSync(blogFilePath, 'utf-8');
    const { content, data } = matter(source);
    const mdxSource = await renderToString(content, { components });

    if (!blogFilePath) {
        console.warn('No MDX file found for slug')
    }

    return {
        props: {
            source: mdxSource,
            frontMatter: data,
        },
    }
}

export default Blog

But then you will have an issue when referencing assets inside your mdx files, the tailwind blog solves this by adding a loader for assets, unfortunately next-mdx-remote doesn't seem to support imports inside MDX (or probably need a specific configuration) so you'll have to move your images to the public folder, something like public/blog/Rustin_Cohle.jpg

The difference with the tailwind blog is that you're using Dynamic SSG

Also, there is this another approach that I didn't test completely https://github.com/vercel/next.js/issues/9524#issuecomment-580239600

diedu
  • 19,277
  • 4
  • 32
  • 49
  • I was using `next-mdx-remote` already but I wanted to have images inside the same folder as the post so I switched to this thing. the author of that library said it isn't a solvable problem as `next-mdx-remote` treats markdown content as data & doesn't pass through `webpack` at all. although another approach which you didn't test completely worked like a charm so thank you for that :) – deadcoder0904 Sep 25 '20 at 05:15
  • hey @deadcoder0904 - were you able to get imports inside of MDX working with the untested approach? I get "Cannot use import statement outside a module". – tdc Nov 26 '20 at 21:46
  • @T_Conroy yes, the untested approach worked fine. however, I switched from `next-mdx-remote`. I cloned tailwind repo → https://github.com/tailwindlabs/blog.tailwindcss.com & made some changes to it & used it. that is my new approach for now. – deadcoder0904 Nov 27 '20 at 05:23