iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🍴

Introducing a library for elegantly processing microCMS Rich Editor content

に公開

Introduction

As those using microCMS are likely aware, the "Rich Editor" is one of the input forms available when configuring API schemas.

As shown here, it is an editor with various options. The data is returned as HTML, which can cover many use cases on its own.

However, I personally rarely use the retrieved HTML data as-is; instead, I often process it on the server before including it in the client build.

Since it felt like I was writing the same processing logic every time, I generalized the process to some extent, created a library, and published it. Please feel free to use it.

https://www.npmjs.com/package/microcms-richedit-processer

Features

This section introduces the features of this library.

About HTML data processing functions

  • img tag
    • Lazy loading (currently supports class names for the lazysizes lazy loading library)
    • Responsive image support (technology to deliver optimal images according to window width using srcSet and sizes attributes)
    • Placeholder image settings
    • Adding imgix parameters
    • Automatic setting of width and height attributes
  • iframe tag
    • Lazy loading (currently supports class names for the lazysizes lazy loading library)
    • Responsive support
  • code tag inside pre tag
    • Adding class names for syntax highlighting (currently supports highlight.js)
  • Common
    • Adding class names
    • Adding arbitrary attribute values

About automatic generation of other data from HTML

  • Table of Contents list creation

Usage

First, install it.

npm i microcms-richedit-processer
# yarn add microcms-richedit-processer

I will explain using Next.js here. If you are using another framework, please adapt it accordingly.

Since we want to process it at build time, we use getStaticProps.

import { GetStaticProps, NextPage } from "next";

import { createTableOfContents, processer } from "microcms-richedit-processer";

type Props = {
  body: string;
  toc: {
    id: string;
    text: string;
    name: string;
  }[];
};

export const getStaticProps: GetStaticProps<Props> = async () => {
  const { contents } = await fetch(
    "https://{SERVICE_ID}.microcms.io/api/v1/{ENDPOINT}",
    {
      headers: {
        "X-API-KEY": "{API_KEY}",
      },
    }
  ).then((res) => res.json());

  // Assuming HTML data is retrieved in contents.body.
  return {
    props: {
      body: await processer(contents.body),
      // To pass options
      // body: processer(contents.body, {}),
      toc: createTableOfContents(contents.body),
      // To pass options
      // toc: createTableOfContents(contents.body, {}),
    },
  };
};

Two functions are imported.

processer is responsible for processing HTML data.
createTableOfContents is responsible for creating a table of contents list.

Just use the string data processed this way for rendering on the client side!

Operational Details

The following sections explain each operation in detail.

Common Options

I will introduce common options that can be specified for each element, using the img tag as an example.

Adding class names

processer(content, { img: { addClassName: ["class01", "class02"] } });
<img
-  src="https://sample.com/image.png"
   alt
+  width="Width of the image is automatically inserted"
+  height="Height of the image is automatically inserted"
+  class="class01 class02 lazyload"
+  data-src="https://sample.com/image.png?auto=format"
+  data-srcset="https://sample.com/image.png?auto=format&w=640 640w, https://sample.com/image.png?auto=format&w=750 750w, https://sample.com/image.png?auto=format&w=828 828w, https://sample.com/image.png?auto=format&w=1080 1080w, https://sample.com/image.png?auto=format&w=1200 1200w, https://sample.com/image.png?auto=format&w=1920 1920w, https://sample.com/image.png?auto=format&w=2048 2048w, https://sample.com/image.png?auto=format&w=3840 3840w"
+  data-sizes="100vw"
/>

Adding arbitrary attribute values

processer(content, {
  img: { addAttributes: { "aria-label": "sampleLabel", "data-id": "dataid" } },
});
<img
-  src="https://sample.com/image.png"
   alt
+  aria-label="sampleLabel"
+  data-id="dataid"
+  width="Width of the image is automatically inserted"
+  height="Height of the image is automatically inserted"
+  class="lazyload"
+  data-src="https://sample.com/image.png?auto=format"
+  data-srcset="https://sample.com/image.png?auto=format&w=640 640w, https://sample.com/image.png?auto=format&w=750 750w, https://sample.com/image.png?auto=format&w=828 828w, https://sample.com/image.png?auto=format&w=1080 1080w, https://sample.com/image.png?auto=format&w=1200 1200w, https://sample.com/image.png?auto=format&w=1920 1920w, https://sample.com/image.png?auto=format&w=2048 2048w, https://sample.com/image.png?auto=format&w=3840 3840w"
+  data-sizes="100vw"
/>

img tag processing

Next, I'll introduce the processing of img tags.
First, the default behavior.

<img
-  src="https://sample.com/image.png"
   alt
+  data-src="https://sample.com/image.png?auto=format"
+  data-srcset="https://sample.com/image.png?auto=format&w=640 640w, https://sample.com/image.png?auto=format&w=750 750w, https://sample.com/image.png?auto=format&w=828 828w, https://sample.com/image.png?auto=format&w=1080 1080w, https://sample.com/image.png?auto=format&w=1200 1200w, https://sample.com/image.png?auto=format&w=1920 1920w, https://sample.com/image.png?auto=format&w=2048 2048w, https://sample.com/image.png?auto=format&w=3840 3840w"
+  data-sizes="100vw"
+  width="Width of the image is automatically inserted"
+  height="Height of the image is automatically inserted"
+  class="lazyload"
/>

As shown above, the value specified in the src attribute is converted to a data-src attribute, and the auto parameter is set to format.
Details of the auto parameter
The width and height are then retrieved from the image URL specified in the src attribute and assigned.
Please rest assured that if you specify image sizes within the rich editor, those values will be set in the width and height attributes.

For responsive image settings, the default array of values [640, 750, 828, 1080, 1200, 1920, 2048, 3840] is used.
Based on these, URLs for each image size are created, allowing you to deliver images optimized for various device sizes without having to think about it.

Responsive Image Options

You can change the device sizes referenced when generating srcset. You can also change the sizes attribute.

processer(content, {
  img: { deviceSizes: [640, 1280], sizes: "(min-width: 640px) 1000px, 100vw" },
});
<img
-  src="https://sample.com/image.png"
   alt
+  data-src="https://sample.com/image.png?auto=format"
+  data-srcset="https://sample.com/image.png?auto=format&w=640 640w, https://sample.com/image.png?auto=format&w=1280 1280w"
+  data-sizes="(min-width: 640px) 1000px, 100vw"
+  width="Width of the image is automatically inserted"
+  height="Height of the image is automatically inserted"
+  class="class01 class02 lazyload"
/>

imgix Parameters

In microCMS, you can use the imgix API when retrieving images. This option allows you to set the parameters to be specified at that time.

It uses ts-imgix internally, and since editor completion works, you can specify them with a very low risk of typos.

processer(content, { img: { parameters: { q: 50, w: 800, h: 600 } } });
<img
-  src="https://sample.com/image.png"
  alt
+  width="800"
+  height="600"
+  data-src="https://sample.com/image.png?auto=format&w=800&h=600&q=50"
+  data-srcset="https://sample.com/image.png?auto=format&w=640&h=480&q=50 640w, https://sample.com/image.png?auto=format&w=750&h=563&q=50 750w, https://sample.com/image.png?auto=format&w=828&h=621&q=50 828w, https://sample.com/image.png?auto=format&w=1080&h=810&q=50 1080w, https://sample.com/image.png?auto=format&w=1200&h=900&q=50 1200w, https://sample.com/image.png?auto=format&w=1920&h=1440&q=50 1920w, https://sample.com/image.png?auto=format&w=2048&h=1536&q=50 2048w, https://sample.com/image.png?auto=format&w=3840&h=2880&q=50 3840w"
+  data-sizes="100vw"
+  class="lazyload"
/>

Placeholder Image Settings

You can choose whether to set an alternative image to be displayed until the image is loaded. By combining this feature with the blur-up plugin of lazysizes, you can reduce stress even for users with slow internet connections.

https://github.com/aFarkas/lazysizes/tree/master/plugins/blur-up

processer(content, { img: { placeholder: true } });
<img
-  src="https://sample.com/image.png"
   alt
+  width="Width of the image is automatically inserted"
+  height="Height of the image is automatically inserted"
+  data-srcset="https://sample.com/image.png?auto=format&w=640 640w, https://sample.com/image.png?auto=format&w=750 750w, https://sample.com/image.png?auto=format&w=828 828w, https://sample.com/image.png?auto=format&w=1080 1080w, https://sample.com/image.png?auto=format&w=1200 1200w, https://sample.com/image.png?auto=format&w=1920 1920w, https://sample.com/image.png?auto=format&w=2048 2048w, https://sample.com/image.png?auto=format&w=3840 3840w"
+  data-lowsrc="https://sample.com/image.png?w=50&q=30&blur=10"
+  data-sizes="100vw"
+  class="lazyload"
/>'

Disabling Lazy Loading

Since it automatically configures responsive image settings and the width and height attributes, this library is effective even if you do not use lazy loading.

processer(content, { img: { lazy: false } });

After processing

<img
-  src="https://sample.com/image.png"
+  src="https://sample.com/image.png?auto=format"
+  srcset="https://sample.com/image.png?auto=format&w=640 640w, https://sample.com/image.png?auto=format&w=750 750w, https://sample.com/image.png?auto=format&w=828 828w, https://sample.com/image.png?auto=format&w=1080 1080w, https://sample.com/image.png?auto=format&w=1200 1200w, https://sample.com/image.png?auto=format&w=1920 1920w, https://sample.com/image.png?auto=format&w=2048 2048w, https://sample.com/image.png?auto=format&w=3840 3840w"
+  sizes="100vw"
   alt
+  width="Width of the image is automatically inserted"
+  height="Height of the image is automatically inserted"
/>

iframe tag processing

By default, it determines the aspect ratio based on the width and height of the iframe and makes the size responsive to the width of the parent element.

+ <div style="position: relative; padding-bottom: calc(480 / 854 * 100%);">
    <iframe
-    class="embedly-embed"
+    class="embedly-embed lazyload"
-    src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FO1bhZgkC4Gw%3Ffeature%3Doembed&display_name=YouTube&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DO1bhZgkC4Gw&image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FO1bhZgkC4Gw%2Fhqdefault.jpg&key=d640a20a3b02484e94b4b0a08440f627&type=text%2Fhtml&schema=youtube"
     width="854"
     height="480"
     scrolling="no"
     title="YouTube embed"
     frameborder="0"
     allow="autoplay; fullscreen"
     allowfullscreen="true"
+    data-src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FO1bhZgkC4Gw%3Ffeature%3Doembed&display_name=YouTube&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DO1bhZgkC4Gw&image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FO1bhZgkC4Gw%2Fhqdefault.jpg&key=d640a20a3b02484e94b4b0a08440f627&type=text%2Fhtml&schema=youtube"
+    style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;"
   ></iframe>
+ </div>

Specifying width and height

If you want to overwrite the width and height attributes of the iframe set in microCMS, please use this option. It will make it responsive based on those values.

processer(content, { iframe: { width: 960, height: 640 } });
+ <div style="position: relative; padding-bottom: calc(640 / 960 * 100%);">
    <iframe
-    class="embedly-embed"
+    class="embedly-embed lazyload"
-    src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FO1bhZgkC4Gw%3Ffeature%3Doembed&display_name=YouTube&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DO1bhZgkC4Gw&image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FO1bhZgkC4Gw%2Fhqdefault.jpg&key=d640a20a3b02484e94b4b0a08440f627&type=text%2Fhtml&schema=youtube"
-    width="854"
+    width="960"
-    height="480"
+    height="640"
     scrolling="no"
     title="YouTube embed"
     frameborder="0"
     allow="autoplay; fullscreen"
     allowfullscreen="true"
+    data-src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FO1bhZgkC4Gw%3Ffeature%3Doembed&display_name=YouTube&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DO1bhZgkC4Gw&image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FO1bhZgkC4Gw%2Fhqdefault.jpg&key=d640a20a3b02484e94b4b0a08440f627&type=text%2Fhtml&schema=youtube"
+    style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;"
   ></iframe>
+ </div>

Disabling lazy loading

Since it automatically makes the size responsive to the width of the parent element, this library is effective even if you do not use lazy loading.

processer(content, { iframe: { lazy: false } });
+ <div style="position: relative; padding-bottom: calc(480 / 854 * 100%);">
    <iframe
     class="embedly-embed"
     src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FO1bhZgkC4Gw%3Ffeature%3Doembed&display_name=YouTube&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DO1bhZgkC4Gw&image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FO1bhZgkC4Gw%2Fhqdefault.jpg&key=d640a20a3b02484e94b4b0a08440f627&type=text%2Fhtml&schema=youtube"
     width="854"
     height="480"
     scrolling="no"
     title="YouTube embed"
     frameborder="0"
     allow="autoplay; fullscreen"
     allowfullscreen="true"
+    style="position: absolute; width: 100%; height: 100%; top: 0; left: 0;"
   ></iframe>
+ </div>

Processing pre > code tags

This is off by default, so you can turn it on in the options. In that case, you need to install the library used for syntax highlighting as needed. (Currently, only highlight.js is supported.)

npm i highlight.js
# yarn add highlight.js
processer(content, { code: { enabled: true } });
<pre>
   <code>
-  import { AppProps } from &#x27;next&#x2F;app&#x27;\n\nconst MyApp = ({ Component, pageProps }: AppProps): JSX.Element =&gt; {\n  return &lt;Component {...pageProps} &#x2F;&gt;\n}\n\nexport default MyApp
+  <span class="hljs-keyword">import</span> { AppProps } from <span class="hljs-string">&#x27;next/app&#x27;</span>\n\n<span class="hljs-keyword">const</span> MyApp = ({ Component, pageProps }: AppProps): JSX.<span class="hljs-built_in">Element</span> =&gt; {\n  <span class="hljs-keyword">return</span> &lt;Component {...pageProps} /&gt;\n}\n\n<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> MyApp
   </code>
</pre>

Also, please bundle the theme CSS for highlighting yourself. Here is an example for your reference.

index.tsx
import "highlight.js/styles/github-dark.css";

import { GetStaticProps, NextPage } from 'next'
import { processer } from "microcms-richedit-processer";

type Props = {
  body: string;
};

export const getStaticProps: GetStaticProps<Props> = async () => {
  const { contents } = await fetch(
    "https://{SERVICE_ID}.microcms.io/api/v1/{ENDPOINT}",
    {
      headers: {
        "X-API-KEY": "{API_KEY}",
      },
    }
  ).then((res) => res.json());

  return {
    props: {
      body: await processer(contents.body, {
        code: { enabled: true }
      }),
    },
  };
};

const IndexPage: NextPage<Props> = ({ body }) => {
  return (
    <>
      <style global jsx>{`
      .content pre > code {
        display: block;
        padding: 1rem;
      }
      `}</style>
      <div className="content" dangerouslySetInnerHTML={{ __html: body }} />
    </>
  )
}

Default Behavior of createTableOfContents

I'll introduce the behavior of createTableOfContents, which creates a table of contents list.
The created list follows the same format as the table of contents list introduced on the official microCMS blog.
https://blog.microcms.io/create-table-of-contents

Source HTML data

<h1 id="h98a35185af">What is Lorem Ipsum?</h1>
<p>
  Lorem Ipsum is simply dummy text of the printing and typesetting industry.
</p>
<h2 id="hf76e6834d0">Where does it come from?</h2>
<p>Contrary to popular belief, Lorem Ipsum is not simply random text.</p>
<h3 id="h1c03416cd7">Why do we use it?</h3>
<p>
  It is a long established fact that a reader will be distracted by the readable
  content of a page when looking at its layout.
</p>
<h4 id="rdAK6TEAQqx">Where can I get some?</h4>

Generated list

[
  { id: "h98a35185af", text: "What is Lorem Ipsum?", name: "h1" },
  { id: "hf76e6834d0", text: "Where does it come from?", name: "h2" },
  { id: "h1c03416cd7", text: "Why do we use it?", name: "h3" },
];

Behavior of createTableOfContents with Options

Changing the heading tags used for the table of contents

createTableOfContents(content, { tags: "h2, h4" });
[
  { id: "hf76e6834d0", text: "Where does it come from?", name: "h2" },
  { id: "rdAK6TEAQqx", text: "Where can I get some?", name: "h4" },
];

Disabling the name key

createTableOfContents(content, { dataForName: false });
[
  { id: "h98a35185af", text: "What is Lorem Ipsum?" },
  { id: "hf76e6834d0", text: "Where does it come from?" },
  { id: "h1c03416cd7", text: "Why do we use it?" },
];

Currently, this option only supports tagName and false, but I plan to add more options if microCMS updates its heading tags with more semantic features in the future.

Conclusion

Processing HTML data is often a surprisingly tedious task when building Jamstack sites. By abstracting these details into a library, I believe it makes the source code much cleaner!

Please give it a try!

URLs

https://www.npmjs.com/package/microcms-richedit-processer
https://github.com/dc7290/microcms-richedit-processer#readme

Side note: a small regret

The name of the library I created this time is

microcms-richedit-processer

However, I realized that semantically it might be "processor" instead of "processer"...

But by the time I noticed, it was already too late. Well, the meaning gets across, so I guess it's fine.

Discussion