Building a Server-Side Rendered (SSR) Application with Next.js and a Headless CMS

In the ever-evolving landscape of web development, building dynamic, performant, and SEO-friendly applications is paramount. Server-Side Rendering (SSR) with Next.js provides a powerful solution, allowing developers to pre-render pages on the server, resulting in faster initial load times and improved search engine optimization. When combined with a Headless Content Management System (CMS), the development process becomes even more streamlined, enabling content creators to manage and update content independently of the codebase. This tutorial will guide you through building a complete SSR application using Next.js and a Headless CMS, showcasing how to fetch data, render content, and optimize performance.

Why Server-Side Rendering?

Before diving into the technical aspects, let’s understand why Server-Side Rendering is crucial in modern web development:

  • Improved SEO: Search engines can easily crawl and index pre-rendered content, leading to better search rankings.
  • Faster Initial Load Times: Users see content faster, as the server delivers fully rendered HTML.
  • Better User Experience: Faster load times translate to a more engaging and satisfying user experience.
  • Enhanced Performance: SSR can reduce the load on the client-side, especially for applications with complex data or interactions.

Choosing a Headless CMS

A Headless CMS is a content management system that focuses on content storage and delivery, without dictating how that content is presented. This separation of concerns allows developers to use the CMS as a data source for any front-end application, including Next.js. Several Headless CMS options are available, each with its strengths and weaknesses. For this tutorial, we will use Contentful. Contentful is a popular choice due to its user-friendly interface, robust API, and generous free plan. Other great options include Strapi, Sanity, and Prismic.

Setting Up the Project

Let’s get started by setting up our Next.js project and installing the necessary dependencies. Open your terminal and run the following commands:

npx create-next-app@latest ssr-with-cms --typescript
cd ssr-with-cms
npm install contentful

This creates a new Next.js project with TypeScript support and installs the Contentful JavaScript SDK. We’re using TypeScript for this project to ensure type safety and a better developer experience.

Configuring Contentful

Next, we need to set up our Contentful space and create a content model. If you don’t have a Contentful account, sign up for a free one at Contentful.com.

  1. Create a Space: After logging in, create a new space in Contentful. Give it a descriptive name, like “NextJS-SSR-Blog”.
  2. Create a Content Model: Inside your space, go to “Content Model” and create a new content type. Let’s call it “BlogPost”. Define the following fields:
    • Title (Short text)
    • Slug (Short text – this will be used for the URL)
    • Content (Rich text – for the main body of the blog post)
    • Featured Image (Media – for an image associated with the post)
    • Publish Date (Date and time)
  3. Add Content: Go to “Content” and create a new “BlogPost” entry. Fill in the fields with some sample content. Publish the entry.
  4. Get API Keys: Go to “Settings” -> “API keys”. Create a new API key. You will need the “Space ID” and the “Content Delivery API – access token” for your Next.js application.

Fetching Data from Contentful

Now, let’s write the code to fetch data from Contentful. Create a new file called `lib/contentful.ts` in your project and add the following code:

import { createClient } from 'contentful';

const client = createClient({
  space: process.env.CONTENTFUL_SPACE_ID!,
  accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
  host: 'cdn.contentful.com',
});

export async function getAllBlogPosts() {
  const entries = await client.getEntries({
    content_type: 'blogPost',
    order: '-fields.publishDate',
  });

  return entries.items.map((item) => {
    const { title, slug, content, featuredImage, publishDate } = item.fields;
    return {
      title,
      slug,
      content,
      featuredImage,
      publishDate,
    };
  });
}

export async function getBlogPostBySlug(slug: string) {
  const entries = await client.getEntries({
    content_type: 'blogPost',
    'fields.slug': slug,
  });

  if (entries.items.length === 0) {
    return null;
  }

  const item = entries.items[0];
  const { title, slug, content, featuredImage, publishDate } = item.fields;
  return {
    title,
    slug,
    content,
    featuredImage,
    publishDate,
  };
}

Explanation:

  • We import `createClient` from the `contentful` package.
  • We initialize the Contentful client with your `spaceId` and `accessToken`. Make sure to store these in your `.env.local` file (more on that below).
  • `getAllBlogPosts` fetches all blog posts, orders them by publish date (descending), and maps the results to a simpler format.
  • `getBlogPostBySlug` fetches a single blog post based on its slug.

Setting Up Environment Variables

To keep your API keys secure, store them as environment variables. Create a file called `.env.local` in the root of your project and add the following lines, replacing the placeholders with your actual Contentful credentials:

CONTENTFUL_SPACE_ID=YOUR_SPACE_ID
CONTENTFUL_ACCESS_TOKEN=YOUR_ACCESS_TOKEN

Important: The `.env.local` file should be added to your `.gitignore` file to prevent accidental commits of your sensitive API keys.

Creating the Blog Post Listing Page

Let’s create the page that displays a list of blog posts. Modify the `pages/index.tsx` file:

import type { NextPage } from 'next';
import Link from 'next/link';
import { getAllBlogPosts } from '../lib/contentful';

interface BlogPost {
  title: string;
  slug: string;
  publishDate: string;
}

interface Props {
  blogPosts: BlogPost[];
}

const Home: NextPage = ({ blogPosts }) => {
  return (
    <div>
      <h1>Blog Posts</h1>
      <ul>
        {blogPosts.map((post) => (
          <li>
            
              <a>{post.title}</a>
            
            <p>Published: {new Date(post.publishDate).toLocaleDateString()}</p>
          </li>
        ))}
      </ul>
    </div>
  );
};

export async function getStaticProps() {
  const blogPosts = await getAllBlogPosts();
  return {
    props: { blogPosts },
    revalidate: 60,
  };
}

export default Home;

Explanation:

  • We import `Link` from `next/link` for client-side navigation and `getAllBlogPosts` from `lib/contentful`.
  • We define a `BlogPost` interface to ensure type safety.
  • The `Home` component receives `blogPosts` as props and renders a list of links to each post.
  • `getStaticProps` is a Next.js function that runs at build time. It fetches the blog posts from Contentful and passes them as props to the `Home` component. The `revalidate: 60` option tells Next.js to re-generate the page every 60 seconds (useful for content updates).

Creating the Blog Post Detail Page

Now, let’s create the page that displays the content of a single blog post. Create a new file called `pages/posts/[slug].tsx`:

import type { NextPage } from 'next';
import { getBlogPostBySlug } from '../../lib/contentful';
import { documentToReactComponents } from '@contentful/rich-text-react-renderer';
import { BLOCKS, INLINES } from '@contentful/rich-text-types';

interface BlogPost {
  title: string;
  slug: string;
  content: any; // Content is rich text, needs special handling
  publishDate: string;
  featuredImage: {
    fields: {
      file: {
        url: string;
      };
    };
  } | undefined;
}

interface Props {
  post: BlogPost | null;
}

const Post: NextPage = ({ post }) => {
  if (!post) {
    return <div>Post not found.</div>;
  }

  const options = {
    renderNode: {
      [BLOCKS.EMBEDDED_ASSET]: (node: any) => (
        <img src="{node.data.target.fields.file.url}" alt="{node.data.target.fields.title}" />
      ),
    },
  };

  return (
    <div>
      <h1>{post.title}</h1>
      {post.featuredImage && (
        <img src="{post.featuredImage.fields.file.url}" alt="{post.title}" style="{{" />
      )}
      <p>Published: {new Date(post.publishDate).toLocaleDateString()}</p>
      <div>{documentToReactComponents(post.content, options)}</div>
    </div>
  );
};

export async function getStaticPaths() {
  // In a real application, you'd fetch all slugs from Contentful
  // For this example, we'll return an empty array, which means
  // Next.js will try to generate a page for every possible slug
  // (which is fine if you have a small number of posts)

  return {
    paths: [],
    fallback: 'blocking',
  };
}

export async function getStaticProps({ params }: { params: { slug: string } }) {
  const post = await getBlogPostBySlug(params.slug);
  return {
    props: { post },
  };
}

export default Post;

Explanation:

  • We import `getBlogPostBySlug` from `lib/contentful` and `documentToReactComponents` from `@contentful/rich-text-react-renderer` to render the rich text content. We also import `BLOCKS` and `INLINES` from `@contentful/rich-text-types` for content rendering customization.
  • The `Post` component receives a `post` prop and renders the post’s title, featured image, publish date, and content.
  • `getStaticPaths` is a Next.js function that defines the paths for which the page should be pre-rendered. In this example, we use `fallback: ‘blocking’` which means that if a path isn’t generated at build time, Next.js will generate it on-demand the first time it’s requested. In a production application, you would fetch all the slugs from Contentful at build time and return them here.
  • `getStaticProps` fetches the blog post based on the slug from the URL parameters (`params.slug`).

Rendering Rich Text Content

Contentful’s “Rich Text” field requires special handling to render the content correctly. The `@contentful/rich-text-react-renderer` package provides the `documentToReactComponents` function, which converts the Contentful rich text document into React components. We also use the `BLOCKS` and `INLINES` constants from `@contentful/rich-text-types` to customize the rendering of specific content types, like images.

In the `Post` component, we use the following code to render the content:

<div>{documentToReactComponents(post.content, options)}</div>

The `options` object allows us to customize how different content types are rendered. In this example, we define a `renderNode` function to handle the rendering of embedded assets (images).

Styling the Application (Optional)

You can add styling to your application using CSS, CSS-in-JS libraries (like styled-components), or a CSS framework like Tailwind CSS. For this example, we’ll keep the styling minimal to focus on the core concepts. You can add basic styling to the components using inline styles or by creating a `styles.css` file in the `styles` directory.

Common Mistakes and How to Fix Them

  • Incorrect API Keys: Double-check your `CONTENTFUL_SPACE_ID` and `CONTENTFUL_ACCESS_TOKEN` in your `.env.local` file. Typos are a common source of errors.
  • Missing Content Model Fields: Ensure that your Contentful content model fields (e.g., “Title”, “Slug”, “Content”) match the code’s expectations.
  • Incorrect Rich Text Rendering: If the rich text content isn’t rendering correctly, make sure you’ve installed the `@contentful/rich-text-react-renderer` package and are using `documentToReactComponents` correctly. Also, check the `options` object for any rendering customizations.
  • 404 Errors: If you’re getting 404 errors, verify that the slugs in your Contentful entries match the slugs in your application’s URLs. Also, ensure that your `getStaticPaths` function is correctly configured to generate the necessary paths. If you are using `fallback: ‘blocking’`, remember that the page might take a moment to generate the first time it’s requested.
  • Build Errors: Make sure you have the correct types installed. Run `npm install –save-dev @types/node @types/react @types/react-dom`. Also, review the error messages carefully as they often provide clues about the problem.

Key Takeaways

  • SSR with Next.js: Next.js simplifies Server-Side Rendering, improving SEO, performance, and the user experience.
  • Headless CMS: Headless CMSs like Contentful allow developers to manage content separately from the application’s code.
  • Data Fetching: Use `getStaticProps` to fetch data at build time for static pages or `getServerSideProps` for server-side rendered pages.
  • Rich Text Rendering: The `@contentful/rich-text-react-renderer` package is essential for rendering rich text content from Contentful.
  • Environment Variables: Store API keys and other sensitive information securely using environment variables.

Frequently Asked Questions (FAQ)

Q: Can I use a different Headless CMS?
A: Yes! You can adapt the code to work with other Headless CMS platforms like Strapi, Sanity, or Prismic. The key is to use the CMS’s API to fetch your content.

Q: How do I handle images from Contentful?
A: Contentful provides URLs for images. You can use these URLs directly in your `` tags. The `featuredImage` field in the example demonstrates how to access the image URL.

Q: What is the difference between `getStaticProps` and `getServerSideProps`?
A: `getStaticProps` fetches data at build time, resulting in static pages. `getServerSideProps` fetches data on each request, making it suitable for dynamic content that changes frequently. Choose the option that best fits your needs based on how often your content updates.

Q: How can I deploy this application?
A: You can deploy your Next.js application to platforms like Vercel, Netlify, or AWS. Vercel is particularly well-suited for Next.js applications and offers a streamlined deployment process.

Q: How can I improve the performance of my application?
A: Optimize images (use responsive images and image compression), lazy load components, and consider code splitting to reduce the initial bundle size. Also, caching strategies can greatly improve performance.

Building a Server-Side Rendered application with Next.js and a Headless CMS offers a powerful and flexible approach to web development. By combining the benefits of SSR with the content management capabilities of a Headless CMS, you can create dynamic, performant, and SEO-friendly websites and applications. This tutorial has provided a solid foundation, and you can now expand upon it by adding more features, refining the styling, and integrating additional functionalities to meet your specific project requirements. Remember that the choice of Headless CMS is flexible, and the principles demonstrated here can be adapted to various platforms. Happy coding, and enjoy the benefits of a modern web development workflow!