Liam Brock
Liam Brock

Reputation: 66

Why are my mdx files not showing simple markdown syntax like lists and headings? (Next.js, mdx-bundler)

I'm trying to use Kent C Dodds mdx-bundler on Next.js typescript blog starter example. It seems to render JSX and certain markdown ok but most of the simple markdown syntax like lists and adding spaces to create paragraphs are not working. I'm completely lost as to why it's doing this!

Here's the example mdx file i'm using to test:

---
title: "My first post"
description: "testing mdx"
publishedAt: "2021-12-23"
---

Skeleton component:

import {Skeleton} from '../components/Skeleton'

<Skeleton />

# Heading 1
## Heading 2

Heading
============

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 

Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 

Unordered list:

- a
- b

Ordered list:

1. one
2. two  

**bold** and *italic* and ***bold italic***
~~strikethrough~~

Horizontal rule:

*******

Blockquote:

> Lorem ipsum dolor sit amet, consectetur adipiscing elit
>
>> Lorem ipsum dolor sit amet, consectetur adipiscing elit

indented code block:

    return false;


backtick code block:

\``` (escaped here for stackoverflow)
return true;
\```

Image:

![alt](https://picsum.photos/200)

Here's the output i get:

image of my-first-post.mdx output

Here's the api.ts file that reads the contents of my mdx:

import fs from 'fs'
import { format, parseISO } from "date-fns"
import matter from 'gray-matter'
import path from "path"
import glob from 'glob'
import gfmPlugin from 'remark-gfm'
import remarkBreaks from 'remark-breaks'
import { bundleMDX } from "mdx-bundler"
import { PostMeta } from '../types/post'

const ROOT_PATH = process.cwd()
export const POSTS_PATH = path.join(ROOT_PATH, "posts")

export const getAllPostsMeta = () => {
  const PATH = path.join(POSTS_PATH)

  // Get all file paths in the posts folder (that end with .mdx)
  const paths = glob.sync(`${PATH}/**/*.mdx`)
  return (
    paths
      .map((filePath): PostMeta => {
        // Get the content of the file
        const source = fs.readFileSync(path.join(filePath), "utf8")
        // Get the file name without .mdx
        const slug = path.basename(filePath).replace(".mdx", "")
        // Use gray-matter to extract the post meta from post content
        const data = matter(source).data as PostMeta

        const publishedAtFormatted = format(
          parseISO(data.publishedAt),
          "dd MMMM, yyyy",
        )

        return {
          ...data,
          slug,
          publishedAtFormatted,
        }
      })

      // Sort posts by published date
      .sort(
        (a, b) =>
          Number(new Date(b.publishedAt)) - Number(new Date(a.publishedAt)),
      )
  )
}

// Get content of specific post
export const getPostBySlug = async (slug: string) => {
  // Get the content of the file
  const source = fs.readFileSync(path.join(POSTS_PATH, `${slug}.mdx`), "utf8")

  const { code, frontmatter } = await bundleMDX({
    source,
    xdmOptions(options) {
      options.remarkPlugins = [
        ...(options?.remarkPlugins ?? []),
        gfmPlugin,
        remarkBreaks,
      ]

      return options
    },
    esbuildOptions(options) {
      options.target = "esnext"
      return options
    },
  })

  const publishedAtFormatted = format(
    parseISO(frontmatter.publishedAt),
    "dd MMMM, yyyy",
  )

  const meta = {
    ...frontmatter,
    publishedAtFormatted,
    slug,
  } as PostMeta

  return {
    meta,
    code,
  }
}

This is the [slug].tsx file for rendering the selected post:

import React from 'react';
import Container from '../../components/container'
import Header from '../../components/header'
import PostHeader from '../../components/post-header'
import Layout from '../../components/layout'
import { format, parseISO } from "date-fns"
import { getMDXComponent } from "mdx-bundler/client"
import { GetStaticProps } from 'next';
import { getPostBySlug, getAllPostsMeta } from '../../lib/api'
import Head from 'next/head'
import type { Post } from "../../types/post"
import SectionSeparator from '../../components/section-separator'
import { WEBSITE_SHORT_URL } from '../../lib/constants';

export const getStaticPaths = () => {
  const posts = getAllPostsMeta()
  const paths = posts.map(({ slug }) => ({ params: { slug } }))

  return {
    paths: paths,
    // Return 404 page if path is not returned by getStaticPaths
    fallback: false,
  }
}

export const getStaticProps: GetStaticProps<Post> = async (context) => {
  const slug = context.params?.slug as string
  const post = await getPostBySlug(slug)

  return {
    props: {
      ...post,
      publishedAtFormatted: format(
        parseISO(post.meta.publishedAt),
        "dd MMMM, yyyy",
      ),
    },
  }
}

export default function PostPage({ meta, code }: Post) {
  const Component = React.useMemo(() => getMDXComponent(code), [code]);

  return (
    <Layout>
      <Container>
        <Header />
        <article className="mb-32">
          <Head>
            <title>
              {meta.title} | {WEBSITE_SHORT_URL}
            </title>
          </Head>
          <PostHeader
            title={meta.title}
            coverImage={meta.coverImage}
            date={meta.publishedAt}
          />
          <div className="max-w-4xl mx-auto">
            <Component
              components={{
                SectionSeparator
              }}
            />
          </div>
        </article>
      </Container>
    </Layout>
  );
}

Other things that may be of use...

My Next.js folder structure:

+-- next/
+-- @types/
+-- components/
+-- lib
|   +-- api.ts
+-- node_modules/
+-- pages
|   +-- posts
|   |   +-- [slug].tsx
|   +-- _app.tsx
|   +-- _document.tsx
|   +-- index.tsx
+-- posts
|   +-- my-first-post.mdx
+-- public/
+-- styles/
+-- types/
+-- .gitignore
+-- next-env.d.ts
+-- package-lock.json
+-- package.json
+-- postcss.config.js
+-- tailwind.config.js
+-- tsconfig.json

Package.json:

{
  "private": true,
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start",
    "typecheck": "tsc"
  },
  "dependencies": {
    "@headlessui/react": "^1.4.2",
    "@mdx-js/loader": "^1.6.22",
    "@next/mdx": "^12.0.7",
    "@reduxjs/toolkit": "^1.7.1",
    "axios": "^0.24.0",
    "classnames": "2.3.1",
    "clsx": "^1.1.1",
    "date-fns": "2.21.3",
    "esbuild": "^0.13.15",
    "glob": "^7.2.0",
    "gray-matter": "4.0.3",
    "mdx-bundler": "^8.0.0",
    "next": "latest",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-hook-form": "^7.22.1",
    "react-redux": "^7.2.6",
    "react-scroll": "^1.8.4",
    "react-use": "^17.3.1",
    "react-use-scroll-direction": "^0.1.0",
    "remark": "13.0.0",
    "remark-breaks": "^3.0.2",
    "remark-gfm": "^3.0.1",
    "remark-html": "~13.0.1",
    "typescript": "^4.2.4"
  },
  "devDependencies": {
    "@types/glob": "^7.2.0",
    "@types/jest": "^26.0.23",
    "@types/node": "^15.6.0",
    "@types/react": "^17.0.6",
    "@types/react-dom": "^17.0.5",
    "@types/react-scroll": "^1.8.3",
    "autoprefixer": "^10.2.5",
    "postcss": "^8.3.0",
    "tailwindcss": "^2.1.2"
  }
}

Upvotes: 0

Views: 3461

Answers (2)

Nathnael
Nathnael

Reputation: 73

I had the same issue and I was able to fix it by using a package called @tailwindcss/typography. so first install the package.

npm install @tailwindcss/typography

after that you need to add it as a plugin to the tailwind config file

plugins: [
  require('@tailwindcss/typography')
],

for more detail this blog definitely helped me. here's the link

Upvotes: 5

Liam Brock
Liam Brock

Reputation: 66

Ok got this problem fixed by piping in a custom component with custom css to handle the vanilla markdown syntax (shoutout to homu from Next.js discord for the help).

In MDXComponents.tsx:

export const components = {
    h1: (props) => <h1 className="text-5xl" {...props} />
    ol: (props: any) => <ol className="list-decimal" {...props} />
    ...//etc
}

Then in [slug].tsx pass components as a prop:

...
import { components } from '../../components/MDXComponents';

...

export default function PostPage({ meta, code }: Post) {
  const Component = React.useMemo(() => getMDXComponent(code), [code]);

  return (
    <Layout>
      <Container>
        <Header />
        <article className="mb-32">
          <Head>
            <title>
              {meta.title} | {WEBSITE_SHORT_URL}
            </title>
          </Head>
          <PostHeader
            title={meta.title}
            coverImage={meta.coverImage}
            date={meta.publishedAt}
          />
          <div className="max-w-4xl mx-auto">
            <Component
              components={components} // <-- components passed here
            />
          </div>
        </article>
      </Container>
    </Layout>
  );
}

Upvotes: 2

Related Questions