← Back to all posts

Harnessing the power of Markdown with Contentlayer and Nextjs to build a modern blog

May 25, 2024Jadan JonesBlog, Tutorial, Nextjs, Contentlayer⏱ 19 min read



Harnessing the power of Markdown with Contentlayer and Nextjs to build a modern blog

Harnessing the power of Markdown with Contentlayer and Nextjs to build a modern blog

Creating a blog is a great way to share your knowledge and expertise with others. However, setting up a full-stack blog can be daunting if you're just getting started with web development. In this article, I'll show you just how easy it is to build a full-fledged blog using some of the most popular tools available today.

We'll be using NextJS for the frontend, Contentlayer to handle Markdown content, and Tailwind CSS for styles. And for hosting? We'll deploy everything to Vercel with a single command. By the end, you'll have a fully functioning blog up and running in no time!

Why NextJS?

As a React framework, NextJS makes building powerful websites and applications incredibly quick and simple. Some key advantages for blogging include:

  • Static generation - NextJS can pre-render pages at build time, providing lightning fast load times and optimizing sites for SEO. Perfect for blogs!
  • File-based routing - Pages are defined as .js or .jsx files in the pages directory, keeping routing clean and intuitive.
  • Image optimization - NextJS can automatically optimize images, reducing payload sizes.
  • Easy deployment - Vercel integration makes deploying NextJS sites a breeze, as we'll see later!

So in summary, NextJS gives us a simple fast foundation to build our blog upon.

Enter Contentlayer

While Next.js 14 has built-in support for Markdown, we're going to take things up a notch by introducing ContentLayer into the mix. ContentLayer is a powerful content management system that seamlessly integrates with Next.js, making it a breeze to fetch and render Markdown content on your site.

With ContentLayer, you can organize your content into a structured data model, making it easier to manage and maintain your blog posts. There is no need to involve complex workflows and heavyweight dependencies that will slow down your client. Through the magic of Content Layer, all Markdown processing occurs at build time - eliminating client-side weight for maximum performance!

Contentlayer provides:

Simple Setup - Contentlayer requires just a config file and basic CLI usage. No deploying additional services.

Fast Performance - By avoiding a database or API server, Contentlayer's static site integration is faster and more affordable to run.

Lower Resource Usage - Since content is stored in files rather than a database, Contentlayer doesn't require as much memory, CPU or network bandwidth.

Easy Migration - If needs change, content can be exported to a new platform since it's stored in plain Markdown files without proprietary lock-in.

Hassle-Free Maintenance - We don't have to worry about database administration, upgrades or backups when using Contentlayer.

Soooo Let's get started!

Step 1: Installing Next.js and Tailwind

First things first, we need to set up our Next.js project. Open your terminal and run the following command to create a new Next.js app and ensure you select yes for tailwind css:

npx create-next-app@latest my-blog-site

On installation, you'll see the following prompts (select accordingly):

What is your project named? my-blog-site
Would you like to use TypeScript? No
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like to use `src/` directory? Yes
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias (@/*)? No

This will create a new directory called my-blog-site with all the necessary files and dependencies for a basic Next.js project.

Next you will navigate to your new project folder

cd my-blog-site

Step 2: Installing Dependencies

Important

Before installing dependencies add this code to your package.json file:

"overrides": {
	"next-contentlayer": {
	"next": "$next"
	}
}

next-contentlayer has recently become unmaintained, this means a package upgrade was not created for nextjs14. However we can still take advantage of its features by overriding the dependency.

Your full file should look something like this:

"overrides": {
	"next-contentlayer": {
	"next": "$next"
	}{
    "name": "my-blog-site",
    "version": "0.1.0",
    "private": true,
    "scripts": {
        "dev": "next dev",
        "build": "next build",
        "start": "next start",
        "lint": "next lint"
    },
    "dependencies": {
        "next": "14.2.3",
        "react": "^18",
        "react-dom": "^18"
    },
    "devDependencies": {
        "eslint": "^8",
        "eslint-config-next": "14.2.3",
        "postcss": "^8",
        "tailwindcss": "^3.4.1"
    },
    "overrides": {
        "next-contentlayer": {
            "next": "$next"
        }
    }
}
}

Next, we'll install the required dependencies for our blog site. Navigate to the project directory and run the following command:

npm install next-contentlayer contentlayer @tailwindcss/typography remark-gfm

Here's what each dependency does:

  • next-contentlayer: This is the core library that provides the integration between Next.js and ContentLayer.
  • contentlayer: This plugin allows ContentLayer to read Markdown files from your project.
  • @tailwindcss/typography: This plugin provides stylish typographic defaults for Markdown content.
  • remark-gfm: This plugin enables support for GitHub Flavored Markdown syntax in your Markdown content.

Step 3: Configuring ContentLayer

Now that we have all the dependencies installed, it's time to configure ContentLayer. Create a new file called contentlayer.config.js in the root of your project and add the following code:

import { defineDocumentType, makeSource } from 'contentlayer/source-files';
import  remarkGfm  from  "remark-gfm";
 
export const Blog = defineDocumentType(() => ({
  name: 'Blog',
  filePathPattern: `blog/*.mdx`,
  contentType: 'mdx',
  fields: {
    title: {
      type: 'string',
      required: true,
    },
    date: {
      type: 'date',
      required: true,
    },
  },
  computedFields: {
    slug: {
      type: 'string',
      resolve: (doc) => `/${doc._raw.flattenedPath}`
    },
  },
}));
 
export default makeSource({
  contentDirPath: 'content',
  documentTypes: [Blog],
  mdx: { remarkPlugins: [remarkGfm] }
});

In this configuration file, we define a new document type called Blog using defineDocumentType. This document type represents our blog posts, and we specify that the content will be stored in Markdown files with the .mdx extension in a folder called blog.

We define two fields for our blog posts: title (a required string) and date (a required date). We also define a computed field called slug, which will be used for generating the URL paths for each blog post.

Finally, we create a new source using makeSource , specify the contentDirPath as 'content', which is where we'll store our Markdown files and set contentlayer to use the remark-gfm plugin. We also pass the Blog document type to the documentTypes array.

Step 4: Adding Markdown Content

With our ContentLayer configuration in place, it's time to add some Markdown content. Create a new directory called content in the root of your project, and inside that directory, create a new folder named blog.

Inside the blog folder, create a new file called my-first-post.mdx and add the following content:

--- 
title: My First Blog Post 
date: 2023-05-24 
--- 
# My First Blog Post 
 
This is the content of my first blog post written in Markdown. 
You can use all the standard Markdown syntax here, including: 
- Headings 
- Lists 
-  **Bold** and _Italic_ text 
-  [Links](https://example.com) 

Pretty neat, right?

Notice the front matter at the top of the file, where we define the title and date fields for this blog post.

This is how ContentLayer knows which fields to extract from the Markdown file. You can create as many blog posts as you want by adding more *.mdx files to the content/blog directory.

Step 5: Rendering Blog Posts

Now that we have our Markdown content set up, it's time to render it on our Next.js site. Create a new blog folder, inside that folder create a [slug folder and place a page.js file inside. The file path should look like blog/[slug]/page.js in your src/app folder and add the following code:

import { allBlogs } from "contentlayer/generated";
import { getMDXComponent } from "next-contentlayer/hooks";
 
export async function generateStaticParams() {
  return allBlogs.map((blog) => ({
    slug: blog._raw.flattenedPath.split("/").pop(),
  }));
}
 
export default async function BlogPage({ params }) {
  const blog = allBlogs.find(
    (blog) => blog._raw.flattenedPath === "blog/" + params.slug
  );
  let MDXContent;
  if (!blog) {
    console.log("post not found");
  } else {
    MDXContent = getMDXComponent(blog.body.code);
  }
  return (
    <>
      <div className="pt-10 pb-20 lg:py-20 container px-4 mx-auto">
        <div className="mx-auto max-w-4xl">
          <div className="text-center mb-16 max-w-4l mx-auto">
            <h1 className="text-slate-900 text-center text-4xl/none lg:text-6xl/none font-medium'">
              {blog.title}
            </h1>
          </div>
 
          <article className="prose prose-slate mx-auto max-w-2xl">
            <MDXContent />
          </article>
        </div>
      </div>
    </>
  );
}

In this file, we import the useMDXComponent hook from next-contentlayer/hooks, which allows us to render the Markdown content as React components. We also import the allBlogs array from the contentlayer/generated file, which contains all our blog posts.

The BlogPost component receives the blog prop, which contains the data for the current blog post. We use the useMDXComponent hook to render the Markdown content of the blog post and wrap it in a div with the prose class from the @tailwindcss/typography plugin. This ensures that our Markdown content is styled nicely out of the box.

The getStaticParams function generates the paths for all the blog posts, so that Next.js can pre-render them at build time. We map over the allBlogs array and return an array of path objects with the slug parameter for each blog post.

Finally, the getStaticProps function fetches the blog post data for the given slug parameter and passes it as props to the BlogPost component.

Step 6: Configuring Tailwind CSS, Next config, and jsconfig

To ensure that our Tailwind CSS styles are applied correctly, we need to configure it in our Next.js project. Create a new file called tailwind.config.js in the root of your project and add the following code:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/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')],
};
 

This configuration file tells Tailwind CSS to scan our pages and components for classes to include in the final CSS output. We also add the @tailwindcss/typography and tailwindcss-markdown plugins to enable styling for Markdown content.

Next, open the globals.css file, delete everything and add the following lines:

@tailwind base;
@tailwind components;
@tailwind utilities;
 
@layer components  { 
	.prose img  { 
		margin-left: auto; 
		margin-right: auto; 
	} 
}
 

This imports the Tailwind CSS base styles, components, and utilities. We also add a custom style to center images in our Markdown content.

Next, let's configure ContentLayer in your next.config.js file, you simply need to import the withContentlayer function and wrap your Next.js configuration with it. Here's how you would do it:

Note: You will need to rename your next.config.mjs file to next.config.js

const { withContentlayer } = require('next-contentlayer');
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  pageExtensions: ['js', 'jsx', 'md', 'mdx'],
  //Other Next.js configuration options...
};
 
module.exports = withContentlayer(nextConfig);

The withContentlayer function from the next-contentlayer package is a higher-order function that enhances your Next.js configuration with the necessary settings to support ContentLayer.

By wrapping your nextConfig object with withContentlayer(nextConfig), you're seamlessly integrating ContentLayer into your Next.js application, allowing you to use Markdown and MDX files as content sources without any additional configuration.

Finally, lets add the contentlayer generated path to you jsconfig.json file:

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

Step 7: Adding Navigation and Styling

To make our blog site more user-friendly, let's add some navigation and apply some additional styling.

First, create a new folder and file called components/Layout.js in the src/app folder and add the following code:

import Link from 'next/link';
 
export default function Layout({ children }) {
  return (
    <div className="min-h-screen flex flex-col">
     <header className="bg-gray-900 text-white py-4">
       <nav className="container mx-auto flex justify-between items-center">
         <Link href="/">
           <a className="text-xl font-bold">My Blog</a>
         </Link>
         <ul className="flex space-x-4">
           <li>
             <Link href="/blog">
               <a>Blog</a>
             </Link>
           </li>
           <li>
             <Link href="/about">
               <a>About</a>
             </Link>
           </li>
         </ul>
       </nav>
     </header>
     <main className="flex-1">{children}</main>
     <footer className="bg-gray-900 text-white py-4 text-center">
       <p>&copy; {new Date().getFullYear()} My Blog. All rights reserved.</p>
     </footer>
   </div>
 );
}

This Layout component provides a basic structure for our site, including a header with navigation links, a main content area, and a footer. We use Tailwind CSS classes to style the layout.

Next, create a new file called _app.js in the src/app folder and add the following code:

import Layout from '../app/components/Layout';
import '../app/globals.css';
 
function MyApp({ Component, pageProps }) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  );
}
 
export default MyApp;

This file sets up the Layout component as the root component for our Next.js app, ensuring that it will be rendered on every page.

Finally, we will open the file called page.js in the app folder, delete everything and add the following code:

import Link from "next/link";
import { allBlogs } from "contentlayer/generated";
 
export default function Home({}) {
  const blogs = allBlogs.map((blog) => ({
    slug: blog.slug,
    title: blog.title,
    date: blog.date,
  }));
  return (
    <div className="flex justify-center items-center min-h-screen bg-[#1c062d]">
      <div className="max-w-3xl w-full bg-[#f2e3ff] p-8 rounded-lg">
        <h1 className="text-3xl font-bold mb-8 text-center">
          Welcome to My Blog
        </h1>
        <div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
          {blogs.map((blog) => (
            <div
              key={blog.slug}
              className="bg-white rounded-lg shadow-md overflow-hidden"
            >
              <div className="p-4">
                <h2 className="text-xl font-bold mb-2">{blog.title}</h2>
                <p className="text-gray-600 mb-4">{blog.date}</p>
                <Link
                  key={blog.slug}
                  href={`${blog.slug}`}
                  className="text-blue-500 hover:text-blue-700"
                >
                  Read more
                </Link>
              </div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

This Home component displays a list of all our blog posts on the homepage. We use the allBlogs array from ContentLayer to get the data for each blog post and render it in a grid layout using Tailwind CSS classes.

This is what your folder should look similar to this at this point

├── .next/
├── node_modules/
├── public/
│   ├── favicon.ico
│   └── ...
├── src/
│   ├── app/
│   │   ├── blog/
│   │   │   ├── [slug]/
│   │   │   │   └── page.js
│   │   ├── globals.css
│   │   ├── page.js
│   │   ├── _app.js
│   │   ├── components
│   │   |    └── Layout.js
├─- content/
│  ├── blog/
│   │   ├── my-first-post.mdx
├── contentlayer.config.js
├── .eslintrc.js
├── .gitignore
├── next.config.js
├── package.json
├── package-lock.json
├── postcss.config.js
├── tailwind.config.js
└── ...

Run your project using

npm run dev

We're pretty much done!

Congratulations! You've successfully built a fully-fledged blog site using Next.js 14, ContentLayer, and Tailwind CSS. You've learned how to set up the project, configure ContentLayer, work with Markdown content, render blog posts, style your site with Tailwind CSS, and deploy your site to Vercel.

This is just the beginning, though. You can further enhance your blog site by adding features like commenting, author profiles, categories, and more. The beauty of using Next.js, ContentLayer, and Tailwind CSS is that they provide a solid foundation for building complex and scalable web applications.

Happy coding, and keep exploring the world of web development!