Sulejman
Sulejman

Reputation: 161

Remark and rehype dont convert mdx correctly in nextjs in app router

I have published my code in github if anyone wants to clone it. i also uploaded in code sandbox.

Basically i wanted to create a blog with nextjs with md or mdx and so i looked at the nextjs docs. i saved some mdx files in the blogposts folder and tried two ways to render the blogs on the website like the docs say:

  1. With file based routing creating a page.mdx instead of page.tsx in a route in app folder. (you can test this in /example route)
  2. Dynamically reading all MDX files from a blogposts folder and creating dynamic routes that render the MDX content based on the file name (e.g., /posts/test).

Here's the behavior I've observed:

In the first approach (/example), the MDX content renders correctly, and some plugins like rehype-pretty-code work, but others, like remark-toc, do not. Additionally, custom imported React components do not function; only Markdown is rendered. In the second approach (/posts/test), none of the plugins function correctly, including rehype-pretty-code.

Below are the relevant parts of my code setup:

mdx-components.tsx

import type { MDXComponents } from "mdx/types";

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    ...components,
  };
}

next.config.mjs

import createMDX from "@next/mdx";
import remarkRehype from "remark-rehype";
import remarkToc from "remark-toc";
import rehypeSanitize from "rehype-sanitize";
import rehypeStringify from "rehype-stringify";
import rehypePrettyCode from "rehype-pretty-code";
import rehypeAutolinkHeadings from "rehype-autolink-headings";

const nextConfig = {
  pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
};

// Use dynamic import for rehype-highlight
const withMDX = async () => {
  return createMDX({
    options: {
      remarkPlugins: [remarkToc, remarkRehype],
      rehypePlugins: [
        rehypeSanitize,
        rehypeStringify,
        rehypeAutolinkHeadings,
        [
          rehypePrettyCode,
          {
            theme: "one-dark-pro",
          },
        ],
      ],
    },
  })(nextConfig);
};

export default withMDX();

app/posts/[slug]/page.tsx

import getFormattedDate from "@/lib/getFormattedDate";
import { getBlogPosts } from "@/lib/posts";
import Link from "next/link";
import { notFound } from "next/navigation";

export function generateStaticParams() {
  let posts = getBlogPosts();

  return posts.map(async (post) => ({
    slug: (await post).slug,
  }));
}

export default async function Post({ params }: { params: { slug: string } }) {
  let post = getBlogPosts().find(
    async (post) => (await post).slug === params.slug
  );

  if (!post) {
    notFound();
  }

  return (
    <>
      <main>
        <h1 className="title font-semibold text-2xl tracking-tighter">
          {(await post).metadata.title}
        </h1>
        <div className="flex justify-between items-center mt-2 mb-8 text-sm">
          <p className="text-sm text-neutral-600 dark:text-neutral-400">
            {getFormattedDate((await post).metadata.publishedAt)}
          </p>
        </div>

        <article
          className="prose prose-slate"
          dangerouslySetInnerHTML={{ __html: (await post).content }}
        ></article>
        <Link href="/">← Back to home</Link>
      </main>
    </>
  );
}

and finally the code used to read the blogposts mdx files:

/lib/posts.ts

import fs from "fs";
import path from "path";
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import remarkFrontmatter from "remark-frontmatter";
import remarkToc from "remark-toc";
import rehypeSanitize from "rehype-sanitize";
import rehypeStringify from "rehype-stringify";
import rehypePrettyCode from "rehype-pretty-code";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import html from "remark-html";
import matter from "gray-matter";

export function getBlogPosts() {
  const dir = path.join(process.cwd(), "blogposts");
  const mdxFiles = fs
    .readdirSync(dir)
    .filter((file) => path.extname(file) === ".mdx");

  const blogPosts = mdxFiles.map(async (file) => {
    const filePath = path.join(dir, file);
    const fileContents = fs.readFileSync(filePath, "utf-8");

    // Use gray-matter to parse the post metadata section
    const matterResult = matter(fileContents);

    const processedContent = await unified()
      .use(html)
      .use(remarkParse)
      .use(remarkFrontmatter)
      .use(remarkRehype)
      .use(rehypePrettyCode, {
        theme: "one-dark-pro",
      }) // Prettify code blocks
      .use(rehypeAutolinkHeadings) // Add anchor links to headings
      .use(remarkToc) // Generate table of contents
      .use(rehypeSanitize) // Sanitize HTML input
      .use(rehypeStringify)
      .process(matterResult.content);

    const content = processedContent.toString();

    let slug = path.basename(file, path.extname(file));

    return {
      metadata: matterResult.data,
      slug,
      content,
    };
  });

  return blogPosts;
}

Has anyone faced similar issues or knows how to resolve the plugin and rendering problems in these scenarios? Its the first time i am using these libraries and i am not familiar with many of the plugins, if you guys think many of the plugins are not nescessary please tell me :). Additionally, as I plan to manage the blogs through a database, can the getBlogPosts() function be adapted to fetch from a database instead of reading from files without significant changes?

Thank you a lot for your help.

Upvotes: 0

Views: 433

Answers (1)

al3xg
al3xg

Reputation: 21

I am not sure if this is the underlying issue, but the getBlogPosts declaration is missing the async keyword.

I checked the repo and found the usage of this function

  let post = getBlogPosts().find(
    async (post) => (await post).slug === params.slug
  );

and it seems like Array.find() doesn't work with an async callback.

See Using an async function in Array.find()

Upvotes: 0

Related Questions