Create MDX Blog in your app using Contentlayer and Next.js

In this Blog we will learn how to add mdx to you website.

User Avatar

Faisal Husain

Create MDX Blog in your app using Contentlayer and Next.js blog image

Hello every one i decided to add my blogs to my website . Here is how I built it using Next.js and contentlayer

Previously I used to write blogs on Hashnode (as if I have written many blog 😝). So previously I added the blogs link here. But one day I wanted dont know why that my blogs should be on my website,which is also good for seo. So here we are if you are reading this then surely I have intergrated contentlayer to my website.

okay let's get started

tech stack which I am going to use

  • next.js
  • contentlayer
  • tailwind

Start by creating a nextjs application

terminal
npx create-next-app@latest

now add contentlayer to the project

terminal
npm add contentlayer next-contentlayer

here come you first error ❌

don't error is quite self explainatory

terminal
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: mdx-example@0.1.0
npm ERR! Found: next@14.2.4
npm ERR! node_modules/next
npm ERR!   next@"14.2.4" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer next@"^12 || ^13" from next-contentlayer@0.3.4
npm ERR! node_modules/next-contentlayer
npm ERR!   next-contentlayer@"*" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
npm ERR!
npm ERR! For a full report see:
npm ERR! C:\Users\faisa\AppData\Local\npm-cache\_logs\2024-06-17T06_22_43_221Z-eresolve-report.txt
 
npm ERR! A complete log of this run can be found in: C:\Users\faisa\AppData\Local\npm-cache\_logs\2024-06-17T06_22_43_221Z-debug-0.log

so as what these commands say let's do that

terminal
npm add contentlayer next-contentlayer --force

now it should be installed (meybe in future this error get resolved but for now this is the solution ✅)

now we need to configure next.config.js

Note: In the nextjs by default next.config.mjs is there just rename it to next.config.js (it does not work with .mjs)

next.config.js
const { withContentlayer } = require("next-contentlayer");
/** @type {import('next').NextConfig} */
const nextConfig = {
  pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
  reactStrictMode: true,
  swcMinify: true,
};
 
module.exports = withContentlayer(nextConfig);

now change tsconfig.json

tsconfig.json
{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "baseUrl": ".",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./*"],
      "contentlayer/generated": ["./.contentlayer/generated"]
    }
  },
  "include": 
  ["next-env.d.ts",
   "**/*.ts",
   "**/*.tsx",
   ".next/types/**/*.ts",
   "contentlayer/generated"],
  "exclude": ["node_modules"]
}

now a create a folder in you root directory

app
content
blog
my-first-blog.mdx
public
....
package-lock.json
package.json
tsconfig.json

in the mdx you will write

/content/blog/my-first-blog.mdx
---
title: My first blog
date: 16th june 2024
---
 
Hello world here is my first blog

now the most important file "contentlayer.config.ts"

/contentlayer.config.ts
import { defineDocumentType, makeSource } from "contentlayer/source-files";
 
const Blog = defineDocumentType(() => ({
    name: "Blog",
    filePathPattern: `blog/**/*.mdx`,
    contentType: 'mdx',
    fields: {
        title: { type: "string", required: true },
        date: { type: "string", required: true },
 
    },
}));
 
export default makeSource({
    contentDirPath: "content",
    documentTypes: [Blog]
})

in the above file you declare the path to your content the field which you want to be definitely want in your content.

now when you run :

terminal
npm run dev

you will see

terminal
✅ Ready in 5.9s
Generated 1 documents in .contentlayer

the .contentlayer is the folder generated in your root layout you can see what is generated by contentlayer

now how we display the generated data on our app

now get all blog from the generated .contentlayer folder and display them by title

app/page.tsx
import Link from 'next/link'
import { allBlogs } from '../.contentlayer/generated'
 
export default function Home() {
  const blogs = allBlogs
  return (
    <main className="flex min-h-screen flex-col items-start  justify-center p-24">
      <div className='text-2xl mb-10'>All Blogs</div>
      {blogs.map((blog, index) => (
        <Link href={`/${blog._raw.flattenedPath}`} key={index} className='hover:underline'>{blog.title}</Link>
      ))}
    </main>
  )
}
 

now create a dynamic folder

app/blog/[slug]/page.tsx
import { allBlogs } from '@/.contentlayer/generated'
 
export default async function Blog({ params }: { params: any }) {
  console.log(allBlogs[0]._raw.flattenedPath)
  const blog = allBlogs.find(
    (blog) => blog._raw.flattenedPath === `blog/${params.slug}`,
  )
  if (!blog) {
    return
  }
 
  return (
    <div className="flex flex-col  pt-28 text-white  bg-zinc-900 items-center justify-start mx-auto min-h-screen px-3  md:px-0">
      <article className=" mb-10 max-w-[700px] ">
        <div className="flex flex-col gap-8 mx-1 md:mx-0   ">
          <div className="flex max-w-2xl flex-col gap-4 text-pretty  w-full">
            <h1 className="text-3xl font-bold leading-tight tracking-tight  text-left">
              {blog.title}
            </h1>
          </div>
        </div>
      </article>
    </div>
  )
}

now when you click on rendered blog you will be taken to that blog.

now how to render the blog body as it is mdx we will be using mdx renderer to render the body

make a new folder "_components"

app/blog/_component/mdx-component.tsx
 
import { useMDXComponent } from "next-contentlayer/hooks";
 
interface MdxProps {
  code: string;
}
 
export function Mdx({ code }: MdxProps) {
  const Component = useMDXComponent(code);
 
  return (
    <div className="mdx">
        <Component />
    </div>
  );
}
 

now update

app/blog/[slug]/page.tsx
import { allBlogs } from '@/.contentlayer/generated'
import { Mdx } from '../_components/mdx-component'
 
export default async function Blog({ params }: { params: any }) {
  const blog = allBlogs.find(
    (blog) => blog._raw.flattenedPath === `blog/${params.slug}`,
  )
  if (!blog) {
    return
  }
 
  return (
    <div className="flex flex-col  pt-28 text-white  bg-zinc-900 items-start  justify-start mx-auto min-h-screen px-3  md:px-0">
      <article className=" mb-10 max-w-[700px] p-10  ">
        <div className="flex flex-col gap-8 mx-1 md:mx-0   ">
          <div className="flex max-w-2xl flex-col gap-4 text-pretty  w-full">
            <h1 className="text-3xl font-bold leading-tight tracking-tight  text-left">
              {blog.title}
            </h1>
          </div>
          <div>
            <Mdx code={blog.body.code} />
          </div>
        </div>
      </article>
    </div>
  )
}

as you can see in line number i have added Mdx component now you body will be rendered.

Everything should be working fine by this time buts its not .

now if you try add "## Heading 2 " or "## Heading 1" nothing will work

here comes this package to rescue us tailwindcss-typography

terminal
npm install -D @tailwindcss/typography --force

update tailwind.config.ts

tailwind.config.ts
import type { Config } from "tailwindcss";
 
const config: Config = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      backgroundImage: {
        "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
        "gradient-conic":
          "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
      },
    },
  },
  plugins: [ require('@tailwindcss/typography')],
};
export default config;

update this file

app/blog/[slug]/page.tsx
import { allBlogs } from '@/.contentlayer/generated'
import { Mdx } from '../_components/mdx-component'
 
export default async function Blog({ params }: { params: any }) {
  const blog = allBlogs.find(
    (blog) => blog._raw.flattenedPath === `blog/${params.slug}`,
  )
  if (!blog) {
    return
  }
 
  return (
    <div className="flex flex-col  pt-28 text-white  bg-zinc-900 items-start  justify-start mx-auto min-h-screen px-3  md:px-0">
      <article className=" mb-10 max-w-[700px] p-10  ">
        <div className="flex flex-col gap-8 mx-1 md:mx-0   ">
          <div className="flex max-w-2xl flex-col gap-4 text-pretty  w-full">
            <h1 className="text-3xl font-bold leading-tight tracking-tight  text-left">
              {blog.title}
            </h1>
          </div>
          <div className='prose prose-headings:text-white prose-p:text-white'>
            <Mdx code={blog.body.code} />
          </div>
        </div>
      </article>
    </div>
  )
}
 

what I did above you can read here .do give a read it will be used very much

now if you add "## headind 2 " it will work

blog/my-first-blog
---
title: My first blog
date: 16th june 2024
---
 
Hello world here is my first blog
 
## Heading 2
 
# Heading 1

now your basic mdx website is working in next blog we will learn how to add codeblocks and out own custom components.

if you dont want to follow the whole blog you can find source code here

Thank you

HomeAboutProjectsBlogsHire Me