Table of contents
I have another article that I shown how we can turned md files into data with features of webpack5 in the Nextjs. In this Article, I want to work with contentLayer library for turn mdx files into data.
This article is my experience in 6th day of 100DaysOfCode.
Updated 9 May 2022 | compatible with version 0.2
What's MDX?
MDX allows you to use JSX in your markdown content.
Why contentLayer?
One of reason that I'm using contentLayer is that it supported content sources like:
- Local content (Markdown, MDX, JSON, YAML)
- Contentful
- Sanity (experimental)
you can read other contentLayer features here!
Installation:
1- First thing first, we need install it:
npm install contentlayer next-contentlayer
Open your next.config.js in the root of your project, and add the following content.
import { withContentlayer } from 'next-contentlayer'
export default withContentlayer({})
If you use multiple plugins like me, then you can do the following
const withPlugins = require('next-compose-plugins');
const { withContentlayer } = require('next-contentlayer');
const withTwin = require('./next-twin.js');
const nextConfig = {
reactStrictMode: true,
images: {
dangerouslyAllowSVG: true,
contentSecurityPolicy:
"default-src 'self'; script-src 'none'; sandbox;",
domains: ['pexels.com'],
},
};
const contentLayer = withContentlayer({
nextConfig,
});
const twin = withTwin(nextConfig);
module.exports = withPlugins([contentLayer, twin], nextConfig);
2- create contentlayer.config.ts
file in the root of your project:
touch contentlayer.config.ts
3- open contentlayer.config.ts
and import defineDocumentType and makeSource functions from 'contentlayer/source-files'.
import { defineDocumentType, makeSource } from 'contentlayer/source-files'
Configuration:
Define Document Type
We need to define document type in contentLayer configuration. By this way we declare:
- Content Type (Are files mdx, JSON or md ?),
- Yaml front matter fields,
- File Path
- and rest data that you need to compute for your document ( like reading time )
For example, this is a document type in my project:
export const Blog = defineDocumentType(() => ({
name: 'Blog',
contentType: 'mdx',
filePathPattern: `blogs/**/*.mdx`,
fields: {
title: { type: 'string', required: true },
publishedAt: { type: 'string', required: true },
description: { type: 'string', required: true },
cover: { type: 'string' },
tag: { type: 'string' },
},
computedFields: {
readingTime: {
type: 'json',
resolve: doc => readingTime(doc.body.raw),
},
slug: {
type: 'string',
resolve: doc => doc._raw.sourceFileName.replace(/\.mdx$/, ''),
},
},
}));
Properties in DocumentType:
name
you maybe have several document type like articles, notes, blog and etc. so you need to decalare a name for them.
'name' property will be name of your document type. The below image shows document type 'Blog' after generating
contentType
what's your content type? JSON, MD, MDX? By this property you can set your content type. This property is not exsit in before version 0.1.0. it was
bodyType
instead ofcontentType
.
filePathPattern
Where are your files? you can write down your pattern here, don't need consider the parent of directory. for example if your files located on
_posts/blog/hello-world.mdx
you don't need consider_posts
here. we can declare the parent directory incontentDirPath
in themakeSource
function.As you can see my mdx files located under blogs. so I've used this pattern:
fields
I like yaml because it is a human-friendly data serialization language for all programming languages. I usually used yaml for maintaining metadata in blog posts. since, the content type of my blog posts is
mdx
and MDX dosen't support yaml by default, the contentLayer has been resolved this issue for us. just we need declare our data in `fields' property.computedFields
Do you want add some cool feature to your data? or Do you want calculate some thing before generating data? this property helps you that implement your requirement.
In this case, we add slug and reading time to data:
for reading time, we need install the package and import it to
contentlayer.config.ts
npm i reading-time
NOTE:
You can define several document type in contentLayer, just copy your document type with unique name and edit it.
Make Source
It's time that we provide the documents to generating. The function makeSource
will helps to make it. we already imported it in contentlayer.config.ts
. this function takes an object with several property that we need to fill them.
- contentDirPath
- documentTypes
- mdx
Properties in MakeSource
contentDirPath
We add the parent of posts directory into
contentDirPath
property:Also, After version 0.2.1 you can explicitly exclude or include paths within the contentDirPath. This is especially useful when using your project root (.) to target content. for example:
export default makeSource({ contentDirPath: '.', contentDirInclude: ['blogs', 'config'], documentTypes: [Blog, Customize], mdx: mdxOptions, });
documentTypes
We add our doucmetTypes that already we defined them in
documentTypes
property. Just we need push their name to documentTypes array, if we have several document types. In this case we have one document type.mdx
The MDX has several plugins that helps to make awesome features to our blog. like
remark
andrehype
. Also these plugins have their plugins ๐ง . for example forremark
we haveremarkGfm
and forrehype
we haverehype-highlight
.Anyway the mdx property has specific properties for
remarkPlugins
andrehypePlugins
that we can add the plugins to them.
Great, So far we provide our configuration for generating blog posts.
// contentlayer.config.ts
import { defineDocumentType, makeSource } from 'contentlayer/source-files';
import mdxOptions from './config/mdx';
import readingTime from 'reading-time';
export const Blog = defineDocumentType(() => ({
name: 'Blog',
contentType: 'mdx',
filePathPattern: `blogs/**/*.mdx`,
fields: {
title: { type: 'string', required: true },
publishedAt: { type: 'string', required: true },
description: { type: 'string', required: true },
cover: { type: 'string' },
tag: { type: 'json' },
},
computedFields: {
readingTime: {
type: 'json',
resolve: doc => readingTime(doc.body.raw),
},
slug: {
type: 'string',
resolve: doc => doc._raw.sourceFileName.replace(/\.mdx$/, ''),
},
},
}));
export default makeSource({
contentDirPath: '_posts',
documentTypes: [Blog],
mdx: mdxOptions,
});
and mdxOptions is:
// config/md.ts
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeCodeTitles from 'rehype-code-titles';
import remarkExternalLinks from 'remark-external-links';
import rehypeImgSize from 'rehype-img-size';
import { Pluggable } from 'unified';
import rehypePrism from 'rehype-prism-plus';
import rehypeSlug from 'rehype-slug';
import remarkGfm from 'remark-gfm';
const mdxOptions = {
remarkPlugins: [remarkExternalLinks, remarkGfm],
rehypePlugins: [
rehypeSlug,
rehypeCodeTitles,
rehypePrism,
rehypeAutolinkHeadings,
[
rehypeImgSize,
{
dir: 'public',
},
],
] as Pluggable[],
compilers: [],
};
export default mdxOptions;
TypeScript Config (tsconfig.js)
One of the best option in typescript config is paths. This option lets you declare how TypeScript should resolve an import in your require/imports.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"contentlayer/generated": ["./.contentlayer/generated"]
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
}
},
"include": ["next-env.d.ts", "**/*.tsx", "**/*.ts", ".contentlayer/generated"]
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^
}
How the contentLayer provides the data?
when we start our server, the contentLayer will Generate documents in .contentlayer directory:
For this case, we have this tree structure after npm run dev
.contentlayer
โโโ generated
โ โโโ Blog
โ โ โโโ blogs__javascripts__first-blog.mdx.json
โ โ โโโ blogs__javascripts__my-blog.mdx.json
โ โ โโโ blogs__javascripts__my.mdx.json
โ โ โโโ blogs__javascripts__welcome.mdx.json
โ โ โโโ blogs__my.mdx.json
โ โโโ allBlogs.mjs
โ โโโ index.d.ts
โ โโโ index.mjs
โ โโโ types.d.ts
โโโ package.json
The contentLayer converted the blog posts to json data that we can use them easly. for example for my-blog.mdx, we have this data:
{
"title": "My blog",
"publishedAt": "2-4-2022",
"description": "You can see the my blog here",
"cover": "",
"tag": [
"people",
"music"
],
"body": {
"raw": "\n# My blog\n",
"code": "var Component=(()=>{var p=Object.create;var r=Object.defineProperty;var d=Object.getOwnPropertyDescriptor;var h=Object.getOwnPropertyNames;var u=Object.getPrototypeOf,x=Object.prototype.hasOwnProperty;var i=e=>r(e,\"__esModule\",{value:!0});var b=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),g=(e,t)=>{for(var n in t)r(e,n,{get:t[n],enumerable:!0})},c=(e,t,n,s)=>{if(t&&typeof t==\"object\"||typeof t==\"function\")for(let a of h(t))!x.call(e,a)&&(n||a!==\"default\")&&r(e,a,{get:()=>t[a],enumerable:!(s=d(t,a))||s.enumerable});return e},j=(e,t)=>c(i(r(e!=null?p(u(e)):{},\"default\",!t&&e&&e.__esModule?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e),f=(e=>(t,n)=>e&&e.get(t)||(n=c(i({}),t,1),e&&e.set(t,n),n))(typeof WeakMap!=\"undefined\"?new WeakMap:0);var m=b((O,l)=>{l.exports=_jsx_runtime});var C={};g(C,{default:()=>M,frontmatter:()=>y});var o=j(m()),y={title:\"My blog\",description:\"You can see the my blog here\",publishedAt:\"2-4-2022\",tag:[\"people\",\"music\"],cover:\"\"};function _(e={}){let{wrapper:t}=e.components||{};return t?(0,o.jsx)(t,Object.assign({},e,{children:(0,o.jsx)(n,{})})):n();function n(){let s=Object.assign({h1:\"h1\",a:\"a\",span:\"span\"},e.components);return(0,o.jsxs)(s.h1,{id:\"my-blog\",children:[(0,o.jsx)(s.a,{\"aria-hidden\":\"true\",tabIndex:\"-1\",href:\"#my-blog\",children:(0,o.jsx)(s.span,{className:\"icon icon-link\"})}),\"My blog\"]})}}var M=_;return f(C);})();\n;return Component;"
},
"_id": "blogs/javascripts/my-blog.mdx",
"_raw": {
"sourceFilePath": "blogs/javascripts/my-blog.mdx",
"sourceFileName": "my-blog.mdx",
"sourceFileDir": "blogs/javascripts",
"contentType": "mdx",
"flattenedPath": "blogs/javascripts/my-blog"
},
"type": "Blog",
"readingTime": {
"text": "1 min read",
"minutes": 0.015,
"time": 900,
"words": 3
},
"slug": "my-blog"
}
Since, our document type is .mdx
, in body
property we have two keys : raw
and code
that we can pass code
to MDXContent function in NextJs. we will have html
instead code
if our document type is .md
.
How to import the "Blog" data to files and use it?
All blogs and Type of Blog are in the .contentlayer/generated
in root of your project directory. Since, we have set paths
option in tsconfig.js, we can import data easily:
import { allBlogs } from 'contentlayer/generated';
import type { Blog } from 'contentlayer/generated';
then you can use allBlogs
as an array in the function getStaticProps
:
// pages/blog/[slug].tsx
import BlogLayout from '@layouts/blog';
import { useMDXComponent } from 'next-contentlayer/hooks';
import { allBlogs } from 'contentlayer/generated';
import type { Blog } from 'contentlayer/generated';
type BlogProps = {
blog: Blog;
};
export default function Blog({ blog }: BlogProps) {
const Component = useMDXComponent(blog.body.code);
return (
<BlogLayout blog={blog}>
<Component />
</BlogLayout>
);
}
export const getStaticPaths = async () => {
return {
paths: allBlogs.map(p => ({ params: { slug: p.slug } })),
fallback: false,
};
};
export const getStaticProps = async ({ params }) => {
const blog = allBlogs.find(p => p.slug === params.slug);
return {
props: {
blog,
},
};
};
Reference
Thank you for reading!
Thank you for reading my blog. Feel free to subscribe to my weekly newsletter.