Contentlayer - Content has NEVER been easier!

coffee image

When I first created my portfolio, I have always wanted to integrate my projects as individual pages where I can share my thoughts and goals for each project I have done. However when it came to actually implementing this kind of feature, I ended up being stumped: how will I be able to render my project content to my site?

Solution Precursor

I built my portfolio with Next.js and while I was learning how to use the framework, I did their blog tutorial that teaches the foundations of the framework (See blog tutorial here). One of their sections covered rendering markdown as the example content provided is markdown.

However, there exists a big problem: The first is that this tutorial utilized several libraries (remark, gray-matter) that require a deeper learning curve to utilize (See the tutorial step about remark here) which as a complete beginner to Next and front-end programming as a whole, I ended up quite confused! Not to mention the transition from page router to app router..

What solution did I choose on during this time? Well... I hard-coded all of my project content right into my components and left as is. At the very least, it worked! BUT I could have done better...

Note: MDX is also a feature that is listed within the documention but during this time, it was experimental so I did not have much interest in it.

Enter.. Contentlayer

contentlayer page

Fast forward to today, I wanted to update my portfolio with more projects and at this point, I thought to myself, "Why not give the project content problem another go?"

Meet Contentlayer, an SDK that transform your content into type-safe JSON data that you can simply import right into your pages. Contentlayer offers a feature that allows transformation of local content files (markdown, MDX, JSON, YAML). I have heard of this library several times throughout social media sites (Reddit, Twitter, etc) but never used it until now, so it has been simply been on the back-burner.

Previously, I hard-coded my content right into my code but when I came across this library, I recalled how I originally wanted my project content as markdown files. Then I remembered about the local file transformation feature of Contentlayer and bam, I was struck with some inspiration!

Contentlayer Setup

Setting up Contentlayer for my project has never been truly easier. Looking through their documentation for Next, there are several steps:

  1. Next configuration
  2. Typescript configuration
  3. Ignore Build Output
  4. Define content schema
  5. Create directory to hold content files
  6. Content import

Next configuration

If we want to hook our application into the development and build process of Next, we need to wrap the config file with the provided withContentlayer method, which is what I did for my project:

// next.config.js
const { withContentlayer } = require('next-contentlayer');

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {},
};

module.exports = withContentlayer(nextConfig);

Typescript Configuration

We need to edit our typescript config so that it knows where to look at our files and easier to import right into our project, generated by Contentlayer. To do so, the documentation suggests on adding the following lines:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    //  ^^^^^^^^^^^
    "paths": {
      "contentlayer/generated": ["./.contentlayer/generated"]
      // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    }
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    ".contentlayer/generated"
    // ^^^^^^^^^^^^^^^^^^^^^^
  ]
}

Ignore Build Output

A new directory will be created so we must make sure to add it to our .gitignore file to prevent any issues with Git

# .gitignore

# ...

# contentlayer
.contentlayer

Define Content Schema

With all the setup out of the way, we can now move onto setting up Contentlayer to our needs!

Since the focus of my portfolio is to display my projects as individual pages, it only makes sense to define a schema for my projects.

To create our content schema, Contentlayer requires a config file called contentlayer.config.ts and within it is where I defined my ProjectItem schema:

// contentlayer.config.ts
import { defineDocumentType, makeSource } from 'contentlayer/source-files';

export const ProjectItem = defineDocumentType(() => ({
  name: 'Project',
  filePathPattern: `**/*.md`,
  fields: {
    title: { type: 'string', required: true },
    content: { type: 'string', required: true },
    link: { type: 'string', required: true },
    site: { type: 'string', required: true },
    src: { type: 'string', required: true },
    alt: { type: 'string', required: true },
    skills: { type: 'list', of: { type: 'string' }, required: true },
  },
  computedFields: {
    url: {
      type: 'string',
      resolve: (project) => `/projects/${project._raw.flattenedPath}`,
    },
  },
}));

export default makeSource({
  contentDirPath: 'projectItems',
  documentTypes: [ProjectItem],
});

The most important takeaway is the fields argument within our object, which allows us to define what and how our document type should look like:

  • title: name of each project
  • content: short description of the project
  • link: GitHub repo link/URL
  • site: Live site URL
  • src: screenshot of the project
  • alt: alternative text for the screenshot
  • skills: a list of various technologies used to create the project

Any data objects generated from these files will contain the fields specified above, along with a body field that contains the raw and HTML content of the file. The url field is a special computed field that gets automatically added to all post documents, based on meta properties from the source file.

Create directory to hold content files

The last step to our setup would be to create a directory that holds all of our markdown files. For my case, a projectItems directory that houses all of my projects as individual markdown files.

projectItems/
├── blog.md
├── bookmark.md
├── ip-address-tracker.md
└── ...

Content Import

Last step - linking all of the code together and importing it as content! To start, I imported the project items (array) right into my app/project/[slug]/page.tsx

// app/project/[slug]/page.tsx
import { allProjects } from 'contentlayer/generated';

Once imported, I utilized Next's generateStaticParams function to then statically generate my project's routes at build time by mapping through each individual project item:

// app/project/[slug]/page.tsx

...

export async function generateStaticParams() {
  return allProjects.map((project) => ({
    slug: project._raw.flattenedPath,
  }));
}

And... that's it! With everything said and done, all I have really done is imported my project files directly into the code, no other parsers or transformation needed.

Conclusion

If I were to rate my overall experience on connecting Contentlayer to my project, I would give it a 10 out of 10 star-rating; the entire process was simple and clean, with superb documentation to boot.

At the end of the day, there's a lot more to Contentlayer as I have barely scratched the surface for my project needs but as a content SDK, it does it's job very well!

Resources:

  1. Unsplash Laptop - Lauren Mancke
  2. https://nextjs.org/learn/foundations/about-nextjs
  3. https://nextjs.org/docs/app/building-your-application/configuring/mdx
  4. https://contentlayer.dev/docs/environments/nextjs-dcf8e39e