Next.js Discord

Discord Forum

Next MDX - Is it possible to avoid using dnagerouslySetInnerHTML to render HTML in this case?

Answered
Chum salmon posted this in #help-forum
Open in Discord
Chum salmonOP
Goal: Render markdown content as real HTML elements (<h1>, <ul>, <li>, etc.)

Currently I'm using dnagerouslySetInnerHTML to render raw HTML as real elements.
I understand that I can sanitize the content before putting it in dangerouslySetInnerHTML but I'm looking for a way to avoid using it.
Below is my current setup, I don't mind if I have to break away from this setup.

Here is the current flow
- I store .mdx files in /content/blog/
- In /blog/[slug]/page.tsx I read the content of an .mdx file using fs.readFile
- extract the content and data using matter from gray-matter, process it, and render it using dangerouslySetInnerHTML

Files:
root/content/blog/banana-benefits.mdx
---
title: "7 Surprising Benefits of Bananas"
date: "2025-05-26"
author: "Delta"
---

# 7 Surprising Benefits of Bananas

Bananas aren't just tasty — they're packed with nutrients!

- Rich in potassium
- Boosts energy
- Aids digestion
- Natural mood enhancer
- Supports heart health
- Great for workouts
- Easy to eat on the go


root/app/(pages)/blog/[slug]/page.tsx
import fs from 'fs/promises';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';

type Props = {
  params: { slug: string };
};

export default async function BlogPostPage({ params }: Props) {
  const { slug } = params;

  const fullPath = path.join(process.cwd(), 'content', 'blog', `${slug}.mdx`);
  const fileContent = await fs.readFile(fullPath, 'utf8');

  const { content, data } = matter(fileContent);

  const processedContent = await remark()
    .use(html)
    .process(content);

  const contentHtml = processedContent.toString();

  return (
    <div className="prose mx-auto py-8">
      <h1>{data.title}</h1>
      <p className="text-sm text-gray-500">{data.date} — {data.author}</p>
      <div dangerouslySetInnerHTML={{ __html: contentHtml }} />
    </div>
  );
}
Answered by Yi Lon Ma
just create it in blog page
View full answer

12 Replies

nextjs can render mdx content directly
you don't need to read the file using fs and then render it
just follow the docs and place your mdx files in blog folder
Chum salmonOP
I'll reread the doc, I didn't understand it well. Thank you!
@Yi Lon Ma nextjs can render mdx content directly
Chum salmonOP
Is my understanding correct about "nextjs can render mdx content directly"

So instead of having content/blog/banana-benefits.mdx
I put those mdx files in the app directory directly like this
app/
|--- blog/
| |---banana-benefits.mdx
| |---apple-benefits.mdx

So it's like a file-based approach? Then I can't dynamically render pages with slug? And have to create .mdx file per route?
So no more dynamic slug pages?:
app/
|-- blog/
| |-- [slug]
| |--- page.tsx
I mean you are already creating different files in content folder
just create it in blog page
Answer
or use something like content collections to read your mdx files
it could transform it
@Yi Lon Ma I mean you are already creating different files in content folder
Chum salmonOP
hm.. I have to think...
I can manually create blog posts but later on I'm gonna create a "create post" page for users. So the posts will be in database. That's why I think slug would be the best choice here.

Idk about content collections. I'll have to search.
Chum salmonOP
@Yi Lon Ma I ended up using only compileMDX from next-mdx-remote for all this.
I extract the content with compileMDX and simply display it.
blog/[slug]/page.tsx
import { promises as fs } from 'fs';
import path from 'path';
import { compileMDX } from 'next-mdx-remote/rsc';
import Testing from '@/app/components/mdx/Testing';

export async function generateMetadata({ params }: { params: { slug: string }}) {
    const content = await fs.readFile(path.join(process.cwd(), "content", `${params.slug}.mdx`), 'utf8');
    
    const {frontmatter} = await compileMDX<{ title: string, description: string }>({
        source: content,
        options: {
            parseFrontmatter: true
        },
    })
    return {
        title: frontmatter.title,
        description: frontmatter.description,
    }
}

export default async function BlogPostPage({ params }: { params: { slug: string }}) {

    const content = await fs.readFile(path.join(process.cwd(), "content", `${params.slug}.mdx`), 'utf8');
    
    const data = await compileMDX<{ title: string, nutrients: string[] }>({
        source: content,
        options: {
            parseFrontmatter: true
        },
        components: {
            Testing,
        }
    })

    console.log("data: ", data);

    return (
        <div>
            <h1 className="text-[40px] font-bold text-[var(--accent)]">{data.frontmatter.title}</h1>
            <div className="flex gap-[16px] mt-[24px]">
                {data.frontmatter.nutrients.map((nutrient) => (
                    <h4 
                        key={nutrient}
                        className="px-[16px] py-[8px] bg-black"
                    >
                        {nutrient}
                    </h4>
                ))}
            </div>
            <div className="mt-[24px]">
                {data.content}
            </div>
            
        </div>
    )
}
blog/page.tsx
import { promises as fs } from 'fs'
import { compileMDX } from 'next-mdx-remote/rsc';
import Link from 'next/link';
import path from 'path'

export default async function BlogPage() {

    const filenames = await fs.readdir(path.join(process.cwd(), 'content'));

    const projects = await Promise.all(filenames.map(async (filename) => {
        const content = await fs.readFile(path.join(process.cwd(), "content", filename), 'utf8');
        
        const {frontmatter} = await compileMDX<{ title: string, description: string }>({
            source: content,
            options: {
                parseFrontmatter: true
            },
        })
        return {
            filename,
            slug: filename.replace('.mdx', ''),
            ...frontmatter
        }
    }))

    return (
        <div>
            <h1>Blog Page</h1>
            <div className="">
                {projects.map((project) => (
                    <Link key={project.title} href={`/blog/${project.slug}`}>
                        <div>
                            {project.title}
                        </div>
                    </Link>
                ))}
            </div>
        </div>
    )
}