Blog app with Next.js and Contentlayer

April 6, 2023

Learn how to construct a dynamic and scalable blog application using Next.js version 13 and Contentlayer. This tutorial guides you through the essential steps, from initial setup to deployment. Harness Next.js for server-side rendering and Contentlayer for efficient content management. By the end, you'll have a fully functional blog with improved load times and SEO capabilities.

Configuration

First, we need to install contentlayer packages

npm i contentlayer next-contentlayer

Now, export the contentlayer config in the next.config.js:

/** @type {import('next').NextConfig} */
const { withContentlayer } = require("next-contentlayer");
const nextConfig = {};

module.exports = withContentlayer(nextConfig);

Now, we need to create a contentlayer.config.ts file in the root of the project and add the following code:

import { defineDocumentType, makeSource } from "contentlayer/source-files";

const Post = defineDocumentType(() => ({
  name: "Post",
  filePathPattern: `**/*.mdx`,
  contentType: "mdx",
  fields: {
    title: {
      type: "string",
      description: "The title of the post",
      required: true,
    },
    date: {
      type: "date",
      description: "The date of the post",
      required: true,
    },
  },
  computedFields: {
    url: {
      type: "string",
      resolve: (doc) => `/posts/${doc._raw.flattenedPath}`,
    },
  },
}));

export default makeSource({
  contentDirPath: "posts",
  documentTypes: [Post],
});

Above code is the config for the Contentlayer. It will create a Post document type and will generate the url field based on the flattenedPath field.

You can add more document types and fields such as Author and Category and more.

Now, we need to create a posts folder in the root of the project and create a hello-world.mdx file in it and add the following code:

You can name the file whatever you want. In this tutorial, we will use hello-world.mdx:

---
title: How to create Next.js app
date: 2023-04-06
---

# Install Next.js

### Fist you need to install Next.js with the following command:

npm i create-next-app@latest

Above code is the mdx file. You can use md instead of mdx. You can add more fields such as description and author and more.

Then, build the app with the following command

npm run build

Now, you can see new .contentlayer folder in the root of the project. This folder contains the generated JSON files, types and more.

generate contentlayer folder

We need to install date-fns for sorting posts by date and format the date:

npm i date-fns

And we need to add the following code in the tsconfig.json file:

"paths": {
  "@/*": ["./*"],
  "contentlayer/generated": ["./.contentlayer/generated"]
}

Above code is for importing the generated files from the .contentlayer folder.

Now, we need to create a posts folder in the app folder and create a [slug] folder in it. and create a page.tsx file in [slug] folder and add the following code:

app/posts/[slug]/page.tsx

import { Post, allPosts } from "contentlayer/generated";
import { getMDXComponent } from "next-contentlayer/hooks";
import { format, parseISO } from "date-fns";
import { Metadata } from "next";

type Props = {
  params: { slug: string };
  searchParams: { [key: string]: string | string[] | undefined };
};

export const generateStaticParams = async () =>
  allPosts.map((post:Post) => ({ slug: post._raw.flattenedPath }));
export const generateMetadata = ({ params }: Props): Metadata => {
  const post = allPosts.find(
    (post: Post) => post._raw.flattenedPath === params.slug: any
  );
  return { title: post?.title, description: post?.description };
};

const PostLayout = ({ params }: { params: { slug: string } }) => {
  const post = allPosts.find((post: Post) => post._raw.flattenedPath === params.slug);

  let MDXContent;

  if (!post) {
    return <div>404</div>;
  } else {
    MDXContent = getMDXComponent(post!.body.code);
  }

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{format(parseISO(post.date), "LLLL d, yyyy")}</p>
      <article>
        <MDXContent />
      </article>
    </div>
  );
};

export default PostLayout;

Above code is the page for the blog post. It will render the blog post based on the slug parameter.

Now, you can see the blog rendered in the http://localhost:3000/posts/hello-world:

blog rendered

Styling the blog

Now, we need to style the blog using CSS or others. In this tutorial, we will use CSS:

globals.css

pre {
  padding: 15px 20px;
  border-radius: 10px;
  background-color: #f9f8f981;
  overflow: auto;
  font-size: 0.9rem;
  margin: 40px 0;
}
article p {
  font-size: 1rem;
  line-height: 1.8rem;
  margin-top: 20px;
}
article h1 {
  font-size: 2.5rem;
  line-height: 3.5rem;
  margin-top: 60px;
  font-weight: 425;
}

Get full example css code from here

Now, you can see the blog styled:

styled blog

Also you can use TailwindCSS or others.

Custom components

Custom components is a great feature of MDX. You can create your own components and use them in your blog posts.

For example, we can create a CodeSnippet component:

// app/posts/[slug]/page.tsx
import { allPosts } from "contentlayer/generated";
import { getMDXComponent } from "next-contentlayer/hooks";
import { format, parseISO } from "date-fns";
import { Snippet } from "@geist-ui/core";

...

const CodeSnippet = (props: any) => (
 <Snippet {...props} text={props.text} />
);

 return (
    <div>
      <h1>
        {post.title}
      </h1>
      <p>{format(parseISO(post.date), "LLLL d, yyyy")}</p>
      <article>
        <MDXContent components={{ CodeSnippet }} />
      </article>
    </div>
  );

export default PostLayout;

Now, you can use the CodeSnippet component in your blog posts:

---
title: How to create a Next.js app
date: 2023-04-06
---

# Install Next.js

### First, install Next.js using the following command:

<CodeSnippet text="npx create-next-app@latest" />

Now, you can see the CodeSnippet component in the blog post:

code snippet

List all posts

Now, we need to create a page that lists all posts. For this, we need to create a posts folder in the app folder and create a page.tsx file in it and add the following code:

app/posts/page.tsx

import Link from "next/link";
import { allPosts, Post } from "contentlayer/generated";
import { compareDesc } from "date-fns";

function PostCard(post: Post) {
  return (
    <div>
      <h2>
        <Link href={post.url} legacyBehavior>
          {post.title}
        </Link>
      </h2>
      <p>{post.description}</p>
    </div>
  );
}

function page() {
  const posts = allPosts.sort((a, b) =>
    compareDesc(new Date(a.date), new Date(b.date))
  );

  return (
    <div>
      <div>
        {posts.map((post, idx) => (
          <PostCard key={idx} {...post} />
        ))}
      </div>
    </div>
  );
}

export default page;

Now, you can see the list of all posts in the http://localhost:3000/posts:

list of all posts

Highlight codes

Now, we need to highlight the codes in the blog posts. For this, we will use rehype-pretty-code:

npm i rehype-pretty-code shiki

Now, we need to add the rehype-pretty-code plugin to the contentlayer.config.ts file:

contentlayer.config.ts

import { defineDocumentType, makeSource } from "contentlayer/source-files";
import rehypePrettyCode from "rehype-pretty-code";

const Post = defineDocumentType(() => ({
  ...
}));

const rehypeoptions = {
    // Use one of Shiki's packaged themes
    theme: "light-plus",
    // Set to true to keep the background color
    keepBackground: true ,
    onVisitLine(node: any) {
      if (node.children.length === 0) {
        node.children = [{ type: "text", value: " " }];
      }
    },
    onVisitHighlightedLine(node: any) {
      node.properties.className.push("highlighted");
    },
    onVisitHighlightedWord(node: any, id: any) {
      node.properties.className = ["word"];
    },
  };

export default makeSource({
  contentDirPath: "posts",
  documentTypes: [Post],
  mdx: {
    rehypePlugins: [[rehypePrettyCode, rehypeoptions]],
  },
});

Now, you can see the highlighted codes in the blog posts:

highlighted codes

You can find more information about rehype-pretty-code and Shiki themes.

Finally you can start building your blog with Contentlayer and Next.js. The great feature of Contentlayer is that Contentlayer will automatically save and refresh the blog when you add a new blog post or edit an existing blog post.

Conclusion

In this tutorial, we have learned how to create a blog using Next.js, Contentlayer, and MDX. We have learned how to create a blog post, how to list all posts, and how to highlight codes in the blog posts.

You can find the source code of the Contentlayer blog in the following GitHub repository:

Source code of example

Resources