Contentful App Framework チュートリアル検証 - Create a custom app 編

Contentful App Framework チュートリアル検証 - Create a custom app 編

Contentful の公式チュートリアル「Create a custom app」を実際に実装し、「Word count」および「Reading time」表示機能を持つ Custom App を開発しました。実装過程で発生した Rich Text フィールドの型処理、TypeScript エラーの解決、設定ミスによるトラブルなど、公式ドキュメントでは言及されていない実践的な課題とその解決手法を体系的に記録します。

はじめに

Contentful の公式チュートリアル の 「Create a custom app」 を実際に実行したログをまとめました。

Custom App 動作イメージ

Contentful とは

Contentful は、コンテンツを API 経由で配信するヘッドレス CMS (Content Management System) です。従来の CMS と異なり、フロントエンド表示をコンテンツから分離することで、 Web サイトや生成 AI に学習させるナレッジ用途など柔軟にコンテンツを利用できるようになります。

Contentful の Custom App とは

Contentful Custom App は、 Contentful の管理画面内で動作するカスタム機能を開発するための仕組みです。

活用例

  • エントリーの文字数計算
  • Cloudinary など外部 API との連携

Custom App は React ベースで開発します。 Contentful App Framework を使用して管理画面の様々な場所 (例: サイドバー、フィールドエディタ、ダイアログ、 etc...) に配置できます。

対象読者

  • Contentful Custom App の開発に興味がある
  • 公式の英語チュートリアルを日本語で理解したい
  • 実際やってみたときに遭遇する問題と解決策を事前に知りたい
  • Contentful の拡張性について知りたい

参考

検証

環境構築

まず、 Contentful App Framework のプロジェクトを作成します。 Template/Example の選択肢が表示されたら Template を選択します。プログラミング言語は、今回の検証では TypeScript を選択しました。

npx create-contentful-app my-first-app

ローカル開発サーバーの起動

プロジェクト作成後、開発サーバーを起動します。

cd my-first-app
npm run start

http://localhost:3000 にブラウザでアクセスすると、次の文章が表示されます。

ブラウザアクセス時の表示

App の実装

チュートリアルに従って、サイドバーにブログ記事の「Word count」と「Reading time」を表示するアプリを実装します。今回は、src/locations/Sidebar.tsx を以下のように編集します。

import React, { useState, useEffect } from 'react';
import { List, ListItem, Note } from '@contentful/f36-components';
import { useSDK } from '@contentful/react-apps-toolkit';
import { SidebarAppSDK } from '@contentful/app-sdk';

const CONTENT_FIELD_ID = 'body';
const WORDS_PER_MINUTE = 200;

const Sidebar = () => {
  const sdk = useSDK<SidebarAppSDK>();
  const contentField = sdk.entry.fields[CONTENT_FIELD_ID];
  const [blogText, setBlogText] = useState('');

  useEffect(() => {
    // フィールド値を文字列に変換する関数
    const processFieldValue = (value) => {
      if (value && typeof value === 'object' && value.nodeType === 'document') {
        // Rich Text の場合はテキストを抽出
        return extractTextFromRichText(value);
      } else if (typeof value === 'string') {
        return value;
      }
      return '';
    };

    // 初期値の設定
    const initialValue = contentField.getValue();
    setBlogText(processFieldValue(initialValue));

    // フィールド変更の監視
    const detach = contentField.onValueChanged((value) => {
      setBlogText(processFieldValue(value));
    });

    return () => detach();
  }, [contentField]);

  // Rich Text オブジェクトからプレーンテキストを抽出
  const extractTextFromRichText = (richTextDoc) => {
    if (!richTextDoc || !richTextDoc.content) return '';

    let text = '';

    const extractFromNode = (node) => {
      if (node.nodeType === 'text') {
        text += node.value + ' ';
      } else if (node.content && Array.isArray(node.content)) {
        node.content.forEach(extractFromNode);
      }
    };

    richTextDoc.content.forEach(extractFromNode);
    return text.trim();
  };

  // 単語数と読書時間を計算
  const readingTime = (text) => {
    if (!text || typeof text !== 'string') {
      return { words: 0, text: '0 min read' };
    }

    const wordCount = text.split(' ').filter(word => word.length > 0).length;
    const minutes = Math.ceil(wordCount / WORDS_PER_MINUTE);
    return {
      words: wordCount,
      text: `${minutes} min read`,
    };
  };

  const stats = readingTime(blogText);

  return (
      <>
        <Note style={{ marginBottom: '12px' }}>
          Metrics for your blog post:
          <List style={{ marginTop: '12px' }}>
            <ListItem>Word count: {stats.words}</ListItem>
            <ListItem>Reading time: {stats.text}</ListItem>
          </List>
        </Note>
      </>
  );
};

export default Sidebar;
トラブルシューティング: Rich Text 形式への対応

実装中に以下のエラーに遭遇しました。

Uncaught TypeError: text.split is not a function

原因は、 body フィールドが Rich Text 形式で設定されており、 contentField.getValue() が文字列ではなくオブジェクト構造を返していたことでした。

{
  "nodeType": "document",
  "data": {},
  "content": [
    {
      "nodeType": "paragraph",
      "data": {},
      "content": [
        {
          "nodeType": "text",
          "value": "実際のテキスト内容",
          "marks": [],
          "data": {}
        }
      ]
    }
  ]
}

このオブジェクトに対して text.split() を実行しようとしたため、エラーが発生していました。解決のため、 Rich Text オブジェクトから再帰的にテキストを抽出する extractTextFromRichText 関数を実装しました。

Contentful 管理画面での統合

ローカルでの実装が完了したら、実際に Contentful 管理画面で App を動作させるための設定を行います。

App の設定

  1. Contentful コンソールを開き、Apps > Custom apps > Manage app definitions から Create app を選択
    Create app ボタン
  2. App を設定
    • Name: 任意の名前 (例: Blog Post Metrics)
    • Frontend: http://localhost:3000
    • Locations: Entry sidebar を選択
      App の設定
  3. Save をクリックし保存

スペースへの App インストール

  1. 使用したいスペースに移動
  2. 上部メニューの Apps > Custom apps を選択
  3. 作成したアプリの Install を選択
    Install ボタン

Content Type への割り当て

  1. Content model を開き BlogPost Content Type を選択
  2. 左ペインメニューより Sidebar を選択
  3. Available items から Blog Post Metrics を見つけて + ボタンをクリック
    Custom App の追加
  4. Save をクリックして変更を保存
    Save ボタン

動作確認

設定完了後、実際に App が動作することを確認します。

  1. Content を開き BlogPost エントリーを作成または編集
  2. エントリー編集画面のサイドバーに「Blog Post Metrics」が表示されるのを確認
    動作確認

body の内容を編集すると、即座にメトリクスが再計算されて表示が更新されます。

トラブルシューティング

実際の検証の中で起きた問題とその解決方法について記載します。

リロード後 App が表示されなくなった

検証のため再インストールを行ったため、Content Type のサイドバー設定で Blog Post Metrics の表示設定が外れてしまっていたことが原因でした。Content model > BlogPost > Sidebar で再度追加したところ解決しました。

コードの変更が反映されない

実装したコードではなく「Hello Sidebar Component」が表示されました。localhost を停止してアプリの動作を確認したところエラー表示に変化したため、Contentful がリアルタイムで localhost:3000 を参照していることは確認できました。
エラー表示

最終的な原因としては、テストファイルである Sidebar.spec.tsx を編集していたことが原因でした。正しく Sidebar.tsx を編集したところ解決しました。

console.log

.tsx ファイルに console.log を仕込んでおくことで、ブラウザのデベロッパーツールの Console から動作検証を行うことが可能です。

    console.log('=== Rich Text Debug ===');
    console.log('Full Rich Text object:', JSON.stringify(richTextDoc, null, 2));
    console.log('Content array:', richTextDoc.content);
    console.log('======================');

デベロッパーツール

まとめと所感

Contentful App Framework は、管理画面の機能を自由度高く拡張できるツールであり、非常に簡単にカスタム機能を実装できることが分かりました。ただし、Rich Text フィールドの対応など実際の開発では公式チュートリアルに記載されていない課題に遭遇することがあります。問題に遭遇した際は、 console.log でのデバッグやエラーメッセージの確認を通じた、原因の切り分けが解決への近道です。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.