Use Contentlayer with NextJS

Dec 23, 2021 Edit in Github

I don’t write a lot (though is something I want to change), but this site is some kind of playground to try new things. The other day stumbled upon a list of personal websites of developers and designers and going through them found some using Contentlayer, it looked interesting, so let’s try it!

Contentlayer turns your content into data - making it super easy to import MD(X) and CMS content in your app.

The first step is to install the needed libraries:

yarn add contentlayer next-contentlayer

Once the installation is complete, a contentlayer.config.ts file needs to be created in the root folder of your project. This is the file where all the content definition and project configuration are done.

In my case I only have one type of content, blog posts coming from MDX files.

Use defineDocumentType for each type of content type you want Contentlayer to manage. Using the fields property we map frontmatter fields from the document to object properties you can freely use (and to typescript types!).

import { defineDocumentType } from 'contentlayer/source-files';

const Post = defineDocumentType(() => ({
	name: 'Post',
	filePathPattern: 'posts/*.mdx',
	bodyType: 'mdx',
	fields: {
		title: { type: 'string', required: true },
		date: { type: 'date', required: true },
		tags: { type: 'list', of: { type: 'string' } },
		image: { type: 'string' },
		excerpt: { type: 'string' },
	},
	computedFields,
}));

You see that computedFields property?

This are some kind of virtual fields that can go through and extra process, for example to get the slug from the file name, or to get some extra metadata like reading time.

import type { ComputedFields } from 'contentlayer/source-files';
import readingTime from 'reading-time';

const computedFields: ComputedFields = {
	slug: {
		type: 'string',
		resolve: (doc) => doc._raw.sourceFileName.replace(/\.mdx$/, ''),
	},
	readingTime: { type: 'json', resolve: (doc) => readingTime(doc.body.raw) },
};

Finally we complete the configuration using makeSource. We need to add the content types defined above and we have the option to set extra configuration for MDX files, like using remark and rehype plugins.

import { makeSource } from 'contentlayer/source-files';
import { rehypeAccessibleEmojis } from 'rehype-accessible-emojis';
import slug from 'rehype-slug';
import remarkGfm from 'remark-gfm';

const contentLayerConfig = makeSource(async () => {
	return {
		contentDirPath: 'content',
		documentTypes: [Post],
		mdx: {
			remarkPlugins: [remarkGfm],
			rehypePlugins: [slug, rehypeAccessibleEmojis],
		},
	};
});

export default contentLayerConfig;

With this we are almost done with the configuration, but since we are using NextJS we can hook up to its build process to autogenerate the content and enable live-reload. To do this we need to add some changes on next.config.js. In my case I’m using next-compose-plugins so it looks like this:

const withPlugins = require('next-compose-plugins');
const { createContentlayerPlugin } = require('next-contentlayer');

const withContentlayer = createContentlayerPlugin({
	// Additional Contentlayer config options
});

const nextConfig = {
	// extra next config
};

module.exports = withPlugins([withContentlayer], nextConfig);

With everything in place, running yarn dev (or yarn build for production) will trigger Contentlayer build process, which generates several files inside the node_modules/contentlayer/generatedfolder which then you can import wherever you want to use them.

For my content I defined a content type called Post, so for example to look for the single post when a slug is accessed:

import { allPosts } from 'contentlayer/generated';

export const getStaticProps = async ({ params }) => {
	const post = allPosts.find(
		(post) => post.slug === (params?.slug as string),
	);

	return {
		props: {
			post,
		},
	};
};

And since I’m using MDX we need the useMDXComponent hook to render the content:

import { useMDXComponent } from 'next-contentlayer/hooks';
import type { Post } from 'contentlayer/generated'; // Typescript type too!

const SinglePost = ({ post }: { post: Post }) => {
	const Component = useMDXComponent(post.body.code);

	return (
		<article>
			<Component />
		</article>
	);
};

That’s it! Our Post object has access to all the properties we defined as fields and computedFields plus some extra (like the above body.code for MDX files).

You can see the complete changeset for this here: https://github.com/osiux/osiux.ws/pull/5/files

Only major issue was that I was using a remark plugin (to embed media content) that is using an old unified version, while Contentlayer uses latest version and it was causing some kind of incompatibility.

The process to implement Contentlayer was pretty straightforward, currently the library lacks some documentation, but given its alpha state it’s understandable, and looking at websites already using it made it easier to use.