Rendering Storyblok images with Next.Js

Rendering Storyblok images with Next.Js

Like all CMS's Storyblok provides content editors with the facility to upload images to an asset library (or digital asset manager for a more current term). This asset library gives users the ability to organise all their assets into folders, assign tags, as well as more advanced tools like setting expiration dates and setting focal points.

For developers the asset library provides a CDN for serving images in way of AWS Cloudfront to minimise latency. There is also an image service with functionality to crop images as well as adjust the quality level and apply filters.

Storyblok Assets

Rendering Images with Next.Js

To render images in Next.Js you should use Next's Image component. This extends the standard HTML <img> element to provide automatic optimisation.

1import Image from 'next/image'
2
3export default function Page() {
4 return (
5 <Image
6 src="/profile.png"
7 width={500}
8 height={500}
9 alt="Picture of the author"
10 />
11 )
12}

The automatic optimisation will resize images to the desired height and width at a particular quality level.

As well as this the Image component offers other properties such as specifying responsive sizes.

Using next/image with Storyblok

To use the Next.js image component with Storyblok I create my own Image component that wraps next/image.

As you can see in the code below, this accepts properties for the StoryblokAsset as well as any other properties I might want to pass through such as className or the loading setting.

The component then extracts the filename from the StoryblokAsset to set the src property on the Next.Js Image component.

1import { StoryblokAsset } from "@/types/storyblok";
2import Image from "next/image";
3import React from "react";
4
5export interface ImageProps {
6 image: StoryblokAsset;
7 width?: number;
8 height?: number;
9 className?: string;
10 loading?: "lazy" | "eager";
11}
12
13export const ImageComponent: React.FC<ImageProps> = ({
14 image,
15 width,
16 height,
17 className,
18 loading = "lazy",
19}) => {
20 if (image.filename == null || image.filename.length === 0) {
21 console.error("Image filename is null");
22 return null;
23 }
24
25 return (
26 <Image
27 src={image.filename}
28 alt={image.alt || ""}
29 className={className}
30 width={width}
31 height={height}
32 loading={loading}
33 />
34 );
35};
36

However there's an improvement that could be made to this. Storyblok and Next.Js both provide the ability to optimise images and by directly using Next.Js's image component we will be using Next.Js over Storyblok.

This isn't a massive issue, but by using Next.Js the image request will now go through these steps:

  1. The browser will request the Image from the website host (not Storyblok). e.g. Vercel or Netlify
  2. The host will request the image from Storyblok's CDN
  3. Storybloks CDN will return the full size image
  4. The host will resize the image
  5. The host will return the image to the browser

Not only is this extra steps than is really needed, it also means we're using bandwidth on the website host. So wouldn't it be better if the client could just directly load the image from Storyblok and have Storyblok resize it to begin with?

Next.Js Image Loaders

Next.Js provides a property on the Image component to specify a loader, which is a function to generate the image url. When a loader is set images will be set to use this URL rather than Next.Js's own url with the Next.Js image optimisation.

The loader can accept parameters for the original src, a width and quality level so that a URL can be constructed to a different image service. Note: Image loaders do not support a height parameter.

In the code below I have created a loader that will return the URL for Storybloks image service with the width and quality parameters being set.

1const storyblokLoader: ImageLoader = ({
2 src,
3 width,
4 quality,
5}: ImageLoaderProps): string => {
6 return `${src}/m/${width}x0/filters:quality(${quality || 75})`;
7};

The image loaders also work in conjunction with the sizes property meaning the correct responsive size urls will be populated using the loader.

The code for my complete ImageComponent is below.

1import { StoryblokAsset } from "@/types/storyblok";
2import Image, { ImageLoader, ImageLoaderProps } from "next/image";
3import React from "react";
4
5export interface ImageProps {
6 image: StoryblokAsset;
7 width?: number;
8 height?: number;
9 className?: string;
10 loading?: "lazy" | "eager";
11}
12
13function isStoryblokImage(src: string): boolean {
14 return src.includes("a.storyblok.com");
15}
16
17const storyblokLoader: ImageLoader = ({
18 src,
19 width,
20 quality,
21}: ImageLoaderProps): string => {
22 return `${src}/m/${width}x0/filters:quality(${quality || 75})`;
23};
24
25export const ImageComponent: React.FC<ImageProps> = ({
26 image,
27 width,
28 height,
29 className,
30 loading = "lazy",
31}) => {
32 if (image.filename == null || image.filename.length === 0) {
33 console.error("Image filename is null");
34 return null;
35 }
36
37 return (
38 <Image
39 loader={isStoryblokImage(image.filename) ? storyblokLoader : undefined}
40 src={image.filename}
41 alt={image.alt || ""}
42 className={className}
43 width={width}
44 height={height}
45 loading={loading}
46 />
47 );
48};
49