(ゴリ押しで)microCMSにテキストカウンターを実装してみた

プロ�フィール画像

柴田俊也

2024年12月14日

リンクをコピーXでシェアするfacebookでシェアする

この記事は「BEMALab アドベントカレンダー 2024Open in new tab」14日目の記事です。

はじめに

みなさんこんにちは。メンバーズルーツカンパニーの柴田です。

コンテンツ管理システム(CMS)の機能は多様化していますが、その中でもテキストコンテンツを扱う際に、文字数をカウントする機能は非常に重要ではないでしょうか。
特に、SEOやコンテンツのボリュームを管理する上で、ライターやコンテンツ制作チームにとって役立つ機能かと思います。

そこで今回は、microCMSにリッチテキストエディタで入力された文字数をカウントする機能を実装してみます。
タイトルにある通り、ゴリ押しで実装をしているため、リアルタイムでの文字数確認はできないことをご承知おきください。

制作物にあたっての注意点

リアルタイム確認の制約
この実装では文字数をリアルタイムで確認できません。確認するには公開保存後に再読み込みが必要です。ただし、APIの設定によっては下書き状態でも文字数確認が可能です。

UUIDによる識別の注意
特定コンテンツを識別するためUUIDを使用していますが、管理が不適切だと別の記事の文字数を参照してしまう可能性があります。

API制限への配慮
microCMSのAPIにはリクエスト制限があるため、大量のデータを扱う場合は注意が必要です。

APIキーの安全な管理
今回、ローカル確認のためソースコードにAPIキーを直接記載していますが、サーバーにアップロードする際は外部公開を防ぐ対策が必要です。

文字数カウンター機能を実装する方法

実装環境

今回はフロントエンドフレームワークであるAstroを使って実装します。
外部ライブラリにはmicroCMSの拡張フィールド用SDKである microcms-field-extension-apiOpen in new tab と、microCMSのSDKである microcms-js-sdkOpen in new tab 、そして正確な文字数をカウントするために runes2Open in new tab を使用します。

動作確認についてですが、microCMSの拡張フィールドはlocalhostを指定することが可能なので、ローカル上で動かしているものをmicroCMSの拡張フィールドに設定して動作確認を行います。

コードの実装

以下が、microCMSの拡張フィールドを用いた文字数カウンターの実装コードです。


<html>

  <head>

    <meta charset="UTF-8" />

    <meta name="robots" content="noindex" />

  </head>

  <body>

    <div>

      <div>

        <p>

          <span class="js-text-count">0</span> 文字

        </p>

        <button class="js-reload">再読み込み</button>

      </div>

    </div>

  </body>

</html>


<script>

  import { setupFieldExtension, sendFieldExtensionMessage } from 'microcms-field-extension-api';

  import { createClient } from 'microcms-js-sdk';

  import { runes } from 'runes2';


  type DataType = {

    uuid: string;

  };


  type PostType = {

    id: string;

    createdAt?: string;

    updatedAt?: string;

    contents: string;

    counter?: DataType;

  };


  function countText() {

    const apiKey = 'EF000h2DLDg2OzkqogK1iETlFOAGs9gbT6mY';

    const serviceDomain = 'shibata-portalsite';

    const origin = `https://${serviceDomain}.microcms.io`;

    const endpoint = 'news';

    const options = {

      origin,

      height: 100,

      width: '100%'

    };

    

    // セットアップ

    setupFieldExtension({

      ...options,

      onDefaultData: async (message) => {

        const articleUuid = message.data.message?.data.uuid ?? null;

        if (articleUuid) {

          const targetArticle = await getArticle(articleUuid);

          if (targetArticle) {

            const count = await countCharacters(targetArticle);

            setCounterText(count);

          } else {

            console.error('記事が見つかりませんでした');

            setCounterText('ERROR');

          }

        } else {

          // UUIDが登録されていなければ登録する

          const uuid = crypto.randomUUID();

          sendFieldExtensionMessage(

            {

              id: message.data.id,

              message: {

                data: {

                  uuid

                }

              }

            },

            origin

          );

        }

      }

    });


    // 記事一覧から該当する記事を取得

    async function getArticle(uuid: string): Promise<PostType | undefined> {

      const client = createClient({

        apiKey,

        serviceDomain

      });


      try {

        const articles = await client.getList<PostType>({

          endpoint

        });


        return articles.contents.find((article) => {

          return article.counter && article.counter.uuid === uuid;

        });

      } catch (error: unknown) {

        console.error(error);


        return undefined;

      }

    }


    // 文字数をカウント

    async function countCharacters(target: PostType): Promise<string> {

      try {

        const textElement = new DOMParser().parseFromString(target.contents, 'text/html').body;

        const count = runes(textElement.innerText).length;


        return count.toString();

      } catch (error: unknown) {

        console.error(error);


        return 'ERROR';

      }

    }


    // HTMLに文字数をセット

    function setCounterText(count: string) {

      const counterElement = document.querySelector<HTMLSpanElement>('.js-text-count');

      if (!counterElement) return;


      counterElement.innerText = count;

    }

  }


  countText();


  // 再読み込みボタン

  const reloadButton = document.querySelector<HTMLButtonElement>('.js-reload');

  reloadButton?.addEventListener('click', () => {

    window.location.reload();

  });

</script>

テキストカウンターのコード実装と解説

HTML部分

HTML部分では、文字数を表示するための要素と再読み込みボタンを配置しています。文字数はJavaScriptで動的に更新されるため、初期値は0としています。

JavaScript部分

JavaScript部分では、microCMSのAPIを利用してリッチテキストフィールドの文字数をカウントする処理を行っています。
主な処理について少しだけ解説します。

UUIDの生成
各記事に対して一意のUUIDを生成し、設定します。

// セットアップ

setupFieldExtension({

  ...options,

  onDefaultData: async (message) => {

    const articleUuid = message.data.message?.data.uuid ?? null;

    if (articleUuid) {

      const targetArticle = await getArticle(articleUuid);

      if (targetArticle) {

        const count = await countCharacters(targetArticle);

        setCounterText(count);

      } else {

        console.error('記事が見つかりませんでした');

        setCounterText('ERROR');

      }

    } else {

      // UUIDが登録されていなければ登録する

      const uuid = crypto.randomUUID();

      sendFieldExtensionMessage(

        {

          id: message.data.id,

          message: {

            data: {

              uuid

            }

          }

        },

        origin

      );

    }

  }

});

このUUIDが重要で、UUIDを設定した後、記事一覧を取得し該当するUUIDを見つけて本文を読み込みます。
その後、読み込んだ本文から文字数をカウントするという処理の流れになっているため、UUIDを正しく設定する必要があります。
4行目にある onDefaultData は、ページ(拡張フィールド)が読み込まれたときのコールバックを定義するものです。
ここで、拡張フィールド自身に設定されたUUIDがあれば文字数をカウントする関数(後ほど解説します)である countCharacters にUUIDを渡し、なければUUIDを生成して拡張フィールドの値として生成したUUIDをmicroCMS側に渡します。

APIとの通信
 microcms-js-sdk を利用して、microCMSから記事データを取得します。

// 記事一覧から該当する記事を取得

async function getArticle(uuid: string): Promise<PostType | undefined> {

  const client = createClient({

    apiKey,

    serviceDomain

  });


  try {

    const articles = await client.getList<PostType>({

      endpoint

    });


    return articles.contents.find((article) => {

      return article.counter && article.counter.uuid === uuid;

    });

  } catch (error: unknown) {

    console.error(error);


    return undefined;

  }

}

onDefaultData で取得したUUIDと一致する記事があれば記事データを返し、そうでなければundefinedを返します。

文字数のカウント
取得した記事の内容を解析し、文字数を計算します。runes2 ライブラリを使用して、テキストの文字数を正確にカウントしています。

// 文字数をカウント

async function countCharacters(target: PostType): Promise<string> {

  try {

    const textElement = new DOMParser().parseFromString(target.contents, 'text/html').body;

    const count = runes(textElement.innerText).length;


    return count.toString();

  } catch (error: unknown) {

    console.error(error);


    return 'ERROR';

  }

}

ポイントとしては、microCMSから渡されるリッチテキストはHTMLのタグ込みで string になっているので一度 HTMLElement に変換してから innerText を取得し、文字数をカウントしています。
この方法により、リッチテキストから純粋な本文だけを抽出し、文字数を正確にカウントすることができます。

microCMSの準備

  1. 設定したいコンテンツAPIのAPI設定からAPIスキーマを開き、「フィールドを追加」を選択します

  1. フィールドIDと表示名を入力したら赤枠で囲まれているプルダウンを選択します

  1. APIスキーマの種類から「拡張フィールド」を選択します
  1. npm run dev で起動したサーバーのURLを設定します(今回は9000番のポートで起動しています)
  1. あとは決定後、「変更する」ボタンを押してAPIスキーマの設定は完了です

動作確認

新規で記事を作成するために記事編集画面を開くと、ちゃんとテキストカウンターが表示されています。

コンテンツに何か文字を書いて、公開ボタンを押して記事を公開します。

すると、以下のようにテキストカウンターが本文からカウントした文字数を表示してくれました!

一度記事の公開が終わった後は、記事を更新するたびに「公開を押す」→「(テキストカウンターの)再読み込みボタンを押す」という流れで文字数を更新することができます。

下書き記事でテキストカウンターを使用したい場合は、APIキーの設定で「下書きコンテンツの全取得」にチェックを入れると、下書き状態の記事でも文字数をカウントすることができます。

おわりに

今回は、microCMSにテキストカウンターを実装する方法について紹介しました。標準機能としては提供されていない文字数カウント機能を、拡張フィールドを利用して実現しました。この実装にはいくつかの制約がありますが、それでもコンテンツ制作者にとっては便利な機能になると思います。

UUIDを設定して特定の記事に対して処理を行うため、テキストカウンターに限らずさまざまな活用方法があると思います。
ぜひこの実装を参考にして、より良いmicroCMSライフを送ってください!

最後までお読みいただき、ありがとうございました!

この記事を書いた人

柴田俊也
柴田俊也
2020年にメンバーズキャリア(現:メンバーズ)に新卒入社。デザインコーダーとして3年ほど経験し、フロントエンドエンジニアに転向。現在は企業の常駐型内製化支援の業務を行なっています。
ページトップへ戻る