Build a Simple Blog with Contentlayer, Next.js and Tailwind CSS
Davis Gitonga / July 22, 2022
9 min read •
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
oryarn dev
to start the next dev server onlocalhost: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.
const { withContentlayer } = require('next-contentlayer')
module.exports = withContentlayer({})
Then add the following lines to your tsconfig.json
file:
{
"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.
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
: JavaScriptDate
object, pulled from the file's frontmatter.body
: An object that contains theraw
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:
---
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.
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!
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.
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.
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:
- Push your code to a git repository (e.g. GitHub, GitLab, BitBucket)
- Import your Next.js project into Vercel
- 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
- Learn more about how Contentlayer works.
- Contentlayer starter project from the official Next.js examples directory.
- Full Source Code from the example above.
Thanks for reading! If this tutorial was helpful, share your feedback in the comments section below.
Happy Coding!🤓