Next.js + Content Layer Starter

This minimal starter template sets up a typesafe Next.js project (using the App Router) that uses ContentLayer to support generating static pages from Markdown (.mdx) files. This combination is great for generating pages for blog posts, documentation, articles, and more. In fact, this page you are currently reading was generated using ContentLayer!

Getting Started

To get started with this starter template, run the following command:

npx create-next-app@latest --example <url>

This will set up your Next.js project using this Next.js + Content Layer Starter as a template.

Once your project is set up, make sure to install all dependencies:

npm install

To run the application locally, you will still use:

npm run dev

How it Works

Content Types

All ContentLayer content can be categorized into content types (aka document types), which specifies the properties of certain types of documents. You may want to generate multiple types of static pages based on different types of content. For example, your site might include static Pages, while BlogPosts may be considered a different category.

To make this easy to both identify and organize, all content types should be defined in the /content/config/types.ts directory. Here is an example of the Page content type which has already been defined, used to represent static website pages:

export const PageContent = defineContentType({
  name: 'Page',
  filePathPattern: `pages/**/*.mdx`,
  contentType: 'mdx',
  fields: {
    title: {
      type: 'string',
      required: true
    }
  }
});

There are a few important sections to note here, including:

  • name: Specifies the name of the content type.
  • filePathPattern: Relative path to identify the markdown content files (in the format .mdx) that should be used to generate content of this type. Based on the file path pattern provided, a page will be created for all .mdx files nested in the directory /content/pages. For example, if the following was the file structure:
    content/pages
    |- index.mdx
    |- products
    |   |- mocha.mdx
    |   |- pumpkin-spice.mdx
    ContentLayer will generate pages for index.mdx, /products/mocha.mdx, and /products/pumpkin-spice.mdx respectively.
  • fields: Specifies the frontmatter fields which are expected to be included with each content page - more on this later. To learn more about how to configure fields, check out this page.

This file also contains two exports: allContentTypes and Content. It is important that you update these exports as you add more content types to your site. For an example of an implementation with many different content types, check out this website. This is a course website for COMP 426: Modern Web Programming at UNC-Chapel Hill, and this starter was based on lessons learned from building this website.

Writing Content

Now that you have set up your content types, you will begin writing content. This template utilizes MDX, which is a superset of traditional Markdown allowing developers to interweave in JSX-based components.

To create content for my Page content type, I will create a .mdx markdown file like so:

---
title: "Next.js + Content Layer Starter"
---
 
Welcome to the Next.js + Content Layer Starter home page!

Notice the top section between the --- marks - this is where you will provide data for your required fields. When ContentLayer generates your content, these fields are accessible from within your Next.js project code.

For an example of a fully written content page from the COMP 426 course website, check out this syllabus page and the corresponding .mdx file that generates it.

Converting Markdown to HTML

This template also provides fine-grained control into how each type of Markdown element is translated into HTML, which can be found in the /components/mdx/mdx-components.tsx file. For example:

const components: MDXComponents = {
  h1: ({ className, ...props }) => (
    <h1
      className={cn(
        'mt-2 scroll-m-20 text-4xl font-bold tracking-tight',
        className
      )}
      {...props}
    />
  ),
  ...
}

This is an example of the mapping from a header, defined in markdown using the syntax # My header, to an <h1> element. This is entirely customizable, so feel free to adjust styling as you prefer. The chosen styling here was based on the Taxonomy project by Shadcn and inspired by Shadcn UI design principles.

This file also exposes a <Mdx> component which accepts generated content from ContentLayer and renders it as HTML.

Using React Components in MDX

One of the best features of MDX is that React components may be used within Markdown. Below is an example of a callout component created in React:

Hello from within the callout!

This was generated using the following .mdx:

---
title: "Next.js + Content Layer Starter"
---
 
# My Sample header
 
Welcome to the **Next.js + Content Layer Starter home page**!
 
<Callout type="green">
    Hello from within the callout!
</Callout>
 
More content here.

Important:
You must manually add components you want to use in MDX into the MDXComponents object above, like so:

const components: MDXComponents = {
    h1: ({ className, ...props }) => (
        <h1
        className={cn(
            'mt-2 scroll-m-20 text-4xl font-bold tracking-tight',
            className
        )}
        {...props}
        />
    ),
    ...,
    Callout // <-- here!
}

Once you add your component to the file, you can use it in an MDX file in the same way you would use it in a JSX or TSX file.

Generating and Displaying Content

ContentLayer generates content when the project builds. The npm run dev command has been remapped to run concurrently "contentlayer2 dev" "next dev", which both runs the contentlayer2 build step and the NextJS build step.

An Aside:
The original ContentLayer package is no longer maintained, so we use ContentLayer2, an up-to-date fork that is maintained by the community.

Once this command runs, a new .contentlayer folder will be added to your project directory. This directory contains the content you wrote in .mdx converted into a new JSON-based data representation.

From within your Next.js project, once content is generated, new objects containing generated content are importable from contentlayer/generated. These objects typically have the name all<ContentType>s. For example, to access all generated Pages data from my content, we can import allPages like so:

import { allPages } from 'contentlayer/generated';

This template also includes convenience helper function that generate Next.js pages for you based on input content. This helper function, called generateNextjsContentPage, generates the Next.js page as well as Next.js generateMetadata and generateStaticParams required for static site exporting. Here is a minimal example generating a page from allPages:

/app/(content)/[[..slug]]/page.tsx
const {
  generateMetadata,
  generateStaticParams,
  ContentPage
} = generateNextjsContentPage(allPages, (doc) => (
    <>
        <h1>{doc.title}</h1>
        <Mdx code={doc.body.code} />
    </>
));
 
export { generateMetadata, generateStaticParams };
export default ContentPage;

The first parameter to generateNextjsContentPage is the content for a specific content type, and the second parameter is function that returns a React component for the page to generate. <Mdx code={doc.body.code} /> will turn into the generated HTML from the content, and you may use other fields for the content type elsewhere on the page.

Note that this page is on a catch-all route for [[...slug]], which allows the route to directly match the path of the content from within its parent content folder. For example, using this content directory structure again:

content/pages
    |- index.mdx
    |- products
    |   |- mocha.mdx
    |   |- pumpkin-spice.mdx

Using the route / will show the content for index.mdx. The route /products/mocha will show the content for mocha.mdx.

You may also prefix these routes by placing the Next.js page at a directory like: /app/(content)/pages/[[...slug]]/page.tsx

Then, using the route /pages/ will show the content for index.mdx. The route pages/products/mocha will show the content for mocha.mdx, and so on. This approach is useful for routing with different types of content.

To see this in action, check out this directory from the COMP 426 course website.