ContentLayer : the Tool That Makes NextJS Markdown Glow Up

NextJS is a great tool for creating websites. However, just like React used to be with state management, Next JS doesn’t care how you provide content to it. And there are many different possibilities. There are loads of Content Management Systems (or CMSs) that have been created to solve that very problem.

But if you’re a solo developer or a small team, you don’t need to pay for an external service. There is a better solution, and that solution is Markdown.

Allow me to explain:

  1. Why I use Markdown with NextJS (and why it might be a good idea for you too if you use NextJS)
  2. What problems I encountered when working with Markdown
  3. And how I use a tool called ContentLayer to solve these problems Let’s dive in

Why I use Markdown

First, why do I use Markdown when creating content for NextJS? I’ve used markdown whenever I can, both for my personal websites and work.

And there are many reasons why I favour markdown Why? Because Markdown gives you freedom that other solutions don’t.

It’s free. You don’t need any additional tools. It’s a simple text file. This means you can** edit it in any text editor**, even on GitHub itself. You don’t need to log in to anywhere. This means there isn’t any vendor lock-in. The fact that it is a text file means you can track your content in Git. And because Markdown is a recognised format, it’s easy to import and export. If need be, You can start with Markdown and move to something else later if you grow out of it.

You can add any additional data using “front matter”, which is YAML formatted data at the top of the file, between two lines of three dashes. Using front matter allows me to define specific data for my blog posts, portfolio items, pages, and more. In fact, for any content. For example, for portfolio items, my frontmatter looks like this. You can see I’ve defined some strings like the title, the path to the image, the prompt and more. I’ve also defined some numbers (namely, here, width, and height) and a date

title: Forest Morning
slug: forest-morning
image: /images/portfolio/forest-morning.jpg
prompt: a dslr photography of a sunrise in a lush autumn forest, ethereal,cinematic tones, dramatic lighting, field of view --style 1f4tF7LChmCv2MWh --ar 16:9
width: 1456
height: 816
date: 2023-11-12
type: Portfolio

And for blog posts my front matter looks like this.

title: "Angular vs React: which should you choose?"
slug: angular-vs-react
featuredImage: /images/blog/react_vs_angular.jpg
date: 2021-05-01T00:00:00.000Z
subtitle: Philosophical differences expressed visually
category: Frameworks
  - angular
  - react
lang: en
  - fr: angular-vs-react
type: Post

As you can see, some fields like title, slug and date are shared. Others, like the prompt, the width, or the category, are specific to the given type of content. As you might also be able to read, YAML allows us to create more complex data types. Here tags is a list of strings, and the alternate links (the alt key) is a list of key-value objects.

What is the problem with Markdown?

However, with great flexibility comes great responsibility. To phrase things differently, this is also Markdown’s biggest problem.

For example, I realised that in some places, I had written “tag” instead of “tags”. In other places, I’d used image instead of featuredImage. And that content is being fed into my code, so it expects the fields to be named in a certain way. With Markdown, I don’t have any way of enforcing the shape of the data stored in the front-matter. And it’s a shame because I’m a fan

What’s more, retrieving content is a chore. Once I set it up for the first time, I copy-pasted what I’d done between projects. To use markdown, you need the filesystem library, fs. You use it to list all the files, load them and parse them. However, if there are two things I’ve learnt as a developer, it’s that :

  • reading and writing files at runtime is to be avoided as much as possible, and
  • copy-pasting code is bad.

On all these counts, the way I was using Markdown was leaving a bit of a sour taste in my mouth.

The solution I use

First, I tried pre-parsing all the files at build-time. Then, I created a JSON with all the content. That allowed me to load the content catalogue as an import. However, I still had incoherent types, and the setup contained lots of custom code. So I started wondering about coding checks for the shape of the data in my front matter.

However, around the same time, I was looking at the work of ShadCN. ShadCN created the UI library with the same name, which I’m looking into. And he’s also working on a GitHub repository called Taxonomy. Taxonomy has a lot of interesting code if you’re into NextJS. Stuff like database integration and Stripe integration.

And in the Taxonomy repo, he mentioned he was using a library called ContentLayer, so I looked into it to see what it was all about. And I very much liked what I saw. So much so that I swapped out all my markdown management code this weekend and replaced it with ContentLayer.

Why? (I mean, other than the fact that I’m a sucker for punishment).

Well, ContentLayer does everything I was trying to code for myself.

You start by defining a schema for your content. For example, my schema for Portfolio items looks like this. As you can see, the name here matches the type in my content, and I define the path to where my content is stored. And then I also define the different fields, their type, and whether they are required. For example, title, slug, and image are required strings, width and height are required numbers, and prompt is an optional string.

export const Portfolio = defineDocumentType(() => ({
	name: 'Portfolio',
	filePathPattern: portfolio/**/*.md,
	fields: {
	title: { type: 'string', required: true },
	slug: { type: 'string', required: true },
	date: { type: 'date', required: true },
	image: { type: 'string', required: true },
	width: { type: 'number', required: true },
	height: { type: 'number', required: true },
	prompt: { type: 'string', required: false },

I tell ContentLayer what kinds of content I have and where it is by calling the makeSource function:

export default makeSource({
contentDirPath: 'src/content',
documentTypes: [Post, Product, Person, Portfolio]

Using all the information I provided to it, ContentLayer then reads the files that match the paths I’ve provided. It checks that:

  • the front matter fits the schema I’ve defined,
  • that there aren’t any additional fields,
  • that all the required fields are present
  • and that the fields all have the right data in them.

Using the schema, ContentLayer then defines a TypeScript type I can use in my code and an accessor that provides me with all the content of that type.

That means that all my complicated file system manipulations using fs and my front matter parsing is now replaced with a simple :

import { allPortfolios, Portfolio } from 'contentlayer/generated';

I spent the weekend implementing it, but do you know what took so long? Truth be told, cleaning up my code was quick and easy. What took time was going through all the problems ContentLayer kept finding in my content. So now, not only is my code a lot cleaner, but my content is a lot cleaner, too.

Okay, but now I’ve cleaned up my content, how do I track it and keep it organised? Well, I’m using Notion and connecting to their API.

Made by kodaps · All rights reserved.
© 2023