Use Contentlayer with NextJS
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!
What is Contentlayer?
Contentlayer turns your content into data - making it super easy to import MD(X) and CMS content in your app.
How to add it to your NextJS site?
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);
How to use it?
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/generated
folder 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
Caveats
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.
Conclusion
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.