Rendering Contentful in Rails and Next.js

A couple of code snippets to help you render your Contentful content in Rails or Next.js.

Rendering Contentful in Rails and Next.js

Recently, I moved the Bento marketing site from ye ol' reliable Rails to Next.js.

One of the things that made that process super smooth was I have been using Contentful to manage my blog posts in Rails. I did this because I couldn't be bothered building a CMS and I love the flexibility that Contentful provides.

For the migration, it ended up being super easy.

I just fetched the content in Next.js similarly to how I did it in Rails and I was off to the races. No need to mess with redirects or manually port content. It #justworked.

Below is two very simplified versions of my implementation that you can use yourself.

Rails Guide

Start by installing the gem.

gem install contentful

Then, create a controller for your blog posts. For Bento, I just wanted to have everything under /posts so created a PostController and added the route.

class PostsController < ApplicationController
	before_action :init_contentful

    def index
        @posts = @client.entries(content_type: "blogPost", include: 2)
    end

    def show
        @post = @client.entries(content_type: 'blogPost', include: 10, 'fields.slug[match]' => params[:id]).first
    end

    private
    def init_contentful
        @client ||= Contentful::Client.new(
            access_token: ENV['CONTENTFUL_ACCESS_TOKEN'],
            space: ENV['CONTENTFUL_SPACE_ID'],
            dynamic_entries: :auto,
            raise_errors: false
        )
    end
end

Once you add your keys and content you should be done! Nice!

You can now render your blog posts in your Rails views however you like.

Next.js

In Next.js it's basically the same process but a little more involved.

First, install contentful.

yarn add contentful

Then create a new utility function to help you fetch all posts.

// utils/contentfulPosts.tsx

const space = process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID
const accessToken = process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN

const client = require('contentful').createClient({
  space: space,
  accessToken: accessToken,
})

export async function fetchEntries() {
  const entries = await client.getEntries({ content_type: 'blogPost', fields: {}, include: 3 })
  if (entries.items) return entries.items
  console.log(`Error getting Entries for ${contentType.name}.`)
}

export default { fetchEntries }

Now we can spin up a new page to render all the posts.

// pages/posts.tsx
import { fetchEntries } from '@utils/contentfulPosts'

export default function Home({ items }) {
  return (
    <>
	  {items.map(item => (
		<div key={item.sys.id}>
		  <h1>{item.fields.title}</h1>
		  <p>{item.fields.body}</p>
		</div>
	  ))}
	</>
  )
}

export async function getStaticProps() {
  const res = await fetchEntries()
  const posts = await res.map((p) => {
    return p.fields
  })
  return {
    props: {
      items: posts,
    },
  }
}

For rendering individual content, I took a slightly different approach to the above.

First, I created a new utility function to initiate the client.

// utils/contentfulClient.tsx
import { createClient } from "contentful";

const client = createClient({
    space: process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID,
    accessToken: process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN,
});

export default client;

Then I create a new page to render a single post.

// pages/[slug].tsx
import client from '@utils/contentfulClient';

export async function getStaticPaths() {
    const res = await client.getEntries({
        content_type: "blogPost",
    });

    const paths = res.items.map((item) => {
        return {
            params: { slug: item.fields.slug },
        };
    });

    return {
        paths,
        // if we go to path that does not exist, show 404 page
        fallback: false,
    };

}

export async function getStaticProps({ params }) {
    const slug = params.slug;

    const res = await client.getEntries({
        content_type: "blogPost",
        "fields.slug": slug,
    });

    const data = await res.items;
    const post = data[0];
    const content = post.fields.body


    return {
        props: {
            post,
            content,
        },
    };
}
export default function Post({ post, content }) {
    return (
        <>
			<h1>{post.title}</h1>
		</>
    )
}

And you're done! Content should be loading from Contentful at the same paths as before.

Subscribe to my personal updates

Get emails from me about building software, marketing, and things I've learned building products on the web. Occasionally, a quiet announcement or two.

Email marketing powered by Bento