GitHub this note show how to use contentlayer and rehype-pretty-code to build mdx content blogs in nextjs
First let create a new next.js project
npx create-next-app@latest
Then, let setup contentlayer
npm install contentlayer next-contentlayer
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
Create and update the contentlayer.config.js as below
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"],
},
},
],
],
},
});
Now we can use contentlayer to transform the mdx file into json by npm run build and check the generated things in .contentlayer
npm run build
Then just pass the generated content in to the MDXComponent and provide styles via components as above section
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;
In this case, already setup rehype-pretty-code with contentlayer. Now let update the app/global.css as below
@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;
}