Make NextJS Image Auto-sized

2022-02-04 Simon Liang

Recently when I am working on this blog site, I would like to cache the images we pulled from Notion into this NextJS image cache. The reason is that Notion uses a temporarily signed URL from AWS S3 to serve the images, while it works without being authenticated and it is great. The signed URL will expire after some time and it would become unavailable before the blog post page revalidates itself and gets a new image.

So I would like to have my NextJS server cache the image, potentially with a longer expiration date so we can keep the images available for longer. By using the <Image /> component provided by next/image, this should be relatively straightforward.

So instead of directly serving the markdown generated by notion-to-md, I added html-react-parser and replaced the <img> tags with NextJS’s <Image /> components.

import React from "react";
import Image from "next/image";
import parse from "html-react-parser";

export function renderHtml(html: string) {
  return parse(html, {
    replace: (domNode: any) => {
      if (domNode.type === "tag" && domNode.name === "img") {
        const img = domNode;
        return (
          <Image
            src={img.attribs?.src}
            layout="fill"
            objectFit="contain"
            alt={img.attribs?.alt}
          />
        );
      }
    },
  });
}

However, when this renders, the images was out of place and I realized that the NextJS Image Components are doing something strange under the hood: It made the position CSS property of the <img> tag to be absolute and relying on the sizes / styles provided by the parent component.

This is fine for the most cases, especially if the image asset has a known size to the developer. However, for blog posts, the image size will not be known by the site from the start, so it is not easy to make it work. I have tried quite a few ways, and the best way I could come up with is by overriding the styles myself.

First of all, add a wrapper around the <Image /> component and add class names to both the component and its wrapper:

export function renderHtml(html: string) {
  return parse(html, {
    replace: (domNode: any) => {
      if (domNode.type === "tag" && domNode.name === "img") {
        const img = domNode;
        return (
          <span className="next-image-wrapper">
            <Image
              className="next-image"
              src={img.attribs?.src}
              layout="fill"
              objectFit="contain"
              alt={img.attribs?.alt}
            />
          </span>
        );
      }
    },
  });
}

Then I moved to the global style sheet and added the following:

/* global styles to make next/image component auto-sized */

.next-image-wrapper > span {
  position: relative!important;
}

.next-image {
  position: relative!important;

  width: auto!important;
  height: auto!important;
}

Note that I had to add !important to these styles because the styles added by <Image /> component is inline inside the <img> tag and the generated <span> wrapper.

I hope this helps other developers with the same issues. And please feel free to let me know what would be the better solution for this. Cheers!