mcpField
C
canvas-data-fetching

canvas-data-fetching

github.com/drupal-canvas開発3225 回インストール6 週間前に更新
リポジトリ

canvas-data-fetching について

Drupal Canvas、Workbench、React ベースのヘッドレスプロジェクト向け開発スキルです。コンテキストフックや JSON:API による取得、クエリ結果とデシリアライズ後のデータ検証を扱います。

主な機能

  1. 01React コンテキストフックの利用
  2. 02SWR による非同期データ取得
  3. 03JSON:API による Drupal コンテンツ照会
  4. 04クエリ結果とデータ形状の検証
  5. 05関連エンティティとフィールド選択の処理

活用シーン

  1. 01React コンポーネントで Drupal コンテンツを読み込む
  2. 02コンテンツ一覧や詳細コンポーネントの構築
  3. 03JSON:API のフィールドやフィルターの調査
  4. 04旧式のデータ取得呼び出しの移行

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/drupal-canvas/skills --skill canvas-data-fetching

インストール後、Claude Code で/canvas-data-fetchingを入力して読み込まれたか確認します。

詳細

データの取得

プロジェクトの種類

このスキルを適用する前に、package.json に @drupal-canvas/headless、または @drupal-canvas/headless- で始まる依存関係があるか確認します。該当する依存関係がある場合、これは Canvas Headless コードベースです。まず canvas-headless を読んでください。ヘッドレスプロジェクトでは、SDK の fetchPage() でページツリーを取得し、リクエストを認識する getClient() でサーバー側のコンテンツを取得します。その際は、フレームワークに適したデータ読み込み方法を使用してください。ポータブルな React コンポーネントでは、drupal-canvas/react の useJsonApiClient() と、アプリケーションの同一オリジン SDK プロキシを使用します。以下の React パターンは、Drupal、Workbench、React ベースのヘッドレスフロントエンドで使用できます。ヘッドレスのサーバー側プリフェッチとフォールバックのルールについては、canvas-headless も参照してください。React を使用しないネイティブコンポーネントでは、React フックではなく、各フレームワークに適したデータ読み込み方法を使用します。該当する依存関係がない場合、これは Canvas でレンダリングされる React コードベースです。コンポーネントは React(index.jsx/.tsx)であり、このスキルの内容がすべてそのまま適用されます。プロジェクトの種類はこの2つのみです。

ポータブルな React ランタイム API

drupal-canvas/react の useJsonApiClient()、usePageContext()、useSiteContext() を使用します。Drupal アイランド、Workbench、設定済みのヘッドレス React レンダラーがプロバイダーを提供します。フックはコンテキストを読み取ります。レンダリングのたびにデータを取得したり、クライアントを作成したりするものではありません。Canvas ツリーの外部では、必要に応じて統合側で CanvasContextProvider、JsonApiClientProvider、またはその両方を提供する必要があります。

フックは、コンポーネントまたはカスタムフックの最上位で、return が発生する可能性のある箇所より前に、無条件で呼び出します。コンテキストを利用できない場合、各フックは null を返すことがあります。コンポーネントは同期処理のままにし、バックエンド URL、Drupal のグローバル変数、認証情報、プロキシ設定、環境分岐をコンポーネント内に含めないでください。ヘルパーにはデータまたはクライアントを渡し、通常のヘルパーからフックを呼び出さないでください。

getPageData()、getSiteData()、new JsonApiClient() は非推奨です。Drupal と Workbench では引き続き動作しますが、コンストラクターに明示的なバックエンド URL を渡した場合でも、それ以外では例外をスローします。新しいコードでは使用しないでください。getNodePath や sortMenu などの既存のユーティリティインポートは変更しないでください。

pull したコンポーネントの移行

canvas pull は、安全な getPageData() と getSiteData() の呼び出しをコンテキストフックに自動で書き換えられます。この機能を使用するには、接続先サイトの capabilities.contextHooks メタデータと、両方のフックをランタイム値としてエクスポートするインストール済みの drupal-canvas/react エントリの両方が必要です。バージョン番号だけでサポートの有無を推測しないでください。サポートを確認できない場合は、ランタイムでサポートされていないインポートを無理に追加するのではなく、報告された制限を確認してください。

移行はコンポーネントファイル単位で一括して行われます。安全でないフックの配置、識別子の競合、getter の戻り値の直接的な分割代入がある場合、ファイルは変更されないことがあります。クライアントの構築とヘルパーモジュールについては診断が表示されますが、自動変換は行われません。--skip-overwrite が指定されたファイルは変更されません。

pull の後は、変更されたソースと診断を確認してください。codemod は null ガードを追加せず、その後のアクセスが安全かどうかも検証しません。nullable な戻り値、フックの順序、型、レンダリング動作を確認してください。残りのクライアント呼び出しは手動で移行します。コンポーネント/カスタムフックの外部では、ヘルパーにデータまたはクライアントを渡すか、ヘッドレスのサーバーコードで page.context とリクエストを認識する getClient() を使用してください。アクセス制御を維持し、認証情報を決して公開しないでください。

ページコンテキストとサイトコンテキスト
import { usePageContext, useSiteContext } from 'drupal-canvas/react';

export default function PageHeader() {
  const page = usePageContext();
  const site = useSiteContext();
  if (!page || !site) return null;

  return (
    <header>
      <a href={site.branding.homeUrl}>{site.branding.siteName}</a>
      <h1>{page.pageTitle}</h1>
    </header>
  );
}

ページコンテキストには pageTitle、breadcrumbs、mainEntity が含まれます。ページコンテキストが存在する場合でも、mainEntity が null のことがあります。エンティティのメタデータ、翻訳、またはクエリで使用する UUID を利用する前に、null でないことを確認してください。サイトコンテキストには branding、baseUrl、themeAssets が含まれます。アセット URL が空の場合にも対応してください。アプリケーションが明示的に調整する場合を除き、Drupal が生成したナビゲーション URL をそのまま維持してください。

SWR を使ったデータ取得

移植可能な React コンポーネントで非同期データを取得するには、SWR を使用します。キャッシュ、再検証、簡潔なフックベースの API が利用できます。ページやサイトのコンテキストの読み取りに SWR は必要ありません。

import useSWR from 'swr';

const fetcher = (url) => fetch(url).then((res) => res.json());

export default function Profile() {
  const { data, error } = useSWR('https://my-site.com/api/user', fetcher);

  if (error) return <div>Failed to load</div>;
  if (!data) return <div>Loading...</div>;
  return <div>Hello, {data.name}!</div>;
}

JSON:API で Drupal のコンテンツを取得する

Drupal からコンテンツ(記事、イベント、その他のコンテンツタイプなど)を取得するには、drupal-canvas/react の useJsonApiClient() で設定済みのクライアントを読み込み、クエリの構築に DrupalJsonApiParams を使用します。クライアントが利用できない場合は、SWR キーに null を渡してください。異なるフィルター、言語、ページネーションで結果が共有されないよう、リソースとすべてのクエリ入力を含む安定したキーを使用します。

SWR が isLoading を報告しているというだけでデータを隠すのではなく、プリフェッチ済みのフォールバックデータを含む利用可能なデータを表示してください。通常の useSWR のレンダリングでは、サーバー上でフェッチャーは実行されず、完了も待機されません。データが埋め込まれた初期 HTML を生成するため、ヘッドレスアプリケーションはサーバーの getClient() でプリフェッチし、一致する SWR フォールバックキーを設定します。詳細は canvas-headless を参照してください。

重要: 最終的なコンポーネントコードでは、デフォルトのシリアライザーを有効にしたままにしてください。Canvas コンポーネントの実行時の契約は、未加工の JSON:API ドキュメントの形状ではなく、JsonApiClient が返すデシリアライズ済みの形状です。

重要: Workbench のモックに、JSON:API リソースのペイロードを捏造して設定しないでください。データを取得するコンポーネントは、ユーザーが静的でデータ取得を行わないプレビュー形状を明示的に求めない限り、Workbench では実際の読み込み中、空、またはエラーの状態を表示してください。

未加工の JSON:API とデシリアライズ済みの Canvas データ

data[0].attributes.title や data[0].relationships.field_image のように、未加工の JSON:API を前提としたコンポーネントロジックを書かないでください。JsonApiClient を使用するコンポーネントが受け取るのは、デシリアライズ済みのオブジェクトです。

  • 接続確認と、エンドポイントが大まかに存在するかどうかの確認に限り、通常の HTTP リクエストを使用します。
  • コンポーネントが実際に利用するデータの形状を確認するには、JsonApiClient を使用します。
  • デバッグのために未加工の JSON:API ドキュメントを調べる場合、それは補助的な診断用の表示として扱い、コンポーネントコードの信頼できる情報源にはしないでください。
  • 最終的なコンポーネントコードでシリアライザーを無効にしないでください。
すべての JSON:API リクエストが期待どおりの結果を返すことを検証する

新しいコンポーネント、リファクタリング、フィルターの変更、include の追加、ソートの変更、既存コンポーネント用の新しいクエリなど、生成する JSON:API リクエストは、これに対するレンダリングロジックを作成または変更する前に、実行して検証する必要があります。「正しそうに見える」という理由でクエリが正しいと思い込まないでください。クエリを作成して実行し、レスポンスが期待どおりであることを確認してください。

リクエストの検証が完了するのは、以下のチェックがすべて成功した場合に限ります。

  • 正常に実行されること。 HTTP エラー、JSON:API エラードキュメント、クライアント例外が発生しないこと。
  • 結果件数が期待どおりであること。 リストクエリは、その種類のコンテンツが存在する場合、空でないコレクションを返すこと。フィルタークエリはフィルターなしのクエリより少ない項目を返し、0 件になるのは実際にそうなることが期待される場合に限ること。単一リソースの取得では、null ではなくリソースが 1 つ返ること。
  • デシリアライズされたオブジェクトに、期待するフィールドが存在し、値が設定されていること — addFields 経由でリクエストしたフィールドも含みます。フィールドが欠落している場合や常に null の場合は調査が必要です。正当な任意値やアクセス制限と、誤ったクエリやフィールド名を区別してください。
  • include が実際の関連エンティティに解決されており、単なる参照ではないこと。 addInclude を使用した場合は、コンポーネントが読み取るデシリアライズ済みオブジェクト上で、リレーションシップが展開されていることを確認してください。
  • フィルターとソートが意図どおりに機能すること。 フィルターされた項目が実際にフィルター条件に一致していること、ソートされた項目が指定した順序になっていることを抜き取り確認してください。

いずれかのチェックに失敗した場合は、レンダリングロジックを変更する前に、クエリ、フィールド名、コンテンツモデル、アクセス/セッションの状態を調査してください。空のレスポンスや誤ったレスポンスを、オプショナルチェーン、フォールバック文字列、「UI では問題なさそう」という判断で取り繕わないでください。修正のたびにプローブを再実行し、レスポンスが期待どおりになるまで先に進まないでください。

次のセクションのプローブパターンを、これらのチェックを行う際の標準的な方法として使用してください。count: 0、keys: []、またはコンポーネントに必要なフィールドが欠けた形状を出力するプローブは、その結果が本当に期待どおりであり、その理由が説明されていない限り、問題ないという判断の根拠にはなりません。

ドラフトコレクションの読み取りでは、返された項目に作業コピーが展開されますが、コレクションのフィルタリング/ソートで作業コピーの値が使われるわけではなく、未公開のエンティティがすべて見つかるわけでもありません。生のレスポンスでは、作業コピーの展開は行われません。匿名プローブで検証できるのは公開データのみです。ドラフトの動作は、想定されているセッション対応の統合を通じて検証してください。DraftSessionError を、公開ユーザーとして再試行したり、静的なフォールバックコンテンツをドラフトの読み取り成功として扱ったりして隠さないでください。

コーディング前にデシリアライズ後の形状を調べる

レンダリングロジックを書く前に、コンポーネントで使用するものと同じ JsonApiClient 呼び出しと DrupalJsonApiParams クエリパターンを使った、その場限りの JavaScript プローブを実行してください。最初に返された項目を調べ、そのデシリアライズ後の形状に合わせてコンポーネントを実装してください。

この公開データプローブは Canvas ランタイムの外部で実行します。解決済みのバックエンド設定を使い、共有のフレームワーク非依存な createJsonApiClient() ファクトリーを使用してください。レガシーガードを回避するために、window、Drupal の設定、ランタイムマーカーを偽装しないでください。ヘッドレスのドラフトプローブでは、アプリケーションのサーバー統合にある、リクエストを考慮するサーバー用 getClient() を使用してください。最終的な React コンポーネントのコードでは、クライアントを明示的に構築せず、useJsonApiClient() を使用してください。

次のパターンに沿ったコマンドを使用してください。

node --input-type=module -e "
import { createJsonApiClient } from 'drupal-canvas/jsonapi-client';
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';

const describeShape = (value) => {
  if (Array.isArray(value)) {
    return value.length > 0 ? [describeShape(value[0])] : ['empty-array'];
  }
  if (value && typeof value === 'object') {
    return Object.fromEntries(
      Object.entries(value).map(([key, nestedValue]) => [key, describeShape(nestedValue)]),
    );
  }
  if (value === null) {
    return 'null';
  }
  return typeof value;
};

const client = createJsonApiClient({
  baseUrl: 'https://example.ddev.site',
  apiPrefix: 'jsonapi',
});
const queryString = new DrupalJsonApiParams()
  .addSort('created', 'DESC')
  .addFields('node--article', ['title', 'created', 'body', 'path'])
  .getQueryString();

const items = await client.getCollection('node--article', { queryString });
const first = items?.[0];

console.log('count:', items?.length ?? 0);
console.log('keys:', first ? Object.keys(first) : []);
console.log('shape:', JSON.stringify(first ? describeShape(first) : null, null, 2));
console.log(JSON.stringify(first, null, 2));
"

baseUrl には /jsonapi エンドポイントではなく、サイトのルートを渡してください。作成中のコンポーネントに合わせて、リソースタイプ、フィルター、include、ソート、フィールドを調整してください。ある形状のクエリを調べて、コンポーネントでは別のクエリを実装しないでください。

ローカルの HTTPS 開発環境でこのプローブが失敗した場合は、JSON:API クライアントやクエリが誤っていると判断する前に、Node がローカルの証明書チェーンを信頼しているかどうかを確認してください。

import { getNodePath } from 'drupal-canvas';
import { useJsonApiClient } from 'drupal-canvas/react';
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
import useSWR from 'swr';

const Articles = () => {
  const client = useJsonApiClient();
  const { data, error } = useSWR(
    client
      ? [
          'node--article',
          {
            queryString: new DrupalJsonApiParams()
              .addSort('created', 'DESC')
              .getQueryString(),
          },
        ]
      : null,
    ([type, options]) => client.getCollection(type, options),
  );

  if (error) return 'An error has occurred.';
  if (!data) return 'Loading...';
  return (
    <ul>
      {data.map((article) => (
        <li key={article.id}>
          <a href={getNodePath(article)}>{article.title}</a>
        </li>
      ))}
    </ul>
  );
};

export default Articles;
addInclude を使った関連データの取得

関連するエンティティ(画像、タクソノミータームなど)が必要な場合は、addInclude を使って、1 回のリクエストで取得します。

JSON:API レスポンスで循環参照を避けてください。 SWR はキャッシュデータの比較に 深い等価性チェックを使います。レスポンスに循環参照が含まれていると、 「再帰が深すぎます」というエラーが発生し、チェックに失敗します。

自己参照フィールドを含めないでください。 クエリ対象と同じエンティティタイプを参照する フィールド(記事クエリの field_related_articles など)は循環参照を引き起こします。記事 A が記事 B を参照し、 記事 B が記事 A を参照するためです。同じタイプの関連コンテンツが必要な場合は、 別のクエリで取得してください。

addFields を使ってレスポンスを制限してください。 必要なフィールドだけを必ず指定してください。 これによりパフォーマンスが向上し、循環参照の問題も回避しやすくなります。

const params = new DrupalJsonApiParams();
params.addSort('created', 'DESC');
params.addInclude(['field_category', 'field_image']);

// Limit fields for each entity type
params.addFields('node--article', [
  'title',
  'created',
  'field_category',
  'field_image',
]);
params.addFields('taxonomy_term--categories', ['name']);
params.addFields('file--file', ['uri', 'url']);

コンテンツ一覧コンポーネントの作成

コンテンツ項目の一覧を表示するコンポーネント(ニュース一覧、イベントカレンダー、 リソースライブラリなど)を作成する場合は、次のワークフローに従ってください。

セットアップの確認

JSON:API の調査やコンテンツタイプの確認を行う前に、ローカル環境のセットアップを確認します。

  1. コードを記述したり Drupal を調査したりする前に、Canvas の設定値を解決します。次の順に確認してください。
    • シェル環境変数
    • プロジェクトルートの .env
    • ~/.canvasrc
  2. 有効な CANVAS_SITE_URL を特定します。
  3. プロジェクトの統合機能を使って JSON:API エンドポイントを解決します。検出されたプレフィックスについては、公開されている /canvas/api/v0/site-data のメタデータを読み取ってください。/jsonapi と決めつけないでください。ヘッドレスプロジェクトでは、 CANVAS_JSONAPI_URL が検出結果を上書きします。検出に失敗した場合、SDK は設定済みのプレフィックスまたは CANVAS_JSONAPI_PREFIX を使い、それもない場合は jsonapi を使います。サイトのインストールパスは保持してください。 別の JSON:API サイトを使用する場合、言語プレフィックス付きリクエストについては SDK の CANVAS_JSONAPI_SITE_URL のガイダンスに従ってください。ヘッドレス専用の設定オプションが Workbench でサポートされるとは 決めつけないでください。
  4. 解決したサイトルートと JSON:API エンドポイントを記録します(認証情報は記録しないでください)。 単独のプローブでもこれらの値を使い、適切な場合は API エンドポイントを baseUrl に代入するのではなく、 ファクトリーの apiUrl オーバーライドを使います。
  5. CANVAS_SITE_URL が JSON:API エンドポイントではなく、サイトルートであることを確認します。 たとえば、https://example.ddev.site を使い、https://example.ddev.site/jsonapi は使わないでください。
  6. 解決した JSON:API エンドポイントに HTTP リクエストを送ります。成功とは HTTP 200 が返ることです。
  7. リクエストが成功したら、Drupal データの取得に進みます。
  8. リクエストが失敗した場合(または必須の値が不足している場合)、Drupal との接続を設定するかどうかをユーザーに尋ねます。
    • Drupal との接続を今すぐ設定する
    • Drupal から取得せず、静的コンテンツを使って続行する
  9. ユーザーが接続の設定を選んだ場合は、セットアップ手順を提示します。
    • CANVAS_SITE_URL=<their Drupal site root>
    • CANVAS_JSONAPI_PREFIX=jsonapi(任意。デフォルトは jsonapi)その後、 ユーザーがシェル環境変数、.env、または ~/.canvasrc を更新したと確認するまで待ち、リクエストを再試行する前に値をもう一度解決します。
  10. ユーザーが接続を設定しないことを選んだ場合は、静的コンテンツで続行します。
  11. 有効な CANVAS_SITE_URL と JSON:API エンドポイントが判明するまで、コンテンツタイプの検出、フィールドの確認、コンポーネントのコーディングを開始しないでください。
  12. 接続の問題を調査するために、Vite の設定(vite.config.*)を更新しないでください。 接続の問題はビルドツールの変更ではなく、正しい設定値と Drupal サイトの可用性によって解決する必要があります。
ステップ 1:一覧の構造を分析する

デザインを確認し、各一覧項目に必要なデータを把握します。

  • 表示するフィールド(タイトル、日付、画像、カテゴリなど)は何ですか?
  • 項目はどのように並べ替えられていますか(新しい順、アルファベット順など)?
  • フィルターやページネーションはありますか?
Step 2: コンテンツタイプを確認または作成依頼する

コードを書く前に、Drupal に適切なコンテンツタイプがあることを確認します。

  1. ローカルの Drupal サイトの JSON:API エンドポイント(Setup ゲートから解決された CANVAS_SITE_URL と JSON:API プレフィックスで設定)を確認し、必要な構造に一致するコンテンツタイプを探します。Setup ゲートを通過した後であれば、エンドポイントの検出に通常の HTTP リクエストを使用できます。

  2. 一致するコンテンツタイプがある場合はそれを使用し、利用可能なフィールドを記録します。

  3. コンポーネントで使用するものと同じリソースタイプおよびクエリパターンを使い、JsonApiClient 経由でサンプルレスポンスを確認します。単発のプローブコマンドを実行し、最初に返された項目を調べ、描画ロジックを書く前にデシリアライズ後のフィールド構造を検証します。

  4. 一致するコンテンツタイプがない場合は、処理を停止してユーザーに作成を依頼します。以下を提示します。

    • 推奨するコンテンツタイプ名
    • リストのデザインに基づく必須フィールド構造
Step 3: コンポーネントを構築する

JSON:API を使用してコンテンツを取得するコンテンツリストコンポーネントを作成します。コンテンツタイプに実際に存在し、JsonApiClient が返すデシリアライズ後のオブジェクトにも存在するフィールドのみを使用してください。生の JSON:API フィールドのネスト構造が実行時のデータ構造と一致すると想定しないでください。

フィルターの処理

リストにエンティティ参照フィールドに基づくフィルター(例:カテゴリや作成者による絞り込み)が含まれる場合:

  • フィルターの選択肢をハードコードしないでください。 フィルターの選択肢は JSON:API を使って動的に取得します。
  • 各フィルターで使用できる選択肢(例:ボキャブラリー内のすべてのタクソノミーターム)を取得し、そのデータを使ってフィルター UI に選択肢を表示します。

これにより、フィルターが Drupal 上の実際のコンテンツと常に同期され、新しい選択肢もコードを変更せずに自動的に表示されます。

ナビゲーション/メニューコンポーネント

ヘッダー、フッター、サイドバーなどのコンポーネントでは、Drupal のメニューリンクが必要になることがよくあります。二重実装を使用してください。Drupal メニューがある場合はそこから取得し、まだメニューが設定されていない場合は静的配列にフォールバックします。

これにより、コンポーネントはすぐに使用でき(ハードコードされたフォールバックを使用)、CMS エディターが該当するメニューを作成すると、Drupal で管理されるライブリンクに自動的に切り替わります。

import { sortMenu } from 'drupal-canvas';
import { useJsonApiClient } from 'drupal-canvas/react';
import useSWR from 'swr';

// Static fallback — always define this; it renders when no Drupal menu exists
const FALLBACK_LINKS = [
  { id: 'home', title: 'Home', url: '/' },
  { id: 'about', title: 'About', url: '/about' },
];

const Navigation = ({ menuName = 'main' }) => {
  const client = useJsonApiClient();
  const { data, error } = useSWR(
    client && menuName ? ['menu_items', menuName] : null,
    ([type, id]) => client.getResource(type, id),
  );

  // Do not disguise a rejected draft session as a successful static preview.
  if (error?.name === 'DraftSessionError') {
    return <div>Preview session expired. Reconnect the preview.</div>;
  }

  // Keep prefetched/live links visible during revalidation.
  const links = data ? Array.from(sortMenu(data)) : FALLBACK_LINKS;

  return (
    <nav>
      {links.map(({ id, title, url }) => (
        <a key={id} href={url}>
          {title}
        </a>
      ))}
    </nav>
  );
};

メニューコンポーネントのルール:

  • 常に代表的なリンクを含む FALLBACK_LINKS 定数を定義します。これにより、Workbench や Drupal メニューがまだ作成されていないサイトでも、コンポーネントを有効に活用できます。
  • menuName をプロパティとして公開し、component.yml に登録します。これにより、CMS エディターはコードを変更せずに使用する Drupal メニューを設定できます。
  • menuName = null の場合は取得を無効にし(SWR キーを null にする)、フォールバックを表示します。これは、純粋な静的プレビューに便利です。
  • ナビゲーションタイプのコンポーネントを構築した後、手動手順のまとめに、Drupal で該当するメニューを /admin/structure/menu/add に作成するようユーザーに案内する注記を含めます。

menuName の component.yml の例:

props:
  properties:
    menuName:
      title: Menu name
      type: string
      examples:
        - main
        - footer

コメント