"Integrating Contentful (A Headless CMS)"

Overview

When this website was first created, my project write-ups and blog posts were powered by markdown files stored directly in the website source code.

This meant that, each time I needed to make a writing edit, no matter how small 🤏, I had to:

  • Open my code editor
  • Make the edits
  • Preview the edits locally
  • Commit the code changes (either usually via PR)
  • Wait patiently for Vercel to redeploy my site to the web

waste of time halfgif

This workflow disrespects the core essence of the writing process... which thrives on rapid iteration and continual refinement. Furthermore, as much as I enjoy VSCode, creative writing directly from a markdown file is quite a meh experience; it's much cooler to use a sleek user interface like Medium or Notion... which both support markdown previews, drafts, comments, and spell-check right OOTB.

To modernize my process, I integrated my website with a headless CMS.

What's a CMS?

For those who are unfamiliar, a Content Management System (CMS) is a software app that enables the creation, management, and publication of digital content to a website.

One of the OG CMS platforms is Wordpress, which came out in May 2003. According to W3Techs at the time of writing, 42.7% of websites are built with Wordpress... which is pretty nuts.

In any case, using Wordpress, you can create posts and pages (aka content), like so:

Wordpress - posts editor

and then create your website using that content, like so:

Wordpress - page editor

So what's a Headless CMS 🤔?

A headless CMS is a content management system that separates the presentation layer (where content is presented) from the content layer (where content is stored and managed).

In other words, it's solely focused on making it easy to manage content; not (necessarily) the website.

Because headless CMS companies don't need to explicitly concern themselves with the visual layer, their product teams can focus solely on the content-editing experience. For example, it's difficult to create a custom data model using Wordpress; the only content types supported by default are pages and posts. By contrast, using a headless CMS, you can easily define whatever data type you desire.

Because headless CMSs are decoupled from the presentation layer, however, this means that they must be extremely easy to integrate with (otherwise no one could use them with their website). As such, headless CMS platforms typically come equipped with extremely performant and well-documented APIs... making them attractive to developers.

developers halfgif

Going into this project, I'd already created this website using NextJS (presentation layer); I just needed a place to store the projects and blog posts (content layer). Hence, my need for a CMS of the headless variety.

Choosing a Headless CMS

Given that it's 2023, there's myriad headless CMS players out there... which one should I pick for lucmarrie.com 🤔?

fork in the road

Here's a few notable providers, as ranked on the G2 grid:

  1. Sanity
  2. Contentful
  3. Hygraph
  4. Strapi
  5. storyblok

To be honest, I'd already heard of/used Contentful while working at Yext, so I went with them. However, in a future project, I will integrate with a few other providers and do a comparison.

Website Development

Headless CMS installation wasn't nearly as hard as I thought it'd be... the code updates more or less comprised of two steps:

  1. Migrate content to Contentful
  2. Data fetch the content at build time

Let's walk through each step:

Migrate to Contentful

Getting started with Contentful was extremely easy.

When you sign-up for an account, they give you the option to install some example content models... but personally I recommend starting from scratch.

First, I needed to create two content types:

  1. Project
  2. Blog Post

Contentful Content Model

I configured each content type with the following field schema:

  • Name (short text)
  • Description (short text)
  • Slug (short text)
  • Markdown (long text)

Contentful - Field Schema

From there, all I had to do was migrate the markdown files from my repo into Contentful. I didn't notice any sort of spreadsheet uploader, so I just manually copy/pasted the markdown by hand. Thankfully, I only had 5 markdown files total... if I'd had tons, I would've had to use Contentful's CLI to blast them in programmatically. If anyone from Contentful ever reads this... might I suggest some sort of spreadsheet upload feature?

To conclude this section, there were a few features that I quite liked when content-modeling with Contentful:

  1. The long text field type has built-in support for Markdown editing. This may be table-stakes in the headless CMS world, but it sure as hell ain't in the standard CMS world. Contentful - Markdown
  2. The short text field type allows you to auto-populate values for slugs; in my workspace, each slug auto-populates based on the project/blog post name Contentful - Slug

Data Fetching

With the content alive and well in its new CMS home, the next step was to ensure my front-end application knew how to fetch it.

To get started, I followed Contentful's JavaScript guide (clean documentation FTW). The game plan was simple:

  1. Set up the Contentful JavaScript Client
  2. Fetch the data
Set up the Contentful JavaScript Client

First, I downloaded the Contentful JavaScript library.

npm install contentful

From there, I created a utils/contentful.js file to initialize the client; like such:

require("dotenv").config(); const contentful = require("contentful"); const client = contentful.createClient({ space: process.env.CONTENTFUL_SPACE_ID, accessToken: process.env.CONTENTFUL_ACCESS_TOKEN, }); module.exports = client;

Client obtained.

Handshake halfgif

Fetch the data

Next, for the highly-anticipated fetching.

Fetch halfgif

To avoid unnecessary API requests to Contentful, I wanted to ensure that my project and blog content was only fetched once at build time; not client-side. I'm a frugal man, and don't want to blow through my Contentful free tier 🤷.

Thankfully, when using the App Router in Next.js, all components are rendered server-side by default. So all I had to do was add some API request logic to my home, project, and blog components.

Using the client from the previous step, I added a getEntries() function to my home page, which fetches projects and blogs from Contentful's Content Delivery API:

import client from "../utils/contentful"; async function getEntries() { try { const entries = await client.getEntries({ order: ["-sys.createdAt"], }); const projects:any = []; const blogPosts:any = []; entries.items.forEach((item) => { const fields = item.fields; if (item.sys.contentType.sys.id === 'project') { projects.push(fields); } else if (item.sys.contentType.sys.id === 'blogPost') { blogPosts.push(fields); } }); return { projects, blogPosts }; } catch (error) { console.error("Error fetching entries:", error); return { projects: [], blogPosts: [] }; } }

From there, I could simply map through the returned entries in my TSX, like so:

export default async function Page() { const { projects, blogPosts } = await getEntries(); return ( <> ... // truncated {projects.map((project) => ( <div> <Link href={`/projects/${project.slug}`} className="link" > {String(project.name)} </Link> <p>{String(project.description)}</p> </div> ))} ... // truncated {blogPosts.map((post) => ( <div> <Link href={`/blog/${post.slug}`} className="link" > {String(post.name)} </Link> <p>{String(post.description)}</p> </div> ))} ... // truncated </> ) }

In addition to the home page, I had to perform some minor surgery on my dynamic routes as well.

As a result of my markdown source, both my app/blog/[slug] and app/projects/[slug] components needed to be updated to handle markdown styling, since that was previously taken care of by Contentlayer transformations (shoutout to Lee Robinson). Without the Contentlayer transforms (refer to my code history 😔), my markdown styling no longer supported tables and code snippet syntax highlighting.

To fix this, I imported react-markdown (with the remark-gfm plugin enabled) and React Syntax Highlighter into my project... two extremely useful tools for rendering beautiful markdown.

import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter'; const MarkdownRenderer = ({ content }) => { return ( <> <article className="prose prose-quoteless prose-neutral prose-invert"> <ReactMarkdown remarkPlugins={[remarkGfm]} components={{ code(props) { const {children, className, node, ...rest} = props const match = /language-(\w+)/.exec(className || '') return match ? ( <SyntaxHighlighter {...rest} PreTag="div" children={String(children).replace(/\n$/, '')} language={match[1]} style={materialDark} /> ) : ( <code {...rest} className={className}> {children} </code> ) } }} > {content} </ReactMarkdown> </article> </> ); }; export default async function Project({ params }) { const project = await getProject({ params }); return ( <section> <h1 className="font-semibold tracking-tighter max-w-[650px]"> <Balancer>{JSON.stringify(project.name)}</Balancer> </h1> <MarkdownRenderer content={project.markdown} /> </section> ); }

Closing Thoughts

Next.js and Contentful both helped me fall into a pit of success during this project. Both companies have very well thought-out products, along with crystal clear documentation and quick-start guides.

In particular, Next.js's server-side by default data fetching paradigm made it a breeze to fetch my content for build-time rendering.

Similarly, Contentful's approach to content modeling is incredibly straight-forward, and their onboarding guide helped me quickly obtain the Contentful environment variables necessary to hook up my workspace to the front-end application.

This project took me about 6 hours... but now I don't have to touch the code again to write new project and blog posts! ... that is, until I need to support paginated requests 😨.