Introduction

GitHub this note show how to use contentlayer and rehype-pretty-code to build mdx content blogs in nextjs

  • Setup contentlayer
  • Setup rehype-pretty-code
  • Configure theme and syntax highlighter

Setup Project

First let create a new next.js project

init project
npx create-next-app@latest

Then, let setup contentlayer

install dependencies
npm install contentlayer next-contentlayer

Project Structure

project structure
|--app
   |--global.css
   |--layout.css
   |--page.tsx
   |--blog
      |--page.tsx
|--assets
   |--moonlight-ii.json
|--posts
   |--post-1.mdx
   |--post-2.mdx
|--tailwind.config.js
|--package.json
|--next.config.js
|--contentlayer.config.js

Setup ContentLayer

Create and update the contentlayer.config.js as below

contentlayer.config.js
import { defineDocumentType, makeSource } from "contentlayer/source-files";
import remarkGfm from "remark-gfm";
import rehypePrettyCode from "rehype-pretty-code";
import rehypeSlug from "rehype-slug";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
 
export const Post = defineDocumentType(() => ({
  name: "Post",
  filePathPattern: `**/*.mdx`,
  contentType: "mdx",
  fields: {
    title: {
      type: "string",
      description: "The title of the post",
      required: true,
    },
    description: {
      type: "string",
    },
    date: {
      type: "date",
      description: "The date of the post",
      required: true,
    },
  },
  computedFields: {
    url: {
      type: "string",
      resolve: (post) => `/posts/${post._raw.flattenedPath}`,
    },
  },
}));
 
export default makeSource({
  contentDirPath: "posts",
  documentTypes: [Post],
  mdx: {
    remarkPlugins: [remarkGfm],
    rehypePlugins: [
      rehypeSlug,
      [
        // options,
        rehypePrettyCode,
        {
          theme: "github-dark",
          onVisitLine(node) {
            // Prevent lines from collapsing in `display: grid` mode, and allow empty
            // lines to be copy/pasted
            if (node.children.length === 0) {
              node.children = [{ type: "text", value: " " }];
            }
          },
          onVisitHighlightedLine(node) {
            node.properties.className.push("line--highlighted");
          },
          onVisitHighlightedWord(node) {
            node.properties.className = ["word--highlighted"];
          },
        },
      ],
      [
        rehypeAutolinkHeadings,
        {
          properties: {
            className: ["anchor"],
          },
        },
      ],
    ],
  },
});

Use Contentlayer

Now we can use contentlayer to transform the mdx file into json by npm run build and check the generated things in .contentlayer

build mdx
npm run build

Then just pass the generated content in to the MDXComponent and provide styles via components as above section

/post/[slug]/page.tsx
import { allPosts } from ".contentlayer/generated";
import { getMDXComponent } from "next-contentlayer/hooks";
 
const getPost = async () => {
  const post = allPosts[0];
  return post;
};
 
const mdxComponents = {
  // h2: ({ props }: any) => (
  //   <h2 className="prose text-3xl" apply="mdx.h2" {...props} />
  // ),
};
 
export const Post = async ({ params }: any) => {
  const post = allPosts[0];
 
  const MDXContent = getMDXComponent(post.body.code);
 
  return (
    <div className="dark:text-white dark:bg-slate-800 min-h-screen">
      <div className="max-w-4xl mx-auto px-10 prose-h2:text-lg prose dark:prose-invert">
        <MDXContent components={mdxComponents}></MDXContent>
      </div>
    </div>
  );
};
 
export default Post;

Rehype Pretty Code

  • Integrate with next/mdx
  • Integrate with contentlayer
  • Modify global.css

In this case, already setup rehype-pretty-code with contentlayer. Now let update the app/global.css as below

global.css
global.css
@tailwind base;
@tailwind components;
@tailwind utilities;
 
::selection {
background-color: #47a3f3;
color: #fefefe;
}
 
html {
min-width: 360px;
}
 
.prose .anchor {
@apply absolute invisible no-underline;
 
margin-left: -1em;
padding-right: 0.5em;
width: 80%;
max-width: 700px;
cursor: pointer;
}
 
.anchor:hover {
@apply visible;
}
 
.prose a {
@apply transition-all decoration-neutral-400 dark:decoration-neutral-600 underline-offset-2 decoration-[0.1em];
}
 
.prose .anchor:after {
@apply text-neutral-300 dark:text-neutral-700;
content: '#';
}
 
.prose \*:hover > .anchor {
@apply visible;
}
 
.prose pre {
@apply border border-neutral-800 bg-neutral-900;
}
 
.prose code {
@apply text-neutral-800 dark:text-neutral-200 px-1 py-0.5 border border-neutral-100 dark:border-neutral-800 rounded-lg bg-neutral-100 dark:bg-neutral-900;
}
 
.prose pre code {
@apply text-neutral-800 dark:text-neutral-200 p-0;
border: initial;
}
 
.prose img {
/_ Don't apply styles to next/image _/
@apply m-0;
}
 
.prose > :first-child {
/_ Override removing top margin, causing layout shift _/
margin-top: 1.25em !important;
margin-bottom: 1.25em !important;
}
 
code[class*='language-'],
pre[class*='language-'] {
@apply text-neutral-50;
}
 
pre::-webkit-scrollbar {
display: none;
}
 
pre {
-ms-overflow-style: none; /_ IE and Edge _/
scrollbar-width: none; /_ Firefox _/
}
 
/_ Remove Safari input shadow on mobile _/
input[type='text'],
input[type='email'] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
 
.prose .tweet a {
text-decoration: inherit;
font-weight: inherit;
}
 
table {
display: block;
max-width: fit-content;
overflow-x: auto;
white-space: nowrap;
}
 
.prose .callout > p {
margin: 0 !important;
}
 
[data-rehype-pretty-code-fragment] code {
@apply grid min-w-full break-words rounded-none border-0 bg-transparent p-0 text-sm text-black;
counter-reset: line;
box-decoration-break: clone;
}
[data-rehype-pretty-code-fragment] .line {
@apply py-1;
}
[data-rehype-pretty-code-fragment] [data-line-numbers] > .line::before {
counter-increment: line;
content: counter(line);
display: inline-block;
width: 1rem;
margin-right: 1rem;
text-align: right;
color: gray;
}
[data-rehype-pretty-code-fragment] .line--highlighted {
@apply bg-slate-500 bg-opacity-10;
}
[data-rehype-pretty-code-fragment] .line-highlighted span {
@apply relative;
}
[data-rehype-pretty-code-fragment] .word--highlighted {
@apply rounded-md bg-slate-500 bg-opacity-10 p-1;
}
[data-rehype-pretty-code-title] {
@apply px-4 py-3 font-mono text-xs font-medium border rounded-t-lg text-neutral-200 border-[#333333] bg-[#1c1c1c];
}
[data-rehype-pretty-code-title] + pre {
@apply mt-0 rounded-t-none border-t-0;
}

Reference