Rich text and images with Contentful, Next.js and Zod

jussinevavuori

Jussi Nevavuori

Posted on August 18, 2022

Rich text and images with Contentful, Next.js and Zod

The previous article demonstrated how we can create models in our codebase for our existing Contentful models to fetch them and interact with them in a fully typesafe manner. We also hinted at how we’re going to work with images and rich text, and what the context object in the previous tutorial will do.

Read more

Read the previous article

In the previous article we explored creating the createContentfulModel function that provides a useful abstraction for working with Contentful models.

How to optimise and tweak Contentful images

In this article we’re going to explore how we can create a contentfulImage function which allows us to automatically fetch our images in optimised next-gen formats and more.

Setting up schemas for Contentful images and rich text

Contentful image fields are regular Contentful entries. We’ll create a schema for the fields of an image and the image entry itself using the contentfulEntrySchema we created in the previous article.

import { z } from "zod";
import { contentfulEntrySchema } from "./contentfulEntrySchema";

export const contentfulImageFieldsSchema = z.object({
  title: z.string(),
  description: z.string(),
  file: z.object({
    url: z.string(),
    details: z.object({
      size: z.number().positive().int(),
      image: z.object({
        width: z.number().positive().int(),
        height: z.number().positive().int(),
      }),
    }),
    fileName: z.string(),
    contentType: z.string(),
  }),
});

export const contentfulImageField = () =>
  contentfulEntrySchema.extend({
    fields: contentfulImageFieldsSchema,
  });
Enter fullscreen mode Exit fullscreen mode

We’re also going to setup a type for our rich text fields. For now, we’re going to take a shortcut and simply use a typecast with a zod transform and not perform any actual data validation as the shape of Contentful rich text is very complicated.

import { z } from "zod";
import { RichTextContent } from "contentful";

export const contentfulRichTextField = () =>
  z.any().transform((x) => x as RichTextContent);
Enter fullscreen mode Exit fullscreen mode

Providing all custom Contentful fields using the context object

Now is time to reveal what we were intending to do with the context object all along. (Note: the context object can be extended to do much more than only provide custom fields but we’re going to start with this).

The context object will contain a contentfulFields property which will in turn contain both the schemas we defined. The type of the context will be updated to the following.

export type CreateContentfulModelContext = {
  contentfulFields: {
    richText: typeof contentfulRichTextField;
    image: typeof contentfulImageField;
  };
};
Enter fullscreen mode Exit fullscreen mode

Now, in our createContentfulModel we must create the context and provide in to the fieldsSchemaCreator function.

// ... more imports
import { contentfulImageField } from "./contentful-image-field";
import { contentfulRichTextField } from "./contentful-rich-text-field";

// ... types

export function createContentfulModel<TDataIn extends {}, TDataOut>(
    contentType: string,
    fieldsSchemaCreator: FieldsSchemaCreator<TDataIn, TDataOut>
) {

    // Set up context
    const context: CreateContentfulModelContext = {
    contentfulFields: {
      richText: contentfulRichTextField,
      image: contentfulImageField,
    },
  };

    // Provide context to fields schema creator
    const fieldsSchema = fieldsSchemaCreator(context);

    // ... Rest of the function
}
Enter fullscreen mode Exit fullscreen mode

Now all fields schema creator functions have access to the newly created field types. Let’s next see how we can use them.

Using the image and rich text field types

Let’s update our example model to contain an image and rich text using the new fields from the context object.

import { z } from "zod";
import { createContentfulModel } from "./create-contentful-model";

export const exampleModel = createContentfulModel("example", (ctx) => z.object({
    title: z.string(),
    description: z.string().optional(),
    rating: z.number().int().positive(),
    body: ctx.contentfulFields.richText(),
    image: ctx.contentfulFields.image(),
}));
Enter fullscreen mode Exit fullscreen mode

There. That’s all there is to it.

Consuming rich text and images

First let’s create a new rich text component that takes RichTextContent as a prop. For this we’re going to use @contentful/rich-text-react-renderer which you can install with

npm i @contentful/rich-text-react-renderer
Enter fullscreen mode Exit fullscreen mode

The component is simple and shown below and simply uses the documentToReactComponents method wrapped in an <article>. However, customising any rich text can now be done in a single place.

import { documentToReactComponents } from "@contentful/rich-text-react-renderer";
import { RichTextContent } from "contentful"

export interface RichTextProps {
    content?: RichTextContent;
}

export function RichText(props: RichTextProps) {
    return <article>
        {documentToReactComponents(props.content as any)}
    </article>
}
Enter fullscreen mode Exit fullscreen mode

Consuming images is simple as the image itself contains the URL of the file which can be directly used as the src for the image. Similarly it contains a title for the alt.

Let’s update our simple React application to render both the image and the rich text content.

<ul>
        {
            examples.map(example => (
                <li key={example.sys.id}>
                    <img
                        src={example.fields.image.fields.file.url}
                        alt={example.fields.image.fields.title}
                    />
                    <p>{example.fields.title}</p>
                    <p>{example.fields.description}</p>
                    <p>{example.fields.rating} / 5</p>
                    <RichText content={example.fields.body} />
                </li>
            ))
        }
    </ul>
Enter fullscreen mode Exit fullscreen mode

Read this article to optimize and tweak your Contentful images. Replace your src={entry.fields.image.fields.file.url} with the improved src={contentfulImage(entry.fields.image, { format: "webp", quality: 60 })} for example using the methods or package described in the article or directly install contentful-image with

npm i contentful-image
Enter fullscreen mode Exit fullscreen mode

That’s it.

Thank you for taking the time to read this article, hopefully it is of use to you!

💖 💪 🙅 🚩
jussinevavuori
Jussi Nevavuori

Posted on August 18, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related