In a previous article, I described my favorite setup for building static websites: using Contentful as a headless content management system, and Next.js (with TypeScript enabled) to build the site.
Side note: if you like to read more about the resilience and flexibility of headless systems in general, I recommend the corresponding article by Stefan Baumgartner on Smashing Magazine.
It can be challenging to create sites for clients who are used to drag & drop page building like with the Elementor plugin for WordPress or services like Wix:
- clients want to move components around on the page and nest them arbitrarily
- clients look for options to add spacings here and there (“Why doesn’t it have any effect when I insert a couple of blank lines here?”)
- clients become creative and change font sizes and colors (“How can I make this very important paragraph red and the font size bigger?”)
I had to build a couple of websites to find a good approach on how to provide flexibility for my clients but still maintain control over the design and rely on a resilient and type-safe codebase behind the scenes. In this article, I would like to describe how I build static websites these days and how I got there.
The code examples in this guide are built on top of the code from my article about a static website dreamteam, still using:
- Node 16.14
- Next.js 12.1
- TypeScript 4.5
Recap: the blog example website
We’ve built a small blog example website (with posts about Harry Potter 🪄) consisting of a homepage that lists all articles and the article detail pages. For simplicity’s sake we hardcoded some text on the homepage.
Now what if there should be additional editable sections on the homepage like an introduction text, a newsletter subscription form, or another call to action? What if we need more pages than just the homepage? For this, we need to start thinking in sections and components. For each component we add in Contentful, we create a component in our Next.js project. Let’s start with implementing the article component we’ve previously configured.
A React component for articles
Last time we added a grid of links to articles to the index.tsx
page. Today we’re going to move the few lines of code into a separate component file. In src/components/Article
, we create three files: Article.tsx
, Article.module.css
, and index.ts
with the following content:
/* Article.tsx */
import React from "react";
import { IArticleFields } from "../../@types/contentful";
import styles from "./Article.module.css";
const Article = ({ slug, title, description }: IArticleFields) => (
<a href={`/${slug}`} className={styles.card}>
<h2>{title} →</h2>
<p>{description}</p>
</a>
);
export default Article;
/* Article.module.css */
/* TODO: Move the .card styles from Home.module.css into this file. */
/* index.ts */
export { default } from "./Article";
I got used to creating one folder for each component containing the actual component code (.tsx
) and the styles (.css
). To avoid imports like import Article from "../components/Article/Article";
(with the component name duplicated), I create the extra index file with just a default export.
In the index.tsx
file—the one where we find all the code to render the homepage—we replace the part rendering a link for every article with our newly created component:
import Article from "../src/components/Article";
/* All the code for the Home page component */
<div className={styles.grid}>
{articles.map((article) => (
<Article key={article.slug} {...article} />
))}
</div>
Text block and article list components
For every website I build, I always need some kind of text block component, often with the option to add an image. Additionally, components to group and list other components (like articles, portfolio entries, partner logos,…) are useful as well.
For extending the example blog website, I go to my Contentful space’s content model section and create a text block component with the following properties:
- an internal title (
internalTitle
; I use this to find the element in Contentful, but won’t render the title on the page): short text, required - the content (
content
): rich text, required
I create a “Text block” and fill it with the text currently on the homepage:
- Internal title: “Homepage introduction”
- Content: “This is a blog with many interesting articles about Harry Potter.”
Next, let’s add an article list component. This one consists of:
- a title (
title
), which should be a short text and required - elements (ID:
elements
), which are references to selected articles, so I select the field type “Reference” and set it to “Many references”. In the validation settings I activate “Accept only specified entry type” and choose “Article”.
When the content model is ready, let’s navigate to “Content” in Contentful and create such a list. I set the title to “Latest articles” and link the first three Harry Potter articles in the “Elements” section.
Don’t forget to run npm run generate:types
every time you make updates to your content model in Contentful to keep your types up to date!
Implementing the components in Next.js
For both components we need to create their equivalents in our Next.js project: one folder containing three files for each of them:
src/
components/
ArticleList/
ArticleList.module.css
ArticleList.tsx
index.ts
TextBlock/
index.ts
TextBlock.module.css
TextBlock.tsx
The article list looks like this (we’re using the previously created article component here):
import React from "react";
import { IArticleListFields } from "../../@types/contentful";
import Article from "../Article";
import styles from "./ArticleList.module.css";
const ArticleList = ({ title, elements }: IArticleListFields) => {
if (!elements || elements.length < 1) {
return null;
}
return (
<>
<h2>{title}</h2>
<div className={styles.grid}>
{elements.map((el) => (
<Article key={el.sys.id} {...el.fields} />
))}
</div>
</>
);
};
export default ArticleList;
Move the styles for the .grid
selector from the current homepage styles to this component’s stylesheet. Following is the code for the text block component:
import { documentToReactComponents } from "@contentful/rich-text-react-renderer";
import React from "react";
import { ITextBlockFields } from "../../@types/contentful";
const TextBlock = ({ content }: ITextBlockFields) => {
return <div>{documentToReactComponents(content)}</div>;
};
export default TextBlock;
The index.ts
files export the component. Styles can be added another time, but I create the file anyway to be prepared.
Add components to the homepage
Now that we have all components we need in place, let’s put them on the homepage. Let me quickly go back in time and tell you how I’ve built my first websites with this setup:
I created a React component for each Contentful component (as described above) with matching types, which was a good first step. Then, I created all the content elements required based on the website design and the client’s needs and filled them with texts and images.
Back in the code, I fetched the elements from Contentful based on their ID at exactly the place where I needed them. You can imagine which big problem this lead to: when the client wanted to add another text block, or shift elements around, or temporarily hide elements, or create a new page, they had to contact me and several code updates were required. 😬
This was frustrating both for me and my clients, so obviously, I had to think of a better solution…
The page component
To make it possible to create new pages easily and modify the content structure on a page in Contentful (and not in the Next.js codebase) we need an additional component: a page component.
The page component consists of:
- a title (required)
- a slug (required to generate the page’s URL, must be unique)
- content modules: a one-to-many reference where text blocks and article lists are accepted entry types (see validation settings)
This is how the generated interface should look like after running npm run generate:types
:
export interface IPageFields {
/** Title */
title: string;
/** Slug */
slug: string;
/** Content modules */
contentModules: (IArticleList | ITextBlock)[];
}
We now create the index page in Contentful and set the title to “Welcome to my Harry Potter blog!”, the slug to index
and add the homepage introduction text block and the latest articles list as content modules.
Using the page component for the homepage
In the homepage’s code (index.tsx
), we need to make some updates to actually use the recently created page component as a content source. We are going to remove the hardcoded text and instead of fetching the articles directly, we fetch the page we created instead. Note that for the index page only, I’m going to fall back to my “fetch-element-by-ID” approach because there should always be one dedicated index page available in Contentful that should never be deleted by the client. We are going to build a component for generic pages—which can be used for an about me page, a contact page, etc.—later.
In the content-service.ts
file, we add another function to the ContentService
class:
getEntryById<T>(id: string) {
return this.client.getEntry<T>(id, {
// Including more levels is necessary if there are nested entries in the Contentful content model
include: 3,
});
}
And here is the updated index file:
import type { GetStaticProps, NextPage } from "next";
import Head from "next/head";
import { IPageFields } from "../src/@types/contentful";
import { HOMEPAGE_ID } from "../src/util/constants";
import ContentService from "../src/util/content-service";
import styles from "../styles/Home.module.css";
type Props = Pick<IPageFields, "title" | "contentModules">;
const Home: NextPage<Props> = ({ title, contentModules }) => (
<div className={styles.container}>
/* The <Head> component should be here, but is not relevant for this code example. */
<main className={styles.main}>
<h1 className={styles.title}>{title}</h1>
// TODO: render the contentModules
</main>
</div>
);
export default Home;
export const getStaticProps: GetStaticProps<Props> = async () => {
const indexPage = await ContentService.instance.getEntryById<IPageFields>(
HOMEPAGE_ID,
);
return {
props: {
...indexPage.fields,
},
};
};
I removed all the content and replaced the title with the title
value now coming from Contentful. In the getStaticProps
function, instead of the articles, I fetch the homepage by its ID—you can find this value when you navigate to the “Info” tab (right sidebar) when editing the page in Contentful and look for “Entry ID”.
The page should look like this in the browser now:
The content module renderer
I want to be able to render every possible content component that could be part of the contentModules
, so I need an additional React component for this task: the ContentModuleRenderer
. I create the following file in the src/components
folder:
/* ContentModuleRenderer.tsx */
import React from "react";
import {
IArticleListFields,
IPageFields,
ITextBlockFields,
} from "../@types/contentful";
import ArticleList from "./ArticleList";
import TextBlock from "./TextBlock";
type Props = {
module: IPageFields["contentModules"][0];
};
const ContentModuleRenderer = ({ module }: Props) => {
const contentTypeId = module.sys.contentType.sys.id;
switch (contentTypeId) {
case "articleList":
return <ArticleList {...(module.fields as IArticleListFields)} />;
case "textBlock":
return <TextBlock {...(module.fields as ITextBlockFields)} />;
default:
console.warn(`${contentTypeId} is not yet implemented`);
return null;
}
};
export default ContentModuleRenderer;
I map the content module’s type to the respective component and pass the fields—in our case, we need to support the “article list” and the “text block” components for now. Back on the index.tsx
page, I replace the TODO
and update the code as follows:
const Home: NextPage<Props> = ({ title, contentModules }) => (
<div className={styles.container}>
/* The <Head> component should be here, but is not relevant right now. */
<main className={styles.main}>
<h1 className={styles.title}>{title}</h1>
{contentModules.map((module) => (
<ContentModuleRenderer key={module.sys.id} {...{ module }} />
))}
</main>
</div>
);
The updated page looks like this in the browser:
The great thing about the setup we’ve just built is that content editors now have full control over the content of a page (order of content modules, content of text blocks,…), while the design is still maintained in the Next.js codebase. Every time a new component is required on the website, you need to create it in Contentful first, re-generate the types, provide the component in Next.js, and finally add it to the “Content module renderer”.
Creating pages automatically
Up until now, we used the page component only for the index page. What if the client created a new page in Contentful and we wanted it to be part of our website? To accomplish this, I am going to restructure the pages
folder: I move the [slug].tsx
file we created for the articles to a subfolder articles/[slug].tsx
and create a new empty [slug].tsx
file in the pages
folder.
pages/
articles/
[slug].tsx
[slug].tsx
_app.tsx
index.tsx
In the Article.tsx
component, we need to update the value of the link’s href
attribute:
<a href={`/articles/${slug}`} className={styles.card}>
In Contentful, let’s create a simple test page with a title (“About me”), a slug (about-me
), and a text block with a few lines of text content (this can be “Lorem ipsum” for now).
In the newly created [slug].tsx
file, we need to
- generate paths for all pages
- fetch the content of all pages
- render the content of all pages
This is similar to what we already implemented for the article detail pages. The only difference in getStaticProps
and getStaticPaths
is that I exclude the page where slug === "index"
, because we handle this page separately.
The component code is almost the same as for the homepage. For demonstration purposes, I reused the Home.module.css
styles. Here’s the full page code:
import { GetStaticPaths, GetStaticProps, NextPage } from "next";
import Head from "next/head";
import React from "react";
import { IPageFields } from "../src/@types/contentful";
import ContentModuleRenderer from "../src/components/ContentModuleRenderer";
import ContentService from "../src/util/content-service";
import styles from "../styles/Home.module.css";
interface Props {
page: IPageFields;
}
const Page: NextPage<Props> = ({ page }) => {
const { title, contentModules } = page;
return (
<div className={styles.container}>
<Head>
<title>{title} | My awesome Harry Potter blog</title>
</Head>
<main className={styles.main}>
<h1 className={styles.title}>{title}</h1>
{contentModules.map((module) => (
<ContentModuleRenderer key={module.sys.id} module={module} />
))}
</main>
</div>
);
};
export default Page;
export const getStaticProps: GetStaticProps<Props, { slug: string }> = async (
ctx,
) => {
const { slug } = ctx.params!;
const pages = (
await ContentService.instance.getEntriesByType<IPageFields>("page")
).filter((page) => page.fields.slug !== "index");
const page = pages.find((page) => page.fields.slug === slug);
if (!page) {
return { notFound: true };
}
return {
props: {
page: page.fields,
},
};
};
export const getStaticPaths: GetStaticPaths = async () => {
const pages = (
await ContentService.instance.getEntriesByType<IPageFields>("page")
).filter((page) => page.fields.slug !== "index");
return {
paths: pages.map((page) => ({
params: {
slug: page.fields.slug,
},
})),
fallback: false,
};
};
When I navigate to http://localhost:3000/about-me
, this is what I can see in my browser:
Summary
Let’s summarize all the steps for a flexible website setup of this article. This is what we did; we:
- Created a React component for the article link (
Article.tsx
) - Created two new Contentful components: Text block and Article list
- Created React components for Text block and Article list (
TextBlock.tsx
andArticleList.tsx
) - Created a reusable Page component in Contentful
- Used the Page component for the index page content
- Built a content module renderer to handle all possible content module elements that can be part of a page (in our case: text block and article list)
- Restructured the
pages
folder and created a generic[slug].tsx
file to automatically build all pages created in Contentful
What’s next?
On the way to a great website, there are still many open tasks left:
- the presented code snippets can be beautified 💅; code can be reused by creating even more components
- more components will be required—for the content and for the layout/structure
- page metadata and navigation are large topics where I am still trying to find a solution that works both for me as a developer and my clients as well
- and much more… 🤯
The goal of this and the previous blog post was to outline my approach to creating a static website setup by defining the structure of the site and its content in Contentful and maintaining the business logic and styles in a Next.js project, all with TypeScript support. 🎉
I am happy to discuss this article and/or answer any open questions over on Twitter! 🐦