Build a Simple Blog with Contentlayer, Next.js and Tailwind CSS

Davis Gitonga

Davis Gitonga / July 22, 2022

9 min read

Build a Simple Blog with Contentlayer, Next.js and Tailwind CSS

What is Contentlayer?

Contentlayer is a content SDK that validates and transforms your Markdown, MD(X) and CMS content into type-safe JSON data you can easily import into your application.

⚠️

Contentlayer is still in Beta. There might still be breaking changes before the upcoming version 1.0 release!

It includes the following features & benefits:

  • Live reloading on content changes
  • Blazing fast build & page performance
  • Simple but powerful schema DSL (Domain Specific Language) to design your content model (validates your content and generates types)
  • Auto-generated TypeScript types based on your content model (e.g. frontmatter or CMS schema)
  • Lightweight & easy to use
  • Great developer experience

Automatic Installation & Configuration

The easiest way to configure a new Next.js project with Contentlayer is by using a starter project from the official Next.js examples directory.

npx create-next-app -e with-contentlayer contentlayer-app
# or
yarn create next-app -e with-contentlayer contentlayer-app

After the installation is complete:

  • Navigate into the project folder cd contentlayer-app
  • Run npm run dev or yarn dev to start the next dev server on localhost:3000.
  • Visit localhost:3000 on your browser to view your application

Manual Installation & Configuration

Let's start with a blank Next.js project with TypeScript. Contentlayer works better with TypeScript as recommended by the development team. We'll use that in our example below.

New Next.js with TypeScript App

Open your terminal and run:

npx create-next-app -e with-typescript contentlayer-example
# or
yarn create next-app -e with-typescript contentlayer-example

That command will place the project in a contentlayer-example directory. Change into that directory.

cd contentlayer-example

Add Tailwind CSS

If you'd like to add some styling as we go without much extra effort, follow these instructions to add Tailwind to your project.

Install Contentlayer

Using Contentlayer in a Next.js project is easiest if you use the next-contentlayer plugin. Install Contentlayer and the Next.js plugin:

npm install contentlayer next-contentlayer
# or
yarn add contentlayer next-contentlayer

Then wrap your next configuration object in the withContentlayer utility. This hooks Contentlayer into the next dev and next build processes.

Create a new file next.config.js in the root of your project, and add the following code.

next.config.js
const { withContentlayer } = require('next-contentlayer')

module.exports = withContentlayer({})

Then add the following lines to your tsconfig.json file:

tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "contentlayer/generated": ["./.contentlayer/generated"]
    }
  },
  "include": ["next-env.d.ts", "**/*.tsx", "**/*.ts", ".contentlayer/generated"]
}

Contentlayer generates files in the .contentlayer/generated directory. This tells TypeScript to create a module alias of contentlayer/generated to the generated files directory.

Simple Blog Site

Define Document Schema

With all the tools we need installed, we can now define the document schema. A document is an individual piece of content that Contentlayer transforms into data you can use in your components.

Because we're building a simple blog site, let's define a single document type called Post. Create a file contentlayer.config.ts in the root of your project, and add the following code.

contentlayer.config.ts
import { defineDocumentType, makeSource } from 'contentlayer/source-files'

export const Post = defineDocumentType(() => ({
  name: 'Post',
  filePathPattern: `**/*.md`,
  fields: {
    title: {
      type: 'string',
      description: 'The title of the post',
      required: true,
    },
    summary: {
      type: 'string',
      description: 'The summary of the post',
      required: true,
    },
    image: {
      type: 'string',
      description: 'The banner image of the post',
      required: true,
    },
    date: {
      type: 'date',
      description: 'The date of the post',
      required: true,
    },
  },
  computedFields: {
    slug: {
      type: 'string',
      resolve: (post) => `/posts/${post._raw.flattenedPath}`,
    },
  },
}))

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

The above config specifies a single document type called Post. These documents are expected to be markdown .md files that live within a posts directory in the root of your project. The data objects generated from these files will have the following properties:

  • title: String pulled from the file's frontmatter.
  • summary: String pulled from the file's frontmatter.
  • image: String pulled from the file's frontmatter.
  • date: JavaScript Date object, pulled from the file's frontmatter.
  • body: An object that contains the raw content from the markdown file and the converted html string. (This is built into Contentlayer by default and does not have to be defined.)
  • url: A string that takes the name of the file (without the extension) and prepends /posts/ to it, thus defining the path at which that content will be available on your site.

Add Post Content

Create a few markdown .md files in the posts directory and add some placeholder content. Below is an example of how one might look like:

posts/post-one.md
---
title: Blog Post One Title
summary: Lorem ipsum dolor sit amet, consectetur adipiscing elit.
image: post-one.jpg
date: 2021-12-24
---

Nullam vestibulum cursus laoreet. Morbi neque lorem, molestie a nibh nec,
egestas hendrerit ex. Phasellus tincidunt sapien ac libero ultricies
sollicitudin. Curabitur vitae ex et ligula ornare faucibus id ac ex.
Ut ac aliquam tortor. Duis posuere posuere arcu sit amet gravida. Nullam
augue urna, ornare vel pulvinar in, lobortis quis est. Donec dolor neque,
pellentesque nec augue sed, varius porta neque.

Mollit nisi cillum exercitation minim officia velit laborum non Lorem
adipisicing dolore. Labore commodo consectetur commodo velit adipisicing
dolore dolor reprehenderit aliquip. Reprehenderit cillum mollit eiusmod
excepteur elit ipsum aute pariatur in. Cupidatat ex culpa velit culpa ad non
labore exercitation irure laborum.

Add blog Feed

Now we bring it all together by displaying the data into our pages.

Date Formatting Helper

Add the date-fns library to help us with formating the date.

npm install date-fns
# or
yarn add date-fns

Update Home Page

Update the default home page pages/index.tsx markup with a list of all the posts and links to the individual post pages.

pages/index.tsx
import Link from 'next/link'
import Head from 'next/head'
import { compareDesc, format, parseISO } from 'date-fns'
import { allPosts } from 'contentlayer/generated'

export async function getStaticProps() {
  const posts = allPosts.sort((a, b) => {
    return compareDesc(new Date(a.date), new Date(b.date))
  })
  return { props: { posts } }
}

function PostCard(post) {
  return (
    <div className="mb-6 bg-gray-100 p-4 shadow hover:shadow-lg rounded-xl">
      <h2 className="text-xl font-bold mb-2">
        <Link href={post.slug}>
          <a className="text-blue-700 hover:text-blue-900">{post.title}</a>
        </Link>
      </h2>
      <time dateTime={post.date} className="block text-sm text-slate-600">
        {format(parseISO(post.date), 'LLLL d, yyyy')}
      </time>
      <p className="text-base text-gray-700">{post.summary}</p>
    </div>
  )
}

export default function Home({posts}) {
  return (
    <div className="mx-auto max-w-2xl py-16">
      <Head>
        <title>Contentlayer Blog Example</title>
      </Head>

      <h1 className="mb-8 text-3xl font-bold">Contentlayer Blog Example</h1>

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

Notice that the data was already available to us as allPosts, coming from contentlayer/generated. We used allPosts to sort the posts using the date property, and then sent the posts to the home page as props.

The home page then used the post data to map through the individual posts and render PostCard components. As your site grows, you'll want to break these components out into their own files. We're showing everything in the same file here to keep things simple.

Preview the App

Fire up the Next.js dev server to preview your app.

npm run dev
# or
yarn dev

Visit localhost:3000 on your browser. You should see a list of the posts you added to the posts directory!

Example blog localhost preview on the browser.

Add Post Layout

Notice that if you click on individual posts, you get a 404 error. That's because we haven't created the pages for these posts. Let's do that!

We will use Next.js dynamic routes to achieve that.

Create the page at pages/posts/[slug].tsx and add the following code.

pages/posts/[slug].tsx
import Head from 'next/head'
import Link from 'next/link'
import { format, parseISO } from 'date-fns'
import { allPosts } from 'contentlayer/generated'

export async function getStaticPaths() {
  const paths = allPosts.map((post) => post.url)
  return {
    paths,
    fallback: false,
  }
}

export async function getStaticProps({ params }) {
  const post = allPosts.find((post) => post._raw.flattenedPath === params.slug)
  return {
    props: {
      post,
    },
  }
}

const PostLayout = ({ post }) => {
  return (
    <>
      <Head>
        <title>{post.title}</title>
      </Head>
      <article className="mx-auto max-w-2xl py-16">
        <div className="mb-6 text-center">
          <Link href="/">
            <a className="text-center text-sm font-bold uppercase text-blue-700">Home</a>
          </Link>
        </div>
        <div className="mb-6 text-center">
          <h1 className="mb-1 text-3xl font-bold">{post.title}</h1>
          <time dateTime={post.date} className="text-sm text-slate-600">
            {format(parseISO(post.date), 'LLLL d, yyyy')}
          </time>
        </div>
        <div className="cl-post-body" dangerouslySetInnerHTML={{ __html: post.body.html }} />
      </article>
    </>
  )
}

export default PostLayout

Notice again that we're importing data from contentlayer/generated. This is the beauty of Contentlayer. It has already loaded and shaped our data objects and keeps the logic in getStaticPaths() and getStaticProps() nice, clean and simple.

Now clicking on a post link from the home page should lead you to a working post page.

Blog post layout browser preview.

Deploying to Vercel

Our simple blog is now showing a list of posts, fetched from your local disk, and displayed nicely using Tailwind CSS. Let's deploy our Next.js application to Vercel:

  1. Push your code to a git repository (e.g. GitHub, GitLab, BitBucket)
  2. Import your Next.js project into Vercel
  3. Click "Deploy"

Vercel will auto-detect you are using Next.js and enable the correct settings for your deployment. Finally, your application is deployed at a URL like contentlayer.vercel.app.

Conclusion

You now have a simple blog site with Contentlayer and Next.js which can be customized to fit your specific needs.

Resources

Thanks for reading! If this tutorial was helpful, share your feedback in the comments section below.

Happy Coding!🤓

share your thoughts