Instagram グラフ API で投稿画像(タイムライン)を一覧表示

ホームページにインスタグラムのタイムライン(一覧)を埋め込む方法について。

Instagram グラフ API を使ってインスタグラムで投稿した画像やビデオの一覧(タイムライン)をプラグインを使わずに Web ページに表示する方法です。

アクセストークンと Instagram ビジネスアカウント ID の取得方法や取得したデータを使って PHP や JavaScript で投稿の画像やいいねの数の表示方法、 WordPress に記述する方法など。

2024年7月28日更新

現時点での最新のバージョン(v20.0)に合わせて書き換えました。また、ボタンを表示して追加で投稿を読み込む方法や WordPress で表示する方法も追加しました。

作成日:2021年11月28日

関連ページ:WordPress インスタ表示用カスタムブロックの作成

概要

インスタグラムで投稿した画像などのメディアを取得してウェブに表示するには「Facebook for Developers」で提供されている Web API を使います。

以下では Instagram グラフ(Graph)API を利用してインスタグラムの一覧をWebページに表示します。

Instagram グラフ API を利用するにはアクセストークンと Instagram ビジネスアカウント ID を取得する必要がありますが、この部分が多少手間がかかります。

この時点のグラフAPIバージョンは v20.0 です。

必要なもの

  • Instagram のアカウント
  • Facebook のアカウントとページ
  • スマートフォンやタブレット(インスタグラムのアプリの操作)

Instagram グラフ API を利用するには Instagram のアカウントの他に Facebook のアカウントも必要になります。これは Instagram グラフ API が Facebook の開発者ツール「Facebook for Developers」に組み込まれているためです。

大まかな流れ

必要なアカウントをセットアップし、Facebook ページを作成して Instagram のアカウントをリンクさせます。そして Facebook 開発者ツール(Facebook for Developers)で Facebook アプリを作成し、アクセストークンや Instagram 表示に必要な ID を取得します。

取得したアクセストークンと ID を使って Instagram の Web API から画像などのデータを取得して Web ページに表示します。以下の例ではデータの取得や Web ページの表示には PHP を使います。

  1. Instagram プロアカウントへ切り替え(インスタグラムのアプリで実行)
  2. Facebook 開発者アカウントの作成(まだ作成していない場合)
  3. Facebook ページの作成
    • Facebook ページと Instagram プロアカウントをリンク(関連付け)
  4. Facebook アプリの作成(Facebook for Developers)
  5. Instagram グラフ API で使用するアクセストークンの取得(Facebook for Developers)
    • 短期アクセストークン(有効期限1時間)の取得
    • 長期アクセストークン(有効期限2ヶ月)の取得
    • 無期限のアクセストークンの取得
  6. Instagram ビジネスアカウント ID の取得
  7. Instagram グラフ API を使ってインスタグラムの一覧のデータを取得して表示(PHP・JS)

無期限のアクセストークンの取得

Instagram グラフ API で使用する無期限(有効期限なし)のアクセストークンを取得するには段階的に取得する必要があります。

試してみたところ、グラフ API エクスプローラを使って短期アクセストークンを取得後、アクセストークンデバッガーを使って延長するのが比較的簡単でした。

参考ドキュメント(Facebook for Developers ドキュメント)

インスタグラムのアカウントをプロアカウントに

Instagram グラフ API を利用するには(個人用アカウントの場合は)プロアカウント(無料)に切り替える必要があります。この作業はスマートフォンやタブレットのインスタグラムのアプリで行います。

但し、プロアカウントではアカウントを非公開にできなくなります。

インスタグラムのアプリにログインし、「設定」→「アカウント」の順に移動し一番下の「プロアカウントに切り替える」を選択し、「OK」をタップします。

「クリエイター」か「ビジネス」の選択はどちらでも構いません。一度選択しても後からアカウントの画面の「アカウントタイプを切り替え」で変更可能です。

Facebook 開発者アカウントの作成

Instagram グラフ API は Facebook アプリを作成して利用します。

Facebook アプリの作成は Facebook の開発者ツール「Facebook for Developers」を使うので、開発者アカウント(無料)が必要です(Facebook アカウントを持っていることが前提)。

開発者アカウントを作成していない場合は Facebook for Developers にアクセスして作成します。

Facebook にログイン済みであれば、自分の Facebook アカウントが表示されるのでアカウントを作成するために「次へ」をクリックします。

電話番号(SMS)でアカウントの認証を行います(Facebook アカウントに電話番号を登録していない場合は電話番号の入力を求められます)。

続いてメールアドレスを確認します。

いずれか当てはまるものを選択すれば登録完了です。

Facebook ページの作成

Instagram グラフ API を利用するには Facebook ページと Instagram のアカウントをリンクさせる必要があるため、Facebook ページを作成します。

Facebook にログインします(個人用アカウントで大丈夫です)。

ログイン後、左側メニューから「ページ」をクリックします。

「+新しいページを作成」をクリックします。

必須項目の「ページ名」と「カテゴリ」を入力し「Facebookページを作成」をクリックします。ページ名には使えない言葉など決まりがあります(どのようなページ名が認められますか)

「ページの設定を完了しよう」と表示されるので、必要に応じて情報を入力し、「次へ」をクリックしていきます。

この例ではサンプルページなので何も入力せず、最後に「完了」をクリックしました。

ウェブサイトを追加

「ページを管理」では「基本データ」タブでインスタグラムで投稿した画像を表示するウェブサイトを追加します(後からこの「ページを管理」→「基本データ」タブで追加することもできます)。

後で Facebook アプリを作成する際に、ここで追加したウェブサイトのプライバシーポリシーページの URL を登録します。

Facebook アプリの作成

Facebook アプリを作成するには Facebook for Developers (Facebook の開発者ツール)にアクセスして右上の「マイアプリ」を選択します。

「アプリを作成」を選択します。すでに作成済みのアプリがあれば表示されます。

「現時点ではビジネスポートフォリオをリンクしない」のまま「次へ」をクリックします。

「その他」を選択して「次へ」をクリックします。

「アプリタイプを選択」では「ビジネス」を選択して「次へ」をクリックします。

アプリ名を入力し、連絡先メールアドレスを確認して、「アプリを作成」をクリックします。

ビジネスポートフォリオは任意ですが、必要に応じてビジネスポートフォリオを選択します。

また、表示名は任意の名前を付けられますが、Instagram や Insta などを含めるとエラーになります(日本語のインスタは大丈夫でした)。

パスワードの入力を求められるので入力して「送信」をクリックするとアプリが作成されます。

基本設定情報

作成した Facebook アプリの基本的な設定情報は、左側メニューの「設定」を展開して「ベーシック」で確認できます。

「アプリ ID」や「app secret」の値を確認することができます。「app secret」 は「表示」をクリックするとパスワードの入力を求められるので入力して表示します。

プライバシーポリシーのURL

上記設定ページの「プライバシーポリシーのURL」や「利用規約のURL」には設置先の該当するページの URL を入力して、ページ右下の「変更を保存」をクリックして保存します。

この設定は後からでも可能ですが、設定していない場合、アプリのモードを「開発」から「ライブ」に変更しようとすると、「アプリをライブで使用するには有効なプライバシーポリシー URL を入力する必要があります」と表示されます。

ライブモードへの切り替え

新しく作成されたすべてのアプリは、最初に開発モードになります。

アプリ開発が完了したらライブモードに切り替えることができますが、前述のプライバシーポリシーのURLに設定したサイトが Facebook のページに追加されている必要があります。

後から、サイトを Facebook ページに追加するには、作成した Facebook ページの「ページを管理」の「基本データ」タブで行います。

※ ライブモードへの切り替えは、アプリ開発が完了してから必要に応じて行います(開発モードでもインスタの画像を表示することはできます)。

Meta ドキュメント:アプリモード

Facebook アプリの削除

テスト用に作成したアプリなど、アプリを削除するには対象のアプリの右下のボタンをクリックして「アプリを削除」を選択します。

アクセストークンの取得

Instagram グラフ API を利用するにはアクセストークを発行して取得する必要があります。

アクセストークンは Facebook for Developers で発行できますが、無期限のアクセストークンを取得するには、短期アクセストークン(有効期限1時間)を取得後、有効期限2ヶ月の長期アクセストークンを取得し、その後無期限のアクセストークンを取得します。

参考(Facebook for Developers ドキュメント):

グラフ API エクスプローラ

アクセストークンの取得にはグラフAPIエクスプローラを使います。

グラフAPIエクスプローラはグラフ API へのクエリを作成して実行し、その応答を表示することができるツールで、自分でブラウザから API にクエリを送信するより便利です。

グラフAPIエクスプローラは Facebook for Developers の上部にある「ツール」→「グラフAPIエクスプローラ」でアクセスできます。または、https://developers.facebook.com/tools/explorer/ からもアクセスできます。

アクセスすると以下のような画面が表示されます。

コンポーネント 概要
クエリ文字列フィールド クエリを入力して右横の「送信」ボタンをクリックして実行することができます
応答ウィンドウ 送信したクエリに対する応答がこのウィンドウに表示されます
アクセストークンフィールド アクセストークンを取得すると、このフィールドに表示されます
アプリドロップダウン 対象の(作成した)アプリを選択します
アクセストークンドロップダウン 取得するトークンの種類を選択します。この例の場合、ユーザートークン。
許可追加ダウン 必要な許可を追加します

参考:グラフAPIエクスプローラガイド

短期ユーザーアクセストークンの取得

最初は有効期限1時間の短期ユーザーアクセストークンを取得します。

Facebook for Developers 上部の「ツール」から「グラフAPIエクスプローラ」を選択すると上記の画面が開きます。

右側の「Metaアプリ」に作成したアプリの名前(この例の場合はインスタフィード)が表示されていることを確認します(表示されていなければドロップダウンから選択します)。

初期状態ではおそらくアクセストークン発行ボタン(Generate Access Token)が無効になっているので、まずアクセス許可を追加します。

アクセス許可を追加

「許可を追加」をクリックすると、以下のような項目が表示され、クリックすると関連する許可(Permission)が表示されます。

ユーザートークンに付与する以下の許可を選択して追加します。

Events Groups Pages を展開
  • business_management
  • pages_manage_ads
  • pages_manage_metadata
  • pages_read_engagement
  • pages_read_user_content
  • pages_show_list
Other を展開
  • instagram_basic
  • instagram_manage_comments
  • instagram_manage_insights

以下はそれぞれの許可の概要です(詳細は:アクセス許可のリファレンス)。付与する許可は必要に応じて変更(追加・削除)します。

アクセス許可 説明
business_management ビジネスマネージャAPIを利用した読み取りや書き込みをアプリが行えるようになる
pages_manage_ads ページに関連付けられた広告をアプリが管理できるようになる
pages_manage_metadata アプリがページでのアクティビティに関するWebhooksを受け取るためにサブスクリプション登録したり、ページの設定をアップデートしたりできるようになる
pages_read_engagement ページに投稿されたコンテンツ(投稿、写真、動画、イベント)、フォロワーのデータやプロフィール写真、ページについてのメタデータやその他のインサイトをアプリが読み取れるようになる
pages_read_user_content アプリがページ上のユーザー作成コンテンツ(投稿、コメント、評価)を読み取り、ページ投稿のユーザーのコメントを削除できるようになる
pages_show_list 利用者が管理しているページのリストにアプリがアクセスできるようになる
instagram_basic アプリがInstagramアカウントのプロフィール情報やメディアを読み取れるようになる
instagram_manage_comments ページにリンクしたInstagramアカウントに代わって、アプリがコメントを作成する、削除する、非表示にすることができるようになる
instagram_manage_insights FacebookページにリンクされたInstagramアカウントのインサイトにアプリがアクセスできるようになる
public_profile 以前は自動的に付与されていたと思うのですが、現在は許可する項目にありません。
トークンを発行

許可を追加したら確認し、「Generate Access Token」をクリックしてトークンを発行します。

以下のようなアカウント切り替えのウィンドウが表示されたら、問題なければ「次へ」をクリックします。

確認のたのウィンドウが表示されるので、問題なければ「...として続行」をクリックします。

続いて以下のような確認ウィンドウが表示されるので、必要に応じてオプションを選択して「続行」をクリックします(以下の「インスタフィード」は作成したアプリの名前です)。

続いて以下のようなアクセス許可の確認ウィンドウが表示されるので、問題がなければ「保存」をクリックします。

続いて以下表示されるので「OK」をクリックします。

アクセストークンフィールドに発行されたトークンが表示されます。

1回目のアクセストークンの有効期間は1時間なので、1時間以内に2回目(有効期間2ヶ月)のアクセストークンを取得します。1時間以内に有効期間2ヶ月のアクセストークンを取得できない場合は再発行する必要があります。

取得したトークンの確認

表示されているアクセストークンの左にあるアイコン をクリックすると以下のような「アクセストークン情報」のウィンドウが表示されます。

アクセストークンデバッガーで延長

「アクセストークン情報」のウィンドウで「アクセストークンツールで開く」をクリックすると、アクセストークンデバッガーのページが表示されます。この画面ではトークンの詳細な情報を確認したり、アクセストークンを延長することができます。

以下の場合、トークンの有効期限が1時間以内であることが確認できます。

ページの下の方にある「アクセストークンを延長」をクリックしてトークンの有効期限を延長します。

パスワードを求められたら、パスワードを入力して送信します。

以下のように画面の下の部分に「この長期アクセストークンはxxxx年xx月xx日に期限切れとなります」と表示されるので、「デバッグ」をクリックします。

有効期限が「約2ヶ月以内」となっていれば延長成功です。

延長されたアクセストークン(長期ユーザーアクセストークン)の値をコピーします。

ユーザーアクセストークンを無期限に延長

グラフ API エクスプローラに戻り、コピーした延長されたアクセストークンの値を「アクセストークン」のフィールドにペーストし、左にあるアイコン をクリックします。

「アクセストークン情報」のウィンドウが開くので「アクセストークンツールで開く」をクリックします。

アクセストークンデバッガーのページが表示され、タイプが「User」で有効期限が「受け取らない」となっていればこのユーザーアクセストークンを無期限に延長できました。

この無期限に延長されたユーザーアクセストークンを使用するのでコピーして保管します。

無期限のページアクセストークン

もし、無期限のページアクセストークンが必要な場合はこのトークンの値を使って「長期ページアクセストークンの取得」を実行します。

Instagram ビジネスアカウント ID の取得

Instagram のメディアの表示に必要な Instagram ビジネスアカウント ID を取得します(ビジネスアカウントとはプロアカウントに切り替えた時点で発行されるアカウント)。

「ツール」→「グラフAPIエクスプローラ」でグラフ API エクスプローラを開き、クエリ文字列フィールド(入力欄)に me?fields=accounts{instagram_business_account} と入力して「送信」をクリックします。

"instagram_business_account" の下の "id" に Instagram ビジネスアカウント ID が表示されます。

※ この値も後で使用するのでコピーして保存しておきます。

インスタグラム投稿の一覧を出力

取得した Instagram ビジネスアカウント ID とアクセストークンを使って Instagram グラフ API からインスタグラムの投稿の一覧のデータを取得することができます。

グラフ API はブラウザー内で URL をリクエストして直接利用できます。

例えば、以下のような URL を作成してブラウザでアクセス(HTTP メソッドの GET でリクエスト)するとインスタグラム投稿(メディア)の JSON データが4件取得できます。

https://graph.facebook.com/v20.0/{instagram_business_account}?fields=media.limit(4){caption,media_url,thumbnail_url,permalink,like_count,comments_count,media_type}&access_token={user_access_token}

https://graph.facebook.com/ はグラフ API の URL(ルートエンドポイント)で、その後に API のバージョン(この時点では v20.0)を指定し、{instagram_business_account} には取得したビジネスアカウント ID を指定します。

? 以降はクエリ部分で、fields パラメーターを使用して media を指定し、.limit(4) で取得する結果を4件に制限し、必要なフィールド(caption や media_url など)をリスティングしています。

また、& access_token パラメータにアクセストークンを指定します。{user_access_token} の部分には取得したアクセストークンの値を指定します。

ブラウザには以下のような JSON 形式のレスポンス(応答)が表示されます。取得した media_url や permalink の URL にアクセスすると投稿された画像や投稿が表示されます。

この例の場合、.limit(4) で取得する結果の数を制限しているので、4件のデータの指定したフィールドが配列で取得されます。

上記 URL のリクエストに対するレスポンスの例
{
   "media": {  //fields パラメーターに指定した media
      "data": [
         {
            "caption": "Test",  //キャプション(もしあれば)
            "media_url": "https://scontent-nrt1-1・・・中略・・・B8", //画像の URL
            "permalink": "https://www.instagram.com/p/xxxxxxxx/",  //投稿の URL
            "like_count": 0,  //like のカウント数
            "comments_count": 0,  //コメントのカウント数
            "media_type": "IMAGE",  //メディアの種類
            "id": "xxxxxxxx"
         },
         {
            "media_url": "https://scontent-nrt1-1・・・中略・・・25",
            "permalink": "https://www.instagram.com/p/xxxxxxxx/",
            "like_count": 0,
            "comments_count": 0,
            "media_type": "IMAGE",
            "id": "xxxxxxxx"
         },
         ・・・残り2件のデータ部分は省略・・・
      ],
      "paging": {
         "cursors": {
            "before": "QVFIUl・・・中略・・・bi1WNGR3",
            "after": "QVFIUlB・・・中略・・・abjNLb0xR"
         }
      }
   },
   "id": "xxxxxxxx"  // Instagram ビジネスアカウント ID
}

参考(Facebook for Developers ドキュメント)

PHP で投稿一覧のデータを取得

ブラウザー内で URL をリクエストして API を利用できますが、Web ページに一覧を表示するには PHP や JavaScript で API にリクエストし、レスポンスの JSON データを使って出力します。

以下ではインスタグラムの投稿一覧のデータを Instagram グラフ API から取得して PHP で出力します。

API からのデータの取得は cURL 関数を使います。cURL は HTTP リクエストにより外部サイトの情報を取得することができる関数です。

cURL 関数の基本的な使用法は以下のようになります。

  1. curl_init() で cURL セッションを初期化
  2. curl_setopt() で転送時のオプションを設定
  3. curl_exec() で転送を実行
  4. curl_close() でセッションを終了

curl_exec() の戻り値はデフォルトでは結果を表す真偽値なので、curl_setopt() で CURLOPT_RETURNTRANSFER を設定して成功した場合に結果(データ)を取得するようにします。

クエリ部分はこの例では配列で作成して、http_build_query() で URL エンコードしてリクエストの URL に設定しています。

以下が一覧表示する部分のコードです。

3行目と5行目の xxxxxx には取得した Instagram ビジネスアカウント ID とアクセストークンを指定します。取得するメディアの件数や API のバージョンは必要に応じて変更します。

また、デフォルトでは取得結果にエラーがあれば、何も表示しません。$show_error を true にすれば、エラーがある場合は「エラーが発生しました」というメッセージとエラーコードを表示します。

<?php
//Instagram ビジネスアカウント ID
$business_id = "xxxxxx";
//アクセストークン
$token = 'xxxxxx';
//グラフAPI ホストURL(ルートエンドポイント)
$api = 'https://graph.facebook.com/';
//API のバージョン
$version = 'v20.0';
//取得するメディアの件数(0 または false を指定した場合は件数の制限なし)
$limit = '4';
//取得するフィールド
$field = 'caption,media_url,thumbnail_url,permalink,like_count,comments_count,media_type';
//クエリ(この場合、配列で記述して後で URL エンコード)
$query = [
  //取得件数の制限と取得するフィールドを指定($countの値が0やfalseの場合は件数の制限なし)
  'fields' => $limit ? 'media.limit(' . $limit . '){' . $field . '}' : 'media{' . $field . '}',
  //アクセストークンを指定
  'access_token' => $token
];
//URL を作成(クエリは http_build_query() で URL エンコード)
$url = $api . $version . '/' . $business_id . '?' . http_build_query($query);

//cURL セッションを初期化
$ch = curl_init();
//取得する URL を指定
curl_setopt($ch, CURLOPT_URL, $url);
//GET メソッドを使用(省略可能)
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
//戻り値を文字列で返す(データを結果として取得)
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
//結果を変数に代入(失敗すれば false、成功すれば取得結果が格納される)
$result = curl_exec($ch);
//cURL セッションを終了
curl_close($ch);
//レスポンスを格納する変数の初期化
$insta_response = null;
//エラーを表示するかどうか
$show_error = false;

if ($result) {
  //取得結果(JSON)をデコードしてレスポンスを格納する変数に代入
  $insta_response = json_decode($result);
  if (isset($insta_response->error)) {
    //$show_error が true  であれば
    if ($show_error) {
      //エラーコードとメッセージを表示
      echo '<div class="insta-error" style="background-color: #f6d7d7; padding: 20px; margin: 50px 0"> ';
      echo '<p style="color:red; font-size:24px">エラーが発生しました</p> ';
      $error = $insta_response->error;
      echo '<p>Error Code :' . $error->code . ' </p> <code>' . $error->message . '</code></div>';
    }
    //レスポンスを格納する変数 $insta_response を false に
    $insta_response = false;
  } elseif (!isset($insta_response->media)) {
    // 投稿(メディア)が存在しない場合
    $insta_response = false;
  }
}

if ($insta_response) :  //取得結果にエラーがなく投稿が存在すればタイトルと一覧を出力
?>
  <div class="insta-wrapper">
    <h2 class="insta-icon">Instagram 一覧表示テスト</h2>
    <div class="insta-container">
      <?php
      //レスポンスのデータは配列なので foreach でループ処理
      foreach ($insta_response->media->data as $val) {
        //メディアタイプがビデオの場合
        if ($val->media_type === 'VIDEO') {
          $src = $val->thumbnail_url;
          //ビデオの場合は以下の HTML でアイコンを表示
          $video = '<span class="video-icon"></span>';
        }
        //メディアタイプが画像の場合
        else {
          $src = $val->media_url;
          $video = '';
        }
      ?>
        <div class="insta-media">
          <a href="<?php echo $val->permalink; ?>" target="_blank" rel="noopener">
            <img src="<?php echo $src; ?>" alt="<?php echo isset($val->caption) ? $val->caption : ''; ?>">
            <?php echo $video ? $video : ''; //ビデオの場合は追加の span 要素を出力 ?>
            <span class="like-count"><?php echo $val->like_count; ?></span>
            <span class="comments-count"><?php echo $val->comments_count; ?></span>
          </a>
        </div>
      <?php } ?>
    </div>
  </div>
<?php endif; ?>

上記のコード(13行目)では投稿のメディア表示するために caption, media_url, thumbnail_url, permalink, like_count, comments_count, media_type というフィールドを取得しています。

$field = 'caption,media_url,thumbnail_url,permalink,like_count,comments_count,media_type';

指定できるフィールドには以下のようなものがあります。詳細は「IGメディア」に記載されています。

フィールド 説明
caption キャプション
comments_count メディアに付けられたコメントの数(コメントへの返信を含みます)
like_count メディアに対する「いいね!」の数
media_type メディアタイプ。CAROUSEL_ALBUM、IMAGE、または VIDEO
media_url メディア(画像など)の URL
thumbnail_url メディア(ビデオ)のサムネイルのURL。VIDEO でのみ利用可能
permalink メディア(投稿)を指す URL
timestamp ISO 8601形式のUTCでの作成日付(デフォルトはUTC ±00:00)
id メディアID
username メディアを作成したユーザーのユーザーネーム

この例の場合、curl_exec() の戻り値($result)が false でなければ、$result には JSON 形式の取得結果が入っているので、json_decode() を使って PHP で処理できるように変換して変数 $insta_response に格納しています。

取得結果にエラープロパティが設定されていれば、エラーメッセージを表示し、変数 $insta_response に false を代入して一覧表示の処理は行わないようにしています。また、投稿(メディア)が存在しない場合も同様に一覧表示の処理は行いません。

取得結果にエラーがなく投稿が存在すれば、レスポンスから foreach でそれぞれの投稿のデータを使って画像を出力します(タイトルを h2 要素で出力していますが、必要に応じて変更します)。

その際、media_type が VIDEO の場合は img 要素の src 属性には thumbnail_url フィールド(プロパティ)の値を指定し、そうでなければ media_url の値を指定します。

この例ではビデオの場合は、video_icon というクラスを指定した span 要素を出力して CSS でビデオ用のアイコンを表示するようにしていますが、不要であればこの部分(84行目)は削除します。そのままでも73行目で定義した span 要素が出力されるだけなので、CSS で何も指定しなければ問題ありません。

また、表示する画像にはその画像の投稿へのリンクを設定(href 属性に permalink フィールドの値を指定)し、画像の alt 属性には caption フィールドの値が設定されていればその値を設定しています。

そして span 要素で like_count と comments_count の値を使って「いいね!」の数とコメントの数を出力しています。この部分(85,86行目)も不要であれば削除します。

上記の場合、例えば以下のような HTML が出力されます。

<div class="insta-wrapper">
  <h2 class="insta-icon">Instagram 一覧表示テスト</h2>
  <div class="insta-container">
    <div class="insta-media">
      <a href="https://www.instagram.com/reel/C93YYr.../" target="_blank" rel="noopener">
        <img src="https://scontent-nrt1-2.cdninstagram.com/v/t51.29350-15/45..." alt="">
        <span class="video-icon"></span>
        <span class="like-count">0</span>
        <span class="comments-count">0</span>
      </a>
    </div>
    <div class="insta-media">
      <a href="https://www.instagram.com/p/C93War.../" target="_blank" rel="noopener">
        <img src="https://scontent-nrt1-2.cdninstagram.com/v/t51.29350-15/45,,," alt="">
        <span class="like-count">0</span>
        <span class="comments-count">0</span>
      </a>
    </div>
    <div class="insta-media">
      <a href="https://www.instagram.com/p/C9tsYZ.../" target="_blank" rel="noopener">
        <img src="https://scontent-nrt1-2.cdninstagram.com/v/t51.29350-15/45..." alt="">
        <span class="like-count">0</span>
        <span class="comments-count">0</span>
      </a>
    </div>
    <div class="insta-media">
      <a href="https://www.instagram.com/p/CW9Na.../" target="_blank" rel="noopener">
        <img src="https://scontent-nrt1-2.cdninstagram.com/v/t51.29350-15/26..." alt="Test">
        <span class="like-count">0</span>
        <span class="comments-count">0</span>
      </a>
    </div>
  </div>
</div>

以下は表示例です。

この例では、レイアウトは CSS GridCSS aspect-ratio を利用しています。

また、表示時にちらつく可能性があるので、CSS でフェードインアニメーションを設定しています。

.insta-container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  grid-auto-rows: minmax(100px, auto);
  gap: 10px;
}

.insta-media {
  position: relative;
}

.insta-media img {
  max-width: 100%;
  aspect-ratio: 1/1;
  object-fit: cover;
  display: block;
  width: 100%;
  height: auto;
}

.like-count,
.comments-count {
  position: absolute;
  bottom: 10px;
  color: #fff;
  filter: drop-shadow(1px 1px 1px #333);
}

.like-count {
  right: 25%;
}

.comments-count {
  right: 5%;
}

.like-count::before {
  content: "";
  display: inline-block;
  height: 18px;
  width: 18px;
  vertical-align: -3px;
  margin-right: 5px;
  background-repeat: no-repeat;
  /*アイコンのSVG画像*/
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23ffffff' viewBox='0 0 16 16'%3E  %3Cpath d='M8.864.046C7.908-.193 7.02.53 6.956 1.466c-.072 1.051-.23 2.016-.428 2.59-.125.36-.479 1.013-1.04 1.639-.557.623-1.282 1.178-2.131 1.41C2.685 7.288 2 7.87 2 8.72v4.001c0 .845.682 1.464 1.448 1.545 1.07.114 1.564.415 2.068.723l.048.03c.272.165.578.348.97.484.397.136.861.217 1.466.217h3.5c.937 0 1.599-.477 1.934-1.064a1.86 1.86 0 0 0 .254-.912c0-.152-.023-.312-.077-.464.201-.263.38-.578.488-.901.11-.33.172-.762.004-1.149.069-.13.12-.269.159-.403.077-.27.113-.568.113-.857 0-.288-.036-.585-.113-.856a2.144 2.144 0 0 0-.138-.362 1.9 1.9 0 0 0 .234-1.734c-.206-.592-.682-1.1-1.2-1.272-.847-.282-1.803-.276-2.516-.211a9.84 9.84 0 0 0-.443.05 9.365 9.365 0 0 0-.062-4.509A1.38 1.38 0 0 0 9.125.111L8.864.046zM11.5 14.721H8c-.51 0-.863-.069-1.14-.164-.281-.097-.506-.228-.776-.393l-.04-.024c-.555-.339-1.198-.731-2.49-.868-.333-.036-.554-.29-.554-.55V8.72c0-.254.226-.543.62-.65 1.095-.3 1.977-.996 2.614-1.708.635-.71 1.064-1.475 1.238-1.978.243-.7.407-1.768.482-2.85.025-.362.36-.594.667-.518l.262.066c.16.04.258.143.288.255a8.34 8.34 0 0 1-.145 4.725.5.5 0 0 0 .595.644l.003-.001.014-.003.058-.014a8.908 8.908 0 0 1 1.036-.157c.663-.06 1.457-.054 2.11.164.175.058.45.3.57.65.107.308.087.67-.266 1.022l-.353.353.353.354c.043.043.105.141.154.315.048.167.075.37.075.581 0 .212-.027.414-.075.582-.05.174-.111.272-.154.315l-.353.353.353.354c.047.047.109.177.005.488a2.224 2.224 0 0 1-.505.805l-.353.353.353.354c.006.005.041.05.041.17a.866.866 0 0 1-.121.416c-.165.288-.503.56-1.066.56z'/%3E%3C/svg%3E");
}

.comments-count::before {
  content: "";
  display: inline-block;
  height: 18px;
  width: 18px;
  vertical-align: -3px;
  margin-right: 5px;
  background-repeat: no-repeat;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23ffffff' viewBox='0 0 16 16'%3E  %3Cpath d='M2.678 11.894a1 1 0 0 1 .287.801 10.97 10.97 0 0 1-.398 2c1.395-.323 2.247-.697 2.634-.893a1 1 0 0 1 .71-.074A8.06 8.06 0 0 0 8 14c3.996 0 7-2.807 7-6 0-3.192-3.004-6-7-6S1 4.808 1 8c0 1.468.617 2.83 1.678 3.894zm-.493 3.905a21.682 21.682 0 0 1-.713.129c-.2.032-.352-.176-.273-.362a9.68 9.68 0 0 0 .244-.637l.003-.01c.248-.72.45-1.548.524-2.319C.743 11.37 0 9.76 0 8c0-3.866 3.582-7 8-7s8 3.134 8 7-3.582 7-8 7a9.06 9.06 0 0 1-2.347-.306c-.52.263-1.639.742-3.468 1.105z'/%3E%3C/svg%3E");
}

.video-icon {
  position: absolute;
  top: 3px;
  right: 5px;
  filter: drop-shadow(1px 1px 1px #333);
}

.video-icon::before {
  content: "";
  display: inline-block;
  height: 18px;
  width: 18px;
  vertical-align: -3px;
  background-repeat: no-repeat;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23ffffff' viewBox='0 0 16 16'%3E  %3Cpath fill-rule='evenodd' d='M0 5a2 2 0 0 1 2-2h7.5a2 2 0 0 1 1.983 1.738l3.11-1.382A1 1 0 0 1 16 4.269v7.462a1 1 0 0 1-1.406.913l-3.111-1.382A2 2 0 0 1 9.5 13H2a2 2 0 0 1-2-2V5zm11.5 5.175 3.5 1.556V4.269l-3.5 1.556v4.35zM2 4a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H2z'/%3E%3C/svg%3E");
}

.insta-icon::before {
  content: "";
  display: inline-block;
  height: 30px;
  width: 30px;
  vertical-align: -6px;
  background-repeat: no-repeat;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23a41867' viewBox='0 0 448 512'%3E%3Cpath d='M194.4 211.7a53.3 53.3 0 1 0 59.3 88.7 53.3 53.3 0 1 0 -59.3-88.7zm142.3-68.4c-5.2-5.2-11.5-9.3-18.4-12c-18.1-7.1-57.6-6.8-83.1-6.5c-4.1 0-7.9 .1-11.2 .1c-3.3 0-7.2 0-11.4-.1c-25.5-.3-64.8-.7-82.9 6.5c-6.9 2.7-13.1 6.8-18.4 12s-9.3 11.5-12 18.4c-7.1 18.1-6.7 57.7-6.5 83.2c0 4.1 .1 7.9 .1 11.1s0 7-.1 11.1c-.2 25.5-.6 65.1 6.5 83.2c2.7 6.9 6.8 13.1 12 18.4s11.5 9.3 18.4 12c18.1 7.1 57.6 6.8 83.1 6.5c4.1 0 7.9-.1 11.2-.1c3.3 0 7.2 0 11.4 .1c25.5 .3 64.8 .7 82.9-6.5c6.9-2.7 13.1-6.8 18.4-12s9.3-11.5 12-18.4c7.2-18 6.8-57.4 6.5-83c0-4.2-.1-8.1-.1-11.4s0-7.1 .1-11.4c.3-25.5 .7-64.9-6.5-83l0 0c-2.7-6.9-6.8-13.1-12-18.4zm-67.1 44.5A82 82 0 1 1 178.4 324.2a82 82 0 1 1 91.1-136.4zm29.2-1.3c-3.1-2.1-5.6-5.1-7.1-8.6s-1.8-7.3-1.1-11.1s2.6-7.1 5.2-9.8s6.1-4.5 9.8-5.2s7.6-.4 11.1 1.1s6.5 3.9 8.6 7s3.2 6.8 3.2 10.6c0 2.5-.5 5-1.4 7.3s-2.4 4.4-4.1 6.2s-3.9 3.2-6.2 4.2s-4.8 1.5-7.3 1.5l0 0c-3.8 0-7.5-1.1-10.6-3.2zM448 96c0-35.3-28.7-64-64-64H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96zM357 389c-18.7 18.7-41.4 24.6-67 25.9c-26.4 1.5-105.6 1.5-132 0c-25.6-1.3-48.3-7.2-67-25.9s-24.6-41.4-25.8-67c-1.5-26.4-1.5-105.6 0-132c1.3-25.6 7.1-48.3 25.8-67s41.5-24.6 67-25.8c26.4-1.5 105.6-1.5 132 0c25.6 1.3 48.3 7.1 67 25.8s24.6 41.4 25.8 67c1.5 26.3 1.5 105.4 0 131.9c-1.3 25.6-7.1 48.3-25.8 67z'/%3E%3C/svg%3E");
  margin-right: 5px;
}

.insta-media {
  animation: fadeInThumbs .9s;
}

@keyframes fadeInThumbs {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

アイコンは Bootstrap Icon の SVG を CSS で使って表示しています。

関連ページ:CSS で svg 要素を表示

ID やトークンを別ファイルに保存

前述の例ではビジネスアカウント ID やアクセストークンの値をファイルに直接記述していますが、別ファイルに記述して別途読み込むこともできます。

例えば、パブリックからアクセスできない場所に配置するか .htaccess でアクセス制御すれば安全です。

以下は insta_vars.php というファイルにビジネスアカウント ID とアクセストークンの値を記述しておき、それらの値を require で読み込む例です。

libs というディレクトリを作成し、その中に insta_vars.php を保存します。

├── libs
│   ├── .htaccess  //libs ディレクトリへのアクセスを制御
│   └── insta_vars.php //ビジネスアカウント ID とアクセストークンの値を記述
└── insta_sample.php //インスタグラム投稿の一覧を出力するファイル

この例では、insta_vars.php を配置するディレクトリ(libs)に以下のような内容の .htaccess を配置してこのディレクトリにアクセスできないようにします。

deny from all

insta_vars.php ではビジネスアカウント ID とアクセストークンの値を define() で定数として定義しておきます。

<?php
// Instagram ビジネスアカウント ID
define('IGBA_ID', '123456789');
// Instagram ユーザーアクセストークン
define('INSTA_TOKEN', 'EAAFPcGBdBr0BAPuJxgo..........Y3kT4ZBf');

require で insta_vars.php からビジネスアカウント ID とアクセストークンの値を読み込みます。

<?php
//ビジネスアカウント ID とアクセストークンを記述したファイルの読み込み
require 'libs/insta_vars.php';
//Instagram ビジネスアカウント ID
$business_id = IGBA_ID;
// ユーザーアクセストークン
$token = INSTA_TOKEN;

//以下は前述のコードと同じなので省略
//グラフAPI ホストURL(ルートエンドポイント)
$api = 'https://graph.facebook.com/';
//API のバージョン
$version = 'v20.0';
以下省略
...
WordPress で表示

クラシックテーマのテンプレート(front-page.php など)の表示したい場所に前述の コード を記述すれば WordPress でも表示できます。

ID やトークンを別ファイルに保存する場合、ファイルをテーマ内のフォルダ(例 libs)に保存して、get_theme_file_path() などを使って読み込むことができます。

get_theme_file_path() の返す絶対パスには末尾にスラッシュは含まれません。

//ビジネスアカウント ID とアクセストークンを記述したファイルの読み込み
require get_theme_file_path() . '/libs/insta_vars.php';
以下省略
管理者としてログインしていればエラーを表示

前述のコード( insta_sample.php)では、API(インスタのレスポンス)からエラーが返ってきた場合、変数 $show_error が true であれば、エラーを表示するようにしていますが、以下のように書き換えれば管理者としてログインしていればエラーを表示します。

//レスポンスを格納する変数の初期化
$insta_response = null;
//エラーを表示するかどうか(以下は使用しないので削除)
// $show_error = true;

if ($result) {
  $insta_response = json_decode($result);
  if (isset($insta_response->error)) {
    // 管理者としてログインしていればエラーを表示
    if (  is_user_logged_in() && current_user_can( 'manage_options' ) ) {
      echo '<div class="insta-error" style="background-color: #f6d7d7; padding: 20px; margin: 50px 0"> ';
      echo '<p style="color:red; font-size:24px">エラーが発生しました</p> ';
      $error = $insta_response->error;
      echo '<p>Error Code :' . $error->code . ' </p> <code>' . $error->message . '</code></div>';
    }
    $insta_response = false;
  } elseif (!isset($insta_response->media)) {
    $insta_response = false;
  }
}

JavaScript で投稿一覧のデータを取得

以下は JavaScript で Instagram グラフ API から取得した投稿一覧データを出力する例です。

この例ではページに id 属性が insta-div の要素があれば、そこに取得した投稿一覧を表示します。※ id 属性の値はユニーク(一意)である必要があるため、ページに1つしか配置できません。

<div id="insta-div"></div>

<script src="js/insta-feeds.js"></script>
├── js
│   └── insta-feeds.js //投稿一覧のデータを取得して表示する JavaScript
└── sample.html //インスタグラム投稿の一覧を表示するファイル

出力先の要素(id 属性が insta-div の要素)が存在すれば、fetch() でデータを取得しています。

以下が一覧表示するコードです。

PHP の場合同様、取得したアクセストークン、Instagram ビジネスアカウント ID と表示件数や取得したいフィールドを指定してリクエスト URL を組み立て、fetch() でデータを取得します。

7行目と9行目の xxxxxx には Instagram ビジネスアカウント ID とアクセストークンを指定します。

また、取得するメディアの件数や全体の見出し文字は data-xxxx 属性で指定可能です(後述)。

document.addEventListener('DOMContentLoaded', ()=> {
  // 出力先の要素を取得
  const target = document.getElementById('insta-div');

  if (target) {
    // Instagram ビジネスアカウント ID
    const businessID = "xxxxxx";
    // アクセストークン
    const accessToken = 'xxxxxx';
    // グラフAPI ホストURL(ルートエンドポイント)
    const api = 'https://graph.facebook.com/';
    // API のバージョン(使用するバージョンに応じて変更)
    const version = 'v20.0';
    // 表示件数(#insta-div に data-thumbs-limit 属性が指定されていればその値)
    const limit = target.dataset.thumbsLimit ? parseInt(target.dataset.thumbsLimit): 4;
    // 取得するフィールド
    const field = 'caption,media_url,thumbnail_url,permalink,like_count,comments_count,media_type';
    // fields パラメータ
    const fields = limit ? `media.limit(${limit}){${field}}`: `media{${field}}`;
    // 渡したいパラメータ(パラメーターを表すオブジェクト)
    const params = {
      fields,  // fields: fields, と同じこと
      access_token: accessToken,
    };
    // オブジェクト形式のパラメータをクエリ文字列に変換
    const query = new URLSearchParams(params);
    // fetch() に渡す URL の組み立て
    const url = `${api}/${version}/${businessID}?${query}`;

    fetch(url)
      .then((response) => {
        // ステータスが ok であればレスポンスを JSON として解析
        if (response.ok) {
          return response.json();
        } else {
          // ステータスが ok でなければエラーにする
          throw new Error(`Insta Fetch リクエスト失敗:${response.status} ${response.statusText}`);
        }
      }).then((instaResponse) => {
        // 引数で受け取った json オブジェクトの media.data
        const data = instaResponse.media.data;
        // 出力する値を入れる変数
        let instaFeeds = '';  // フィード部分の HTML
        let src = '';  // img の src
        let video = ''; // メディアタイプがビデオの場合に出力する要素
        // レスポンスのデータ(media.data)は配列なので forEach でループ処理
        data.forEach(item => {
          // メディアタイプがビデオの場合
          if (item.media_type === 'VIDEO') {
            src = item.thumbnail_url;
            // ビデオの場合は以下の span 要素でアイコンを表示
            video = '<span class="video-icon"></span>';
          }
          // メディアタイプが画像の場合
          else {
            src = item.media_url;
            video = '';
          }
          // フィード部分の HTML を作成
          const instaMediaDiv =
            `<div class="insta-media">
    <a href="${item.permalink}" target="_blank" rel="noopener">
      <img src="${src}" alt="${item.caption ? item.caption : ''}">
      ${video ? video : ''}
      <span class="like-count">${item.like_count}</span>
      <span class="comments-count">${item.comments_count}</span>
    </a>
  </div>
  `;
          instaFeeds += instaMediaDiv;
        });
        // 取得した画像データと見出しなどを出力
        // #insta-div に data-heading-text 属性が指定されていればその値を見出し文字列に(指定がなければ見出しを表示しない)
        const headingText =  target.dataset.headingText ? target.dataset.headingText: '';
        // #insta-div に data-heading-tag 属性が指定されていればその値を heading の要素に(デフォルトは h2)
        const headingTag = target.dataset.headingTag ? target.dataset.headingTag: 'h2';
        // 見出し部分のマークアップを作成
        const heading = headingText ? `<${headingTag} class="insta-icon">${headingText}</${headingTag}>`: '';
        target.innerHTML = `<div class="insta-wrapper">
    ${heading}
    <div class="insta-container">${instaFeeds}</div>
  </div>`;
      }).catch((error) => {
        console.warn(error.message);
        // エラーをページに表示する場合
        // target.innerHTML = `<p>エラー:${error.message} </p>`
      });
  }
});

表示件数と見出し

取得するメディアの件数(表示件数)と見出し(全体のタイトル)は、HTML に記述する出力先の div#insta-div に data-* 属性(カスタムデータ属性)を使って指定できます。

デフォルトでは表示件数は4、全体のタイトルは非表示ですが以下の属性で変更できます。

カスタムデータ属性 説明 デフォルト
data-thumbs-limit 取得するメディアの件数(表示件数) 4
data-heading-text 見出しの文字列(指定がなければ非表示) ""
data-heading-tag 全体のタイトルをマークアップする要素 h2

例えば、以下を記述すると、表示件数を6件、見出しの文字を「インスタ表示テスト」、見出しのマークアップを h3 で出力します。

<div id="insta-div" data-thumbs-limit="6" data-heading-text="インスタ表示テスト" data-heading-tag="h3"></div>

※ CSS で見出しにインスタのアイコンを設定しているので、data-heading-tag でタグを変更する場合は、見出しのサイズ(環境)に合わせてアイコンのサイズを調整する必要があります。

ID やトークンを別ファイルに保存

ビジネスアカウント ID やアクセストークンの値を別ファイルに記述して別途読み込むこともできます。

以下はビジネスアカウント ID とアクセストークンの値を insta-val.js というファイルに記述しておき、それらの値を import で読み込む例です。

但し、この例の場合、SetEnvIf Referer を使って insta-val.js へのアクセスを制御しているので、完全にアクセスを防ぐことはできません(自サイトや特定のURLを経由したアクセスのみ許可)。

├── js
│   ├── .htaccess  //insta-val.js へのアクセスを制御
│   ├── insta-feeds.js //投稿一覧のデータを取得して表示する JavaScript
│   └── insta-val.js //ビジネスアカウント ID とアクセストークンの値を記述した JavaScript
└── sample.html //インスタグラム投稿の一覧を表示するファイル

この例では以下のような内容の .htaccess を配置して http(s)?://example.com/js/insta-feeds.js 以外からは insta-val.js にアクセスできないようにしています(完全に制御できるわけではありませんが)。

SetEnvIf Referer "^http(s)?://example.com/js/insta-feeds\.js$" allow_access
<Files insta-val.js>
  order deny,allow
  deny from all
  allow from env=allow_access
</Files>

もう少し大雑把に以下のようにすれば example.com 以外からはアクセスできないようにすることができます。SetEnvIf Referer は複数指定することもできます。

SetEnvIf Referer "^http(s)?://example.com/.*$" allow_access
<Files insta-val.js>
  order deny,allow
  deny from all
  allow from env=allow_access
</Files>

insta-val.js ではビジネスアカウント ID とアクセストークンの値をエクスポートしています。

// Instagram ユーザーアクセストークン
export const accessToken = "xxxxxxxxxx";
// Instagram ビジネスアカウント ID
export const businessID = "xxxxxx";

import で insta-val.js からビジネスアカウント ID とアクセストークンの値を読み込み、変数に代入します(その他の部分は前述のコードと同じ)。

// 別ファイルに記述されたアクセストークン と Instagram ビジネスアカウント ID をインポートして変数に代入
import {accessToken, businessID} from './insta-val.js';

document.addEventListener('DOMContentLoaded', ()=> {
  // 出力先の要素を取得
  const target = document.getElementById('insta-div');
  if (target) {
    //グラフAPI ホストURL(ルートエンドポイント)
    const api = 'https://graph.facebook.com/';
    //API のバージョン
    const version = 'v20.0';
    // 表示件数
    const limit = target.dataset.thumbsLimit ? parseInt(target.dataset.thumbsLimit): 4;
    ・・・以下省略・・・
  }
});

また、JavaScript の読み込みでは import を使っているので type="module" を指定します。

<script src="js/insta-feeds.js" type="module"></script>
追加で投稿を読み込む

読み込んだ投稿データ以外にもまだデータがある場合は、「もっと見る」のようなボタンを表示して追加で投稿データを読み込むこともできます。

グラフ API のエンドポイントから取得した JSON レスポンスには以下のようなページ分割のパラメーターが含まれています。

{
  "data": [
      ... データ(省略)
  ],
  "paging": {
    "cursors": {
      "after": "MTAxNTExOTQ1MjAwNzI5NDE=",
      "before": "NDMyNzQyODI3OTQw"
    },
    "previous": "https://graph.facebook.com/{your-user-id}/albums?limit=25&before=NDMyNzQyODI3OTQw"
    "next": "https://graph.facebook.com/{your-user-id}/albums?limit=25&after=MTAxNTExOTQ1MjAwNzI5NDE="
  }
}
パラメーター 説明
next データの次ページを返すグラフAPIエンドポイント。含まれていない場合、これがデータの最終ページになります。
previous データの前のページを返すグラフAPIエンドポイント。含まれていない場合、これはデータの最初のページになります。

グラフAPI ドキュメント:ページ分割された結果

このため、取得したデータに next が含まれていれば、その値(エンドポイント)を使って次のデータを取得することができます。

以下は取得したデータに next が含まれていれば、「Load More」というボタンを表示して、クリックすると追加で投稿を読み込む例です。

document.addEventListener("DOMContentLoaded", () => {
  // 次のデータのエンドポイントを入れる変数を宣言
  let next;

  // 出力先の要素を取得
  const target = document.getElementById("insta-div");

  if (target) {
    // Instagram ビジネスアカウント ID
    const businessID = "xxxxxx";
    // アクセストークン
    const accessToken = "xxxxxx";

    //グラフAPI ホストURL(ルートエンドポイント)
    const api = "https://graph.facebook.com/";
    // API のバージョン(使用するバージョンに応じて変更)
    const version = 'v20.0';
    // 表示件数(#insta-div に data-thumbs-limit 属性が指定されていればその値)
    const limit = target.dataset.thumbsLimit ? parseInt(target.dataset.thumbsLimit): 4;
    //取得するフィールド
    const field =
      "caption,media_url,thumbnail_url,permalink,like_count,comments_count,media_type";
    // fields パラメータ
    const fields = limit
      ? `media.limit(${limit}){${field}}`
      : `media{${field}}`;
    // 渡したいパラメータ(パラメーターを表すオブジェクト)
    const params = {
      fields,  // fields: fields, と同じこと
      access_token: accessToken,
    };
    // オブジェクト形式のパラメータをクエリ文字列に変換
    const query = new URLSearchParams(params);
    // fetch() に渡す URL の組み立て
    const url = `${api}/${version}/${businessID}?${query}`;

    fetch(url)
      .then((response) => {
        // ステータスが ok であればレスポンスを JSON として解析
        if (response.ok) {
          return response.json();
        } else {
          // ステータスが ok でなければエラーにする
          throw new Error(
            `Insta Fetch リクエスト失敗:${response.status} ${response.statusText}`
          );
        }
      })
      .then((instaResponse) => {
        // 次のデータのエンドポイントを取得
        next = instaResponse.media.paging.next;
        // 取得したデータから投稿一覧の HTML を作成(関数を別途定義)
        const instaFeeds = createInstaFeeds(instaResponse.media.data);

         // #insta-div に data-heading-text 属性が指定されていればその値を見出し文字列に(指定がなければ見出しを表示しない)
         const headingText =  target.dataset.headingText ? target.dataset.headingText: '';
         // #insta-div に data-heading-tag 属性が指定されていればその値を heading の要素に(デフォルトは h2)
         const headingTag = target.dataset.headingTag ? target.dataset.headingTag: 'h2';
         // 見出し部分のマークアップを作成
         const heading = headingText ? `<${headingTag} class="insta-icon">${headingText}</${headingTag}>`: '';

        if (!next) {
          // 取得した画像データと見出しなどを出力
          target.innerHTML = `<div class="insta-wrapper">
      ${heading}
      <div class="insta-container">${instaFeeds}</div>
    </div>`;
        } else {
          // 次のデータのエンドポイントが存在すれば、投稿を読み込むボタンを出力
          target.innerHTML = `<div class="insta-wrapper">
      ${heading}
      <div class="insta-container">${instaFeeds}</div>
      <button id="load-more-insta-feeds">Load More</button>
    </div>`;
          const loadMoreInstaFeeds = target.querySelector( "#load-more-insta-feeds");
          // 投稿を読み込むボタンをクリックしたら次のデータのエンドポイントからデータを取得
          if (loadMoreInstaFeeds) {
            loadMoreInstaFeeds.addEventListener("click", () => {
              loadMoreFeeds(next);
            });
          }
        }
      }).catch((error) => {
        console.warn(error.message);
        // エラーをページに表示する場合
        // target.innerHTML = `<p>エラー:${error.message} </p>`
      });
  }

  // 引数に fetch() で取得したレスポンスの data を受取、インスタ一覧の HTML を生成する関数
  function createInstaFeeds(data) {
    // 出力する値を入れる変数
    let instaFeeds = ""; // フィード部分の HTML
    let src = ""; // img の src
    let video = ""; // メディアタイプがビデオの場合に出力する要素
    // レスポンスのデータ(media.data)は配列なので forEach でループ処理
    data.forEach((item) => {
      // メディアタイプがビデオの場合
      if (item.media_type === "VIDEO") {
        src = item.thumbnail_url;
        // ビデオの場合は以下の span 要素でアイコンを表示
        video = '<span class="video-icon"></span>';
      }
      // メディアタイプが画像の場合
      else {
        src = item.media_url;
        video = "";
      }
      // フィード部分の HTML を作成
      const instaMediaDiv = `<div class="insta-media">
    <a href="${item.permalink}" target="_blank" rel="noopener">
      <img src="${src}" alt="${item.caption ? item.caption : ""}">
      ${video ? video : ""}
      <span class="like-count">${item.like_count}</span>
      <span class="comments-count">${item.comments_count}</span>
    </a>
  </div>
  `;
      instaFeeds += instaMediaDiv;
    });
    return instaFeeds;
  }

  // 次のデータのエンドポイント(url)を引数に受取、HTML を更新する関数
  function loadMoreFeeds(url) {
    fetch(url)
      .then((response) => {
        // ステータスが ok であればレスポンスを JSON として解析
        if (response.ok) {
          return response.json();
        } else {
          // ステータスが ok でなければエラーにする
          throw new Error(
            `リクエスト失敗:${response.status} ${response.statusText}`
          );
        }
      })
      .then((instaResponse) => {
        const instaFeeds = createInstaFeeds(instaResponse.data);
        // 次のデータのエンドポイントを取得(instaResponse.media.paging.next ではなく instaResponse.paging.next になる)
        next = instaResponse.paging.next;
        // 投稿のコンテナを取得
        const instaContainer = target.querySelector(".insta-container");
        if (instaContainer) {
          // 投稿のコンテナに追加の投稿の HTML を挿入
          instaContainer.insertAdjacentHTML("beforeend", instaFeeds);
          if (!next) {
            // 次のデータのエンドポイント(next)が存在しなければボタンを削除
            target.querySelector("#load-more-insta-feeds").remove();
          }
        }
      });
  }
});

ボタンの CSS を追加

以下を CSS に追加しています。

#load-more-insta-feeds {
  background-color: #111;
  padding: 10px 20px;
  color: #fff;
  margin: 30px 0;
  cursor: pointer;
  border: none;
  transition: background-color .3s;
}

#load-more-insta-feeds:hover {
  background-color: #444;
}

以下は表示例です。表示件数を3、見出しのテキストを「インスタ表示テスト」、見出しのタグを h6 で表示する例です。

<div id="insta-div" data-thumbs-limit="3" data-heading-text="インスタ表示テスト" data-heading-tag="h6"></div>

見出しのタグをデフォルトの h2 から変更すると見出しのサイズが変更されるため、例えば以下のようなスタイルを追加してアイコンのサイズも調整しておくとよいかと思います。

h3.insta-icon::before {
  height: 26px;
  width: 26px;
}

h4.insta-icon::before {
  height: 24px;
  width: 24px;
  vertical-align: -5px;
}

h5.insta-icon::before {
  height: 23px;
  width: 23px;
  vertical-align: -5px;
}

h6.insta-icon::before {
  height: 22px;
  width: 22px;
  vertical-align: -5px;
}
最大読み込み数を指定

ボタンを表示して追加で投稿を読み込む場合に、読み込み数の最大値をカスタムデータ属性(data-max-media-count)を使って指定する例です。

例えば、一度に読み込む投稿数(data-thumbs-limit)を 3、最大表示数を 12 にするには以下のように指定します。但し、最大値が正確に適用されるわけではなく、以下で最大表示数を 11 としても、最大で12件読み込まれます。

また、data-max-media-count 属性を省略するか、値を 0 にすると最後まで読み込みます。

<div id="insta-div" data-thumbs-limit="3" data-max-media-count="12"></div>

以下が JavaScript です。追加部分はハイライト表示しています。

document.addEventListener("DOMContentLoaded", () => {
  // 次のデータのエンドポイントを入れる変数を宣言
  let next;
  // 表示した投稿の総数
  let mediaLoaded = 0;

  // 出力先の要素を取得
  const target = document.getElementById("insta-div");

  if (target) {
    // Instagram ビジネスアカウント ID
    const businessID = "xxxxxx";
    // アクセストークン
    const accessToken = "xxxxxx";

    //グラフAPI ホストURL(ルートエンドポイント)
    const api = "https://graph.facebook.com/";
    // API のバージョン(使用するバージョンに応じて変更)
    const version = 'v20.0';
    // 表示件数(#insta-div に data-thumbs-limit 属性が指定されていればその値)
    const limit = target.dataset.thumbsLimit ? parseInt(target.dataset.thumbsLimit): 4;
    //取得するフィールド
    const field = "caption,media_url,thumbnail_url,permalink,like_count,comments_count,media_type";
    // fields パラメータ
    const fields = limit
      ? `media.limit(${limit}){${field}}`
      : `media{${field}}`;
    // 渡したいパラメータ(パラメーターを表すオブジェクト)
    const params = {
      fields,   // fields: fields, と同じこと
      access_token: accessToken,
    };
    // オブジェクト形式のパラメータをクエリ文字列に変換
    const query = new URLSearchParams(params);
    // fetch() に渡す URL の組み立て
    const url = `${api}/${version}/${businessID}?${query}`;

    fetch(url)
      .then((response) => {
        // ステータスが ok であればレスポンスを JSON として解析
        if (response.ok) {
          return response.json();
        } else {
          // ステータスが ok でなければエラーにする
          throw new Error(
            `Insta Fetch リクエスト失敗:${response.status} ${response.statusText}`
          );
        }
      })
      .then((instaResponse) => {
        // 次のデータのエンドポイントを取得
        next = instaResponse.media.paging.next;
        // 取得したデータから投稿一覧の HTML を作成(関数を別途定義)
        const instaFeeds = createInstaFeeds(instaResponse.media.data);
        // 最大表示数(#insta-div に data-max-media-count 属性が指定されていればその値)
        const maxMediaCount = target.dataset.maxMediaCount ? parseInt(target.dataset.maxMediaCount): 0;
        // 表示した投稿数をカウント
        mediaLoaded += limit;
         // #insta-div に data-heading-text 属性が指定されていればその値を見出し文字列に(指定がなければ見出しを表示しない)
         const headingText =  target.dataset.headingText ? target.dataset.headingText: '';
         // #insta-div に data-heading-tag 属性が指定されていればその値を heading の要素に(デフォルトは h2)
         const headingTag = target.dataset.headingTag ? target.dataset.headingTag: 'h2';
         // 見出し部分のマークアップを作成
         const heading = headingText ? `<${headingTag} class="insta-icon">${headingText}</${headingTag}>`: '';

        if (!next || (maxMediaCount && maxMediaCount <= mediaLoaded)) {
          // 取得した画像データと見出しなどを出力
          target.innerHTML = `<div class="insta-wrapper">
      ${heading}
      <div class="insta-container">${instaFeeds}</div>
    </div>`;
        } else {
          // 次のデータのエンドポイントが存在すれば、投稿を読み込むボタンを出力
          target.innerHTML = `<div class="insta-wrapper">
      ${heading}
      <div class="insta-container">${instaFeeds}</div>
      <button id="load-more-insta-feeds">Load More</button>
    </div>`;
          const loadMoreInstaFeeds = target.querySelector( "#load-more-insta-feeds");
          // 投稿を読み込むボタンをクリックしたら次のデータのエンドポイントからデータを取得
          if (loadMoreInstaFeeds) {
            loadMoreInstaFeeds.addEventListener("click", () => {
              // loadMoreFeeds() を呼び出す際に表示件数と最大表示数も引数に渡す
              loadMoreFeeds(next, limit, maxMediaCount);
            });
          }
        }
      }).catch((error) => {
        console.warn(error.message);
        // エラーをページに表示する場合
        // target.innerHTML = `<p>エラー:${error.message} </p>`
      });
  }

  // 引数に fetch() で取得したレスポンスの data を受取、インスタ一覧の HTML を生成する関数
  function createInstaFeeds(data) {
    // 出力する値を入れる変数
    let instaFeeds = ""; // フィード部分の HTML
    let src = ""; // img の src
    let video = ""; // メディアタイプがビデオの場合に出力する要素
    // レスポンスのデータ(media.data)は配列なので forEach でループ処理
    data.forEach((item) => {
      // メディアタイプがビデオの場合
      if (item.media_type === "VIDEO") {
        src = item.thumbnail_url;
        // ビデオの場合は以下の span 要素でアイコンを表示
        video = '<span class="video-icon"></span>';
      }
      // メディアタイプが画像の場合
      else {
        src = item.media_url;
        video = "";
      }
      // フィード部分の HTML を作成
      const instaMediaDiv = `<div class="insta-media">
    <a href="${item.permalink}" target="_blank" rel="noopener">
      <img src="${src}" alt="${item.caption ? item.caption : ""}">
      ${video ? video : ""}
      <span class="like-count">${item.like_count}</span>
      <span class="comments-count">${item.comments_count}</span>
    </a>
  </div>
  `;
      instaFeeds += instaMediaDiv;
    });
    return instaFeeds;
  }

  // 次のデータのエンドポイント(url)を引数に受取、HTML を更新する関数
  function loadMoreFeeds(url, limit, maxMediaCount) {
    fetch(url)
      .then((response) => {
        // ステータスが ok であればレスポンスを JSON として解析
        if (response.ok) {
          return response.json();
        } else {
          // ステータスが ok でなければエラーにする
          throw new Error(
            `リクエスト失敗:${response.status} ${response.statusText}`
          );
        }
      })
      .then((instaResponse) => {
        const instaFeeds = createInstaFeeds(instaResponse.data);
        // 次のデータのエンドポイントを取得(instaResponse.media.paging.next ではなく instaResponse.paging.next になる)
        next = instaResponse.paging.next;
        // 投稿のコンテナを取得
        const instaContainer = target.querySelector(".insta-container");
        if (instaContainer) {
          // 投稿のコンテナに追加の投稿の HTML を挿入
          instaContainer.insertAdjacentHTML("beforeend", instaFeeds);
          // 読み込み数をカウント
          mediaLoaded += limit;
          // next が存在しないか、表示数が最大表示数より大きければ
          if (!next ||  maxMediaCount && maxMediaCount <= mediaLoaded) {
            // 次のデータのエンドポイント(next)が存在しなければボタンを削除
            target.querySelector("#load-more-insta-feeds").remove();
          }
        }
      });
  }
});

WordPress テンプレートで表示

WordPress の場合でも、テンプレート(front-page.php や home.html など)に id が insta-div の div 要素を記述して、functions.php などで JavaScript を読み込めば、インスタの一覧を表示できます。

必要に応じてカスタムデータ属性(data-max-media-count も含む)を指定します。

<div id="insta-div" data-thumbs-limit="3" data-heading-text="インスタ表示テスト"></div>

前述の JavaScript(insta-feeds.js)をテーマ内に保存します。

以下の例ではテーマディレクトリに insta というフォルダを作成してその中にファイルを保存しています。

my-theme
├── archive.php
├── comments.php
├── footer.php
├── front-page.php  // インスタを表示する場所に <div id="insta-div"></div> を記述
├── functions.php  // JavaScript の読み込み
├── header.php
├── index.php
├── insta  // フォルダを作成してファイルを保存
│   ├── .htaccess
│   ├── insta-feeds.js
│   └── insta-val.js
├── search.php
├── sidebar.php
├── single.php
└── style.css

JavaScript の内容は前述と同じですが、ID やトークンを別ファイルに保存して読み込むようにしています。

// 別ファイルに記述されたアクセストークン と Instagram ビジネスアカウント ID をインポートして変数に代入
import {accessToken, businessID} from './insta-val.js';

document.addEventListener("DOMContentLoaded", () => {
  // 次のデータのエンドポイントを入れる変数を宣言
  let next;
  // 表示した投稿の総数
  let mediaLoaded = 0;

  // 出力先の要素を取得
  const target = document.getElementById("insta-div");

  if (target) {
    //グラフAPI ホストURL(ルートエンドポイント)
    const api = "https://graph.facebook.com/";
    // API のバージョン(使用するバージョンに応じて変更)
    const version = 'v20.0';
    // 表示件数(#insta-div に data-thumbs-limit 属性が指定されていればその値)
    const limit = target.dataset.thumbsLimit ? parseInt(target.dataset.thumbsLimit): 4;
    //取得するフィールド
    const field = "caption,media_url,thumbnail_url,permalink,like_count,comments_count,media_type";
    // fields パラメータ
    const fields = limit
      ? `media.limit(${limit}){${field}}`
      : `media{${field}}`;
    // 渡したいパラメータ(パラメーターを表すオブジェクト)
    const params = {
      fields,   // fields: fields, と同じこと
      access_token: accessToken,
    };
    // オブジェクト形式のパラメータをクエリ文字列に変換
    const query = new URLSearchParams(params);
    // fetch() に渡す URL の組み立て
    const url = `${api}/${version}/${businessID}?${query}`;

    fetch(url)
      .then((response) => {
        // ステータスが ok であればレスポンスを JSON として解析
        if (response.ok) {
          return response.json();
        } else {
          // ステータスが ok でなければエラーにする
          throw new Error(
            `Insta Fetch リクエスト失敗:${response.status} ${response.statusText}`
          );
        }
      })
      .then((instaResponse) => {
        // 次のデータのエンドポイントを取得
        next = instaResponse.media.paging.next;
        // 取得したデータから投稿一覧の HTML を作成(関数を別途定義)
        const instaFeeds = createInstaFeeds(instaResponse.media.data);
        // 最大表示数(#insta-div に data-max-media-count 属性が指定されていればその値)
        const maxMediaCount = target.dataset.maxMediaCount ? parseInt(target.dataset.maxMediaCount): 0;
        // 表示した投稿数をカウント
        mediaLoaded += limit;
         // #insta-div に data-heading-text 属性が指定されていればその値を見出し文字列に(指定がなければ見出しを表示しない)
         const headingText =  target.dataset.headingText ? target.dataset.headingText: '';
         // #insta-div に data-heading-tag 属性が指定されていればその値を heading の要素に(デフォルトは h2)
         const headingTag = target.dataset.headingTag ? target.dataset.headingTag: 'h2';
         // 見出し部分のマークアップを作成
         const heading = headingText ? `<${headingTag} class="insta-icon">${headingText}</${headingTag}>`: '';

        if (!next || (maxMediaCount && maxMediaCount <= mediaLoaded)) {
          // 取得した画像データと見出しなどを出力
          target.innerHTML = `<div class="insta-wrapper">
      ${heading}
      <div class="insta-container">${instaFeeds}</div>
    </div>`;
        } else {
          // 次のデータのエンドポイントが存在すれば、投稿を読み込むボタンを出力
          target.innerHTML = `<div class="insta-wrapper">
      ${heading}
      <div class="insta-container">${instaFeeds}</div>
      <button id="load-more-insta-feeds">Load More</button>
    </div>`;
          const loadMoreInstaFeeds = target.querySelector( "#load-more-insta-feeds");
          // 投稿を読み込むボタンをクリックしたら次のデータのエンドポイントからデータを取得
          if (loadMoreInstaFeeds) {
            loadMoreInstaFeeds.addEventListener("click", () => {
              // loadMoreFeeds() を呼び出す際に表示件数と最大表示数も引数に渡す
              loadMoreFeeds(next, limit, maxMediaCount);
            });
          }
        }
      }).catch((error) => {
        console.warn(error.message);
        // エラーをページに表示する場合
        // target.innerHTML = `<p>エラー:${error.message} </p>`
      });
  }

  // 引数に fetch() で取得したレスポンスの data を受取、インスタ一覧の HTML を生成する関数
  function createInstaFeeds(data) {
    // 出力する値を入れる変数
    let instaFeeds = ""; // フィード部分の HTML
    let src = ""; // img の src
    let video = ""; // メディアタイプがビデオの場合に出力する要素
    // レスポンスのデータ(media.data)は配列なので forEach でループ処理
    data.forEach((item) => {
      // メディアタイプがビデオの場合
      if (item.media_type === "VIDEO") {
        src = item.thumbnail_url;
        // ビデオの場合は以下の span 要素でアイコンを表示
        video = '<span class="video-icon"></span>';
      }
      // メディアタイプが画像の場合
      else {
        src = item.media_url;
        video = "";
      }
      // フィード部分の HTML を作成
      const instaMediaDiv = `<div class="insta-media">
    <a href="${item.permalink}" target="_blank" rel="noopener">
      <img src="${src}" alt="${item.caption ? item.caption : ""}">
      ${video ? video : ""}
      <span class="like-count">${item.like_count}</span>
      <span class="comments-count">${item.comments_count}</span>
    </a>
  </div>
  `;
      instaFeeds += instaMediaDiv;
    });
    return instaFeeds;
  }

  // 次のデータのエンドポイント(url)を引数に受取、HTML を更新する関数
  function loadMoreFeeds(url, limit, maxMediaCount) {
    fetch(url)
      .then((response) => {
        // ステータスが ok であればレスポンスを JSON として解析
        if (response.ok) {
          return response.json();
        } else {
          // ステータスが ok でなければエラーにする
          throw new Error(
            `リクエスト失敗:${response.status} ${response.statusText}`
          );
        }
      })
      .then((instaResponse) => {
        const instaFeeds = createInstaFeeds(instaResponse.data);
        // 次のデータのエンドポイントを取得(instaResponse.media.paging.next ではなく instaResponse.paging.next になる)
        next = instaResponse.paging.next;
        // 投稿のコンテナを取得
        const instaContainer = target.querySelector(".insta-container");
        if (instaContainer) {
          // 投稿のコンテナに追加の投稿の HTML を挿入
          instaContainer.insertAdjacentHTML("beforeend", instaFeeds);
          // 読み込み数をカウント
          mediaLoaded += limit;
          // next が存在しないか、表示数が最大表示数より大きければ
          if (!next ||  maxMediaCount && maxMediaCount <= mediaLoaded) {
            // 次のデータのエンドポイント(next)が存在しなければボタンを削除
            target.querySelector("#load-more-insta-feeds").remove();
          }
        }
      });
  }
});
export const accessToken = 'EAAMSs5U6BZAUBO7Vub3OU5c.......';
export const businessID = "12345678901234567";

この例では ID とトークンを別ファイル(insta-val.js)に保存していますが、.htaccess の SetEnvIf Referer 部分を特定のファイルではなく同じドメインからのアクセスを許可するように変更しています。

SetEnvIf Referer "^http(s)?://example.com/.*" allow_access
<Files insta-val.js>
order deny,allow
deny from all
allow from env=allow_access
</Files>

functions.php で JavaScript を読み込む

JavaScript を functions.php で読み込みます。以下の例ではフロントページのみでインスタを表示することとして、条件分岐タグ is_front_page() を使ってフロントページの場合にのみ JavaScript(insta-feeds.js)を読み込んでいます。必要に応じて条件分岐タグを変更または削除します。

また、この例では import を使って外部ファイルに記述された ID とトークンを読み込むため、JavaScript をモジュールとする必要があるので、script_loader_tag または wp_script_attributes を使ってスクリプトタグに type="module" を追加します。

function add_my_insta_scripts() {
  // フロントページであれば JavaScript を読み込む
  if(is_front_page()) {
    wp_enqueue_script(
      'instaFeeds',
      get_theme_file_uri( '/insta/insta-feeds.js' ),
      array(),
      filemtime( get_theme_file_path( '/insta/insta-feeds.js' ) ),
      true
    );
  }

}
add_action( 'wp_enqueue_scripts', 'add_my_insta_scripts' );

// import を使って外部ファイルに記述された ID とトークンを読み込む場合は type="module" を追加
function add_type_module_to_js($tag, $handle, $src) {
  if('instaFeeds' !== $handle) {
    return $tag;
  }
  return '<script type="module" src="' . esc_url($src) . '"></script>';
}
add_filter('script_loader_tag', 'add_type_module_to_js', 10, 3);

script_loader_tag フィルターの代わりにバージョン 5.7 で導入された wp_script_attributes フィルターを使う場合は、上記17〜23行目を以下に変更します。

// wp_script_attributes で type="module" を追加
function add_type_attribute( $attributes ) {
  if ( isset( $attributes['id'] ) && $attributes['id'] === 'instaFeeds-js' ) {
    $attributes['type'] = 'module';
  }
  return $attributes;
}
add_filter( 'wp_script_attributes', 'add_type_attribute', 10, 1 );

スタイル

スタイルは style.css などに記述します(内容は前述のものと同じです)。

.insta-container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  grid-auto-rows: minmax(100px, auto);
  gap: 20px;
}

.insta-media {
  position: relative;
}

.insta-media img {
  max-width: 100%;
  aspect-ratio: 1/1;
  object-fit: cover;
  display: block;
  width: 100%;
  height: auto;
}

.like-count,
.comments-count {
  position: absolute;
  bottom: 10px;
  color: #fff;
}

.like-count {
  right: 25%;
}

.comments-count {
  right: 10%;
}

.like-count::before {
  content: "";
  display: inline-block;
  height: 18px;
  width: 18px;
  vertical-align: -3px;
  margin-right: 5px;
  background-repeat: no-repeat;
  /*アイコンのSVG画像*/
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23ffffff' viewBox='0 0 16 16'%3E  %3Cpath d='M8.864.046C7.908-.193 7.02.53 6.956 1.466c-.072 1.051-.23 2.016-.428 2.59-.125.36-.479 1.013-1.04 1.639-.557.623-1.282 1.178-2.131 1.41C2.685 7.288 2 7.87 2 8.72v4.001c0 .845.682 1.464 1.448 1.545 1.07.114 1.564.415 2.068.723l.048.03c.272.165.578.348.97.484.397.136.861.217 1.466.217h3.5c.937 0 1.599-.477 1.934-1.064a1.86 1.86 0 0 0 .254-.912c0-.152-.023-.312-.077-.464.201-.263.38-.578.488-.901.11-.33.172-.762.004-1.149.069-.13.12-.269.159-.403.077-.27.113-.568.113-.857 0-.288-.036-.585-.113-.856a2.144 2.144 0 0 0-.138-.362 1.9 1.9 0 0 0 .234-1.734c-.206-.592-.682-1.1-1.2-1.272-.847-.282-1.803-.276-2.516-.211a9.84 9.84 0 0 0-.443.05 9.365 9.365 0 0 0-.062-4.509A1.38 1.38 0 0 0 9.125.111L8.864.046zM11.5 14.721H8c-.51 0-.863-.069-1.14-.164-.281-.097-.506-.228-.776-.393l-.04-.024c-.555-.339-1.198-.731-2.49-.868-.333-.036-.554-.29-.554-.55V8.72c0-.254.226-.543.62-.65 1.095-.3 1.977-.996 2.614-1.708.635-.71 1.064-1.475 1.238-1.978.243-.7.407-1.768.482-2.85.025-.362.36-.594.667-.518l.262.066c.16.04.258.143.288.255a8.34 8.34 0 0 1-.145 4.725.5.5 0 0 0 .595.644l.003-.001.014-.003.058-.014a8.908 8.908 0 0 1 1.036-.157c.663-.06 1.457-.054 2.11.164.175.058.45.3.57.65.107.308.087.67-.266 1.022l-.353.353.353.354c.043.043.105.141.154.315.048.167.075.37.075.581 0 .212-.027.414-.075.582-.05.174-.111.272-.154.315l-.353.353.353.354c.047.047.109.177.005.488a2.224 2.224 0 0 1-.505.805l-.353.353.353.354c.006.005.041.05.041.17a.866.866 0 0 1-.121.416c-.165.288-.503.56-1.066.56z'/%3E%3C/svg%3E");
}

.comments-count::before {
  content: "";
  display: inline-block;
  height: 18px;
  width: 18px;
  vertical-align: -3px;
  margin-right: 5px;
  background-repeat: no-repeat;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23ffffff' viewBox='0 0 16 16'%3E  %3Cpath d='M2.678 11.894a1 1 0 0 1 .287.801 10.97 10.97 0 0 1-.398 2c1.395-.323 2.247-.697 2.634-.893a1 1 0 0 1 .71-.074A8.06 8.06 0 0 0 8 14c3.996 0 7-2.807 7-6 0-3.192-3.004-6-7-6S1 4.808 1 8c0 1.468.617 2.83 1.678 3.894zm-.493 3.905a21.682 21.682 0 0 1-.713.129c-.2.032-.352-.176-.273-.362a9.68 9.68 0 0 0 .244-.637l.003-.01c.248-.72.45-1.548.524-2.319C.743 11.37 0 9.76 0 8c0-3.866 3.582-7 8-7s8 3.134 8 7-3.582 7-8 7a9.06 9.06 0 0 1-2.347-.306c-.52.263-1.639.742-3.468 1.105z'/%3E%3C/svg%3E");
}

.video-icon {
  position: absolute;
  top: 3px;
  right: 5px;
}

.video-icon::before {
  content: "";
  display: inline-block;
  height: 18px;
  width: 18px;
  vertical-align: -3px;
  background-repeat: no-repeat;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23ffffff' viewBox='0 0 16 16'%3E  %3Cpath fill-rule='evenodd' d='M0 5a2 2 0 0 1 2-2h7.5a2 2 0 0 1 1.983 1.738l3.11-1.382A1 1 0 0 1 16 4.269v7.462a1 1 0 0 1-1.406.913l-3.111-1.382A2 2 0 0 1 9.5 13H2a2 2 0 0 1-2-2V5zm11.5 5.175 3.5 1.556V4.269l-3.5 1.556v4.35zM2 4a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H2z'/%3E%3C/svg%3E");
}

.insta-icon::before {
  content: "";
  display: inline-block;
  height: 30px;
  width: 30px;
  vertical-align: -6px;
  background-repeat: no-repeat;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23a41867' viewBox='0 0 448 512'%3E%3Cpath d='M194.4 211.7a53.3 53.3 0 1 0 59.3 88.7 53.3 53.3 0 1 0 -59.3-88.7zm142.3-68.4c-5.2-5.2-11.5-9.3-18.4-12c-18.1-7.1-57.6-6.8-83.1-6.5c-4.1 0-7.9 .1-11.2 .1c-3.3 0-7.2 0-11.4-.1c-25.5-.3-64.8-.7-82.9 6.5c-6.9 2.7-13.1 6.8-18.4 12s-9.3 11.5-12 18.4c-7.1 18.1-6.7 57.7-6.5 83.2c0 4.1 .1 7.9 .1 11.1s0 7-.1 11.1c-.2 25.5-.6 65.1 6.5 83.2c2.7 6.9 6.8 13.1 12 18.4s11.5 9.3 18.4 12c18.1 7.1 57.6 6.8 83.1 6.5c4.1 0 7.9-.1 11.2-.1c3.3 0 7.2 0 11.4 .1c25.5 .3 64.8 .7 82.9-6.5c6.9-2.7 13.1-6.8 18.4-12s9.3-11.5 12-18.4c7.2-18 6.8-57.4 6.5-83c0-4.2-.1-8.1-.1-11.4s0-7.1 .1-11.4c.3-25.5 .7-64.9-6.5-83l0 0c-2.7-6.9-6.8-13.1-12-18.4zm-67.1 44.5A82 82 0 1 1 178.4 324.2a82 82 0 1 1 91.1-136.4zm29.2-1.3c-3.1-2.1-5.6-5.1-7.1-8.6s-1.8-7.3-1.1-11.1s2.6-7.1 5.2-9.8s6.1-4.5 9.8-5.2s7.6-.4 11.1 1.1s6.5 3.9 8.6 7s3.2 6.8 3.2 10.6c0 2.5-.5 5-1.4 7.3s-2.4 4.4-4.1 6.2s-3.9 3.2-6.2 4.2s-4.8 1.5-7.3 1.5l0 0c-3.8 0-7.5-1.1-10.6-3.2zM448 96c0-35.3-28.7-64-64-64H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96zM357 389c-18.7 18.7-41.4 24.6-67 25.9c-26.4 1.5-105.6 1.5-132 0c-25.6-1.3-48.3-7.2-67-25.9s-24.6-41.4-25.8-67c-1.5-26.4-1.5-105.6 0-132c1.3-25.6 7.1-48.3 25.8-67s41.5-24.6 67-25.8c26.4-1.5 105.6-1.5 132 0c25.6 1.3 48.3 7.1 67 25.8s24.6 41.4 25.8 67c1.5 26.3 1.5 105.4 0 131.9c-1.3 25.6-7.1 48.3-25.8 67z'/%3E%3C/svg%3E");
  margin-right: 5px;
}

h3.insta-icon::before {
  height: 26px;
  width: 26px;
}

h4.insta-icon::before {
  height: 24px;
  width: 24px;
  vertical-align: -5px;
}

h5.insta-icon::before {
  height: 23px;
  width: 23px;
  vertical-align: -5px;
}

h6.insta-icon::before {
  height: 22px;
  width: 22px;
  vertical-align: -5px;
}

.insta-media {
  animation: fadeInThumbs .6s;
}

@keyframes fadeInThumbs {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

#load-more-insta-feeds {
  background-color: #111;
  padding: 10px 20px;
  color: #fff;
  margin: 30px 0;
  cursor: pointer;
  border: none;
  transition: background-color .3s;
}

#load-more-insta-feeds:hover {
  background-color: #444;
}
WordPress 投稿ページで表示

WordPress の投稿個別ページで表示するには、個別ページでも JavaScript を読み込むようにして、投稿ページでカスタム HTML ブロックを使います。

function add_my_insta_scripts() {
  // フロントページまたは投稿個別ページであれば JavaScript を読み込む
  if(is_front_page() || is_single()) {
    wp_enqueue_script(
      'instaFeeds',
      get_theme_file_uri( '/insta/insta-feeds.js' ),
      array(),
      filemtime( get_theme_file_path( '/insta/insta-feeds.js' ) ),
      true
    );
  }

}
add_action( 'wp_enqueue_scripts', 'add_my_insta_scripts' );

// import を使って外部ファイルに記述された ID とトークンを読み込む場合は type="module" を追加
function add_type_module_to_js($tag, $handle, $src) {
  if('instaFeeds' !== $handle) {
    return $tag;
  }
  return '<script type="module" src="' . esc_url($src) . '"></script>';
}
add_filter('script_loader_tag', 'add_type_module_to_js', 10, 3);

以下は個別投稿ページでの表示例です。

注意点

この例では、id 属性の値が insta-div の要素( <div id="insta-div"></div> )にインスタの投稿を表示するようにしているため、ページに1つしか配置できません。

投稿の一覧ページで、投稿のコンテンツも表示するようにしていて、インスタの投稿部分も複数表示される場合、最初の id 属性の値が insta-div の要素にしか表示されません。

また、その場合、同じページに複数の同じ id 属性の要素が存在してしまうので、投稿ページのカスタム HTML ブロックでは以下のように id 属性ではなく class 属性を使います。

<div class="insta-div"></div>

そして、JavaScript で対象の要素を取得する部分を id 属性ではなく class 属性を使って対象の要素を取得します。この場合でも、投稿の一覧ページで複数のインスタの投稿部分が表示される場合は、最初の要素にのみインスタの投稿が表示されます。

// 出力先の要素を取得
//const target = document.getElementById("insta-div");
// 上記を以下に変更
const target = document.getElementsByClassName("insta-div")[0];

インスタ表示用カスタムブロックを作成して投稿に表示する方法は以下を御覧ください。

ログインしていればエラーを表示

何らかの理由で API(インスタのレスポンス)からエラーが返ってきた場合、現在はエラー表示部分はコメントアウトしてあるので何も表示されません。

テーマで、body 要素に body_class() を使ってクラスを出力している場合、比較的簡単に通常の閲覧者にはエラーを表示せず、ログインしている場合にのみエラーを表示することができます。

JavaScript の以下の部分を

 .catch((error) => {
  console.warn(error.message);
  // エラーをページに表示する場合
  // target.innerHTML = `<p>エラー:${error.message} </p>`
});

以下のように書き換えます。

.catch((error) => {
  console.warn(error.message);
  // ログインしている場合はエラーをページに表示
  if ( document.body.classList.contains( 'logged-in' ) ) {
    target.innerHTML = `<div class="insta-error" style="background-color:pink; padding:20px; margin:50px 0;">
    <h2>エラー</h2>
    <p>${error.message} </p>
  </div>`;
  }
});

上記のコードでは、body 要素に body_class() が指定されていて、ユーザーがログインしていれば、logged-in クラスが body 要素に出力されるのを利用しています。

AJAX でログイン状態を確認する

もし、管理者としてログインしているかを判定する必要がある場合や body_class() を使っていない場合などでは、AJAX を使ってログイン状態を確認してエラーを表示することもできます。

以下は AJAX を使ってログイン状態を確認し、管理者としてログインしていれば、エラーが発生した際にエラーメッセージをページに表示する例です。

但し、おそらくもっと効率的なコードがあると思いますので、参考程度に。

[追記] この例の場合、AJAX を使わなくても wp_add_inline_script() を使ってログイン状態を出力すればもっと簡潔になります(使用例)。

JavaScript(insta-feeds.js)に AJAX リクエストを行い管理者としてログインしていればエラーを表示する関数 showErrorForAdmin() を追加して、60行目からの catch() で呼び出します。

import { accessToken, businessID } from "./insta-val.js";

document.addEventListener("DOMContentLoaded", () => {
  let next;
  let mediaLoaded = 0;
  const target = document.getElementById("insta-div");

  if (target) {
    const api = "https://graph.facebook.com/";
    const version = "v20.0";
    const limit = target.dataset.thumbsLimit ? parseInt(target.dataset.thumbsLimit) : 4;
    const field = "caption,media_url,thumbnail_url,permalink,like_count,comments_count,media_type";
    const fields = limit ? `media.limit(${limit}){${field}}` : `media{${field}}`;
    const params = {
      fields,
      access_token: accessToken,
    };
    const query = new URLSearchParams(params);
    const url = `${api}/${version}/${businessID}?${query}`;

    fetch(url)
      .then((response) => {
        if (response.ok) {
          return response.json();
        } else {
          throw new Error(
            `Insta Fetch リクエスト失敗:${response.status} ${response.statusText}`
          );
        }
      })
      .then((instaResponse) => {
        next = instaResponse.media.paging.next;
        const instaFeeds = createInstaFeeds(instaResponse.media.data);
        const maxMediaCount = target.dataset.maxMediaCount ? parseInt(target.dataset.maxMediaCount) : 0;
        mediaLoaded += limit;
        const headingText = target.dataset.headingText ? target.dataset.headingText : "";
        const headingTag = target.dataset.headingTag ? target.dataset.headingTag : "h2";
        const heading = headingText ? `<${headingTag} class="insta-icon">${headingText}</${headingTag}>` : "";
        if (!next || (maxMediaCount && maxMediaCount <= mediaLoaded)) {
          target.innerHTML = `<div class="insta-wrapper">
      ${heading}
      <div class="insta-container">${instaFeeds}</div>
    </div>`;
        } else {
          target.innerHTML = `<div class="insta-wrapper">
      ${heading}
      <div class="insta-container">${instaFeeds}</div>
      <button id="load-more-insta-feeds">Load More</button>
    </div>`;
          const loadMoreInstaFeeds = target.querySelector( "#load-more-insta-feeds");
          if (loadMoreInstaFeeds) {
            loadMoreInstaFeeds.addEventListener("click", () => {
              loadMoreFeeds(next, limit, maxMediaCount);
            });
          }
        }
      })
      .catch((error) => {
        console.warn(error.message);
        // 管理者としてログインしていればエラーを表示
        showErrorForAdmin(error.message);
      });
  }

  function createInstaFeeds(data) {
    let instaFeeds = "";
    let src = "";
    let video = "";
    data.forEach((item) => {
      if (item.media_type === "VIDEO") {
        src = item.thumbnail_url;
        video = '<span class="video-icon"></span>';
      }
      else {
        src = item.media_url;
        video = "";
      }
      const instaMediaDiv = `<div class="insta-media">
    <a href="${item.permalink}" target="_blank" rel="noopener">
      <img src="${src}" alt="${item.caption ? item.caption : ""}">
      ${video ? video : ""}
      <span class="like-count">${item.like_count}</span>
      <span class="comments-count">${item.comments_count}</span>
    </a>
  </div>
  `;
      instaFeeds += instaMediaDiv;
    });
    return instaFeeds;
  }

  function loadMoreFeeds(url, limit, maxMediaCount) {
    fetch(url)
      .then((response) => {
        if (response.ok) {
          return response.json();
        } else {
          throw new Error(
            `リクエスト失敗:${response.status} ${response.statusText}`
          );
        }
      })
      .then((instaResponse) => {
        const instaFeeds = createInstaFeeds(instaResponse.data);
        next = instaResponse.paging.next;
        const instaContainer = target.querySelector(".insta-container");
        if (instaContainer) {
          instaContainer.insertAdjacentHTML("beforeend", instaFeeds);
          mediaLoaded += limit;
          if (!next || (maxMediaCount && maxMediaCount <= mediaLoaded)) {
            target.querySelector("#load-more-insta-feeds").remove();
          }
        }
      });
  }

  // Ajax で管理者としてログインしているかを判定してエラーを表示する関数(引数 message には出力するエラーメッセージを受け取る)
  function showErrorForAdmin(message) {
    const data = {
      // アクション名
      action: "login_state_action",
      // nonce(ajax_params はインライン script に出力されたオブジェクト)
      _ajax_nonce: ajax_params.ajax_nonce,
    };
    const xhr = new XMLHttpRequest();
    xhr.responseType = "json";
    // リクエスト先 URL は ajax_params.ajaxurl
    xhr.open("POST", ajax_params.ajaxurl);
    xhr.addEventListener("readystatechange", () => {
      if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300) {
          // レスポンスの loggedin と isAdminUser が true であればエラーを表示
          if (xhr.response.loggedin && xhr.response.isAdminUser) {
            target.innerHTML = `<div class="insta-error" style="background-color:pink; padding:20px; margin:50px 0;">
            <h2>エラー</h2>
            <p>${message} </p>
          </div>`;
          }
        } else {
          console.log(`失敗: ${xhr.status} (${xhr.statusText})`);
        }
      }
    });
    xhr.send(new URLSearchParams(data));
  }
});

functions.php の wp_enqueue_script での insta-feeds.js の登録の後で、wp_add_inline_script を使って AJAX のリクエスト URL と nonce を出力します。

そして AJAX ハンドラを定義して、is_user_logged_in() と current_user_can() でログイン状態を取得してデータとして返します。

また、ID とトークンを別ファイルに保存して読み込むので、wp_script_attributes フィルターで insta-feeds.js のスクリプトタグに type="module" を追加します(script_loader_tag フィルタを使うと、wp_add_inline_script が機能しません)。

function add_my_insta_scripts() {
  if (is_front_page() || is_single()) {
    wp_enqueue_script(
      'instaFeeds',
      get_theme_file_uri('/insta/insta-feeds.js'),
      array(),
      filemtime(get_theme_file_path('/insta/insta-feeds.js')),
      true
    );

    // AJAX のリクエスト URL と nonce を出力
    wp_add_inline_script(
      // 上記で登録したハンドル名を指定
      'instaFeeds',
      // script タグに出力する JavaScript(変数 ajax_params に格納されたオブジェクト)
      'const ajax_params = ' . json_encode(array(
        'ajaxurl' => admin_url('admin-ajax.php'),
        'ajax_nonce' => wp_create_nonce('login-state-ajax-nonce'),
      )),
      // script タグを対象のスクリプトの前に出力するための指定
      'before'
    );
  }
}
add_action('wp_enqueue_scripts', 'add_my_insta_scripts');

// AJAX ハンドラの定義
function login_state_ajax_handler() {
  check_ajax_referer('login-state-ajax-nonce');
  // ユーザーのログイン状態を取得して返す
  $data = [
    'loggedin' => is_user_logged_in(),
    'isAdminUser' => current_user_can('manage_options'),
  ];
  wp_send_json($data);
}
$action_name = 'login_state_action';
add_action('wp_ajax_' . $action_name, 'login_state_ajax_handler');
add_action('wp_ajax_nopriv_' . $action_name, 'login_state_ajax_handler');

// insta-feeds.js をモジュールに
function add_type_attribute( $attributes ) {
  if ( isset( $attributes['id'] ) && $attributes['id'] === 'instaFeeds-js' ) {
    $attributes['type'] = 'module';
  }
  return $attributes;
}
add_filter( 'wp_script_attributes', 'add_type_attribute', 10, 1 );

上記の wp_add_inline_script() により、insta-feeds.js の script タグの出力の前にインラインで ajax_params の定義が出力され、これらのプロパティ(ajaxurl と ajax_nonce)に insta-feeds.js からアクセスできるようになります。

<script id="instaFeeds-js-before">
const ajax_params = {"ajaxurl":"http:\/\/localhost\/wp-sample\/wp-admin\/admin-ajax.php","ajax_nonce":"05f9db32a0"}
</script>
<script src="http://localhost/wp-sample/wp-content/themes/my-theme/insta/insta-feeds.js?ver=1722425318" id="instaFeeds-js" type="module"></script>