WordPress + WooCommerceプラグインでECショップを構築しました。このブログで紹介したオリジナル3Dプリント品を販売するサイトなので、こちらのブログ記事にもショップの当該アイテムページにリンクを貼りたい、それも画像入りカード形式でカッコ良くしたい、というのがゴール。
通常はWordPress同士ならoEmbedというOGPのようなヘッダー情報があり、WordPressのGutenbergエディタにURLをペーストするだけでカード形式になってくれます。しかしなぜか今回の2サイト間では上手くいかない。
こんな感じで「このコンテンツを埋め込めませんでした。」と出てカードに展開されません。

ChatGPTに言われたとおりにApacheの設定かえたり子テーマにPHPコード書いたり二日くらい格闘しましたが解決せず。JSONのエンドポイントをブラウザで開くとレスポンスコードが200できちんと表示されるのに、curlだと401になったり。
あきらめて前から活用しているPZLinkカードというプラグインを使うことも考えたのですが、こちらでも403になって取得できない。
次にEmbedlyというWebサービスベースのプラグインを入れて無事サムネ、タイトル、概要などは出るようになったんですが、iframeで描画されるのでCSSで詳細がカスタムできない。無料プランだとサービスロゴが表示されたりというのもやや気になります。
Geminiにプラグイン書いてもらう
ChatGPTの助言も堂々巡りで疲れ切っていた時、ふと「もう同じサーバー上にあるので、MariaDB(MySQL)のID/PWを使ってPHPで直接データ取得してカードをレンダリングするプラグインを自前で作ったれ、と閃きました。するとChatGPTもセカンドオピニオンとして聞いたGeminiも技術的に可能だと返答。加えてGeminiが「それもいいけど、WordPress(というかWooCommerce)にはREST APIで記事(商品情報)のメタデータを取得する機能がるから、そっち使うのも手だよ」と提案してきました。なるほど、そんないいものが。それなら(まずないけど)将来的にサーバーを分離しても取得できます。やりたいことを読み取って最適な別解も提示してくれるGeminiさん強い。
ということで、そこからは主にGeminiを使ってショートコードプラグインを作ってみたので、ざっくり手順を記録しておきます。
利用イメージは、Gutenbergエディターのショートコードブロックに「[shop id="123"]」のように入れることでカード展開されるというスタイルです。「[shop id="123,124"]」と複数のIDを入れるとカードが連続で並ぶところまで改良してもらいました。
APIにアクセスする認証キーを発行する
まずは読み出される側(ソース)のWordPressの管理画面を開き、WooCommerceプラグインの設定画面から「高度な設定」→「REST API」を開いて、「キーを追加」で発行します。今回は商品情報を参照するのみなので、権限を「Read」にしておけば万一キーが漏洩しても情報を書き換えられないのでお勧めです。

最終的に、Consumer KeyとConsumer Secretという2つの文字列が生成されるので、どこかにコピペしてメモっておきます(再表示できません)。
curlコマンドで動作チェック
商品IDを調べる
特定の商品を参照するID番号を調べます。WooCommerceの商品一覧で赤丸の箇所に表示されるのが商品IDです。

WindowsやMacの黒画面から以下を叩きます。
1 |
curl "https://hogeshop.com/wp-json/wc/v3/products/123?consumer_key=ck_abcdef1234567890&consumer_secret=cs_fedcba0987654321" |
123が商品ID、cunsumer_keyとconsumer_secretはさきほど作成したものに置き換えてください。
StatusCode:200とかJSONデータが返ってくれば成功です。黒画面だと途中までしか見られませんが、ちゃんと価格や在庫情報なども含まれています。これを読み込んでカードHTMLをレンダリングするプラグインを作っていきます。
認証キーを安全な場所(wp-config.php)に記述
プラグインのソースコードにREST API認証キーを直書きするのはセキュリティ的に危険なので、通常Web経由でアクセスできない場所にあるwp-config.php内に定義して、それをプラグインから読み出すようにします。具体的には以下の形式で記述。
1 2 3 4 5 6 7 |
// ********** WooCommerce APIの定数を定義 ********** define( 'WC_EXTERNAL_API_CONSUMER_KEY', 'ck_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ); define( 'WC_EXTERNAL_API_CONSUMER_SECRET', 'cs_yyyyyyyyyyyyyyyyyyyyyyyyyyyyy' ); define( 'WC_EXTERNAL_API_HOSTNAME', 'https://hogeshop.com' ); // ******************************************************* /* これ以上の編集は必要ありません ! WordPress の力を信じましょう。 */ ←これより手前に書く |
wp-config.phpはたぶんWordPress上からは読み書きできないので、シェルでサーバーにログインするか、FTP/SFPTで一旦ダウンロードして編集します。書き込み権限も削ってあると思うので、一時的にchmod +wする必要もあるでしょう。編集が終わったら-wも忘れずに。
WC_EXTERNAL_API_HOSTNAMEは便宜上ホスト名としていますが、もしWordPressがサブディレクトリにインストールされていたら、そのパスも書く必要があると思います。
プラグインのフォルダとファイルを作成
/wp-content/plugin/フォルダ下にshop-item-linkというフォルダを作ります。これがプラグイン名になります。さらに同じ名前でphpファイルshop-item-link.phpという名前で以下のコードをコピペして保存します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
<?php /** * Plugin Name: Shop Item Link * Description: 別のWooCommerceストアから商品情報を取得し、ショートコードで表示します。(キャッシュ無効化済み) * Version: 1.0 * Author: Kazuyoshi Furuta */ // 直接ファイルにアクセスされた場合の保護 if ( ! defined( 'ABSPATH' ) ) { exit; } // ---------------------------------------------------- // 1. ショートコードの登録 // ---------------------------------------------------- add_shortcode( 'shop', 'shop_item_link_product_shortcode' ); // ---------------------------------------------------- // 2. ショートコードのコールバック関数 // ---------------------------------------------------- function shop_item_link_product_shortcode( $atts ) { // ショートコード属性 (id属性にカンマ区切りの文字列を想定) $atts = shortcode_atts( array( 'id' => '', // 商品ID (カンマ区切り文字列) ), $atts, 'woo_product' ); $ids_string = trim( $atts['id'] ); // IDが空の場合はエラー if ( empty( $ids_string ) ) { return '<p style="color: red;">[woo_product] エラー: IDを指定してください。</p>'; } // カンマ区切り文字列を配列に変換し、数値のみを抽出 $product_ids = array_filter( array_map( 'intval', explode( ',', $ids_string ) ) ); if ( empty( $product_ids ) ) { return '<p style="color: red;">[woo_product] エラー: 有効な商品IDが指定されていません。</p>'; } // ---------------------------------------------------- // 3. 認証情報とURLの取得 (wp-config.phpから) // ---------------------------------------------------- $consumer_key = defined( 'WC_EXTERNAL_API_CONSUMER_KEY' ) ? WC_EXTERNAL_API_CONSUMER_KEY : ''; $consumer_secret = defined( 'WC_EXTERNAL_API_CONSUMER_SECRET' ) ? WC_EXTERNAL_API_CONSUMER_SECRET : ''; $site_url = defined( 'WC_EXTERNAL_API_HOSTNAME' ) ? WC_EXTERNAL_API_HOSTNAME : ''; // 認証情報・ホスト名の欠落チェック if ( empty( $consumer_key ) || empty( $consumer_secret ) || empty( $site_url ) ) { // ... 致命的エラー出力 (変更なし) ... $missing = []; if ( empty($consumer_key) ) $missing[] = 'Consumer Key'; if ( empty($consumer_secret) ) $missing[] = 'Consumer Secret'; if ( empty($site_url) ) $missing[] = 'Hostname'; $error_msg = '【致命的エラー】wp-config.phpで以下の定数が未定義です: ' . implode(', ', $missing); error_log( $error_msg ); return '<div style="background-color: #ffcccc; padding: 10px; border: 1px solid red;">' . esc_html($error_m\ sg) . '</div>'; // ---------------------------------------------------- // 4. 各IDを処理し、HTML出力を結合 // ---------------------------------------------------- $final_output = '<h2>こちらで注文できます</h2>'; foreach ( $product_ids as $product_id ) { // APIからデータを取得し、HTMLを生成するメイン処理 $final_output .= shop_item_link_fetch_and_render( $product_id, $site_url, $consumer_key, $consumer_secret )\ ; } return $final_output; } /** * 個別の商品情報をAPIから取得し、HTMLをレンダリングする関数 * この関数は、ショートコードのループから呼ばれます。 */ function shop_item_link_fetch_and_render( $product_id, $site_url, $consumer_key, $consumer_secret ) { // ホスト名末尾のスラッシュを削除し、エンドポイントを構築 $url = rtrim($site_url, '/') . "/wp-json/wc/v3/products/{$product_id}"; $authorization = base64_encode( "{$consumer_key}:{$consumer_secret}" ); $response = wp_remote_get( $url, array( 'headers' => array( 'Authorization' => 'Basic ' . $authorization, ), 'timeout' => 15, ) ); // ---------------------------------------------------- // APIレスポンスのチェック (デバッグ出力) // ---------------------------------------------------- if ( is_wp_error( $response ) ) { $error_message = $response->get_error_message(); error_log( 'API Request WP_Error: ' . $error_message . ' for ID: ' . $product_id ); return '<div style="background-color: #ffcc99; padding: 10px; border: 1px solid orange;">【API通信エラー】I\ D ' . $product_id . '、エラー: ' . esc_html($error_message) . '</div>'; } $status_code = wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); if ( $status_code < 200 || $status_code >= 300 ) { // ステータスコードエラー (401:認証失敗, 404:商品見つからずなど) error_log( 'API Response Code Error: ' . $status_code . '. Product ID: ' . $product_id . '. Body: ' . $body\ ); $debug_output = '<div style="background-color: #ffffcc; padding: 10px; border: 1px solid #c00;">'; $debug_output .= '【APIステータスエラー】商品ID ' . $product_id . ' の取得失敗 (Status Code: ' . $status_co\ de . ')<br>'; $error_obj = json_decode($body); if (isset($error_obj->message)) { $debug_output .= '詳細メッセージ: ' . esc_html($error_obj->message); } else { $debug_output .= 'サーバー応答ボディの抜粋: ' . esc_html(substr($body, 0, 100)) . '...'; } $debug_output .= '</div>'; return $debug_output; } $product_data = json_decode( $body ); // JSONデコードの失敗をチェック if ( json_last_error() !== JSON_ERROR_NONE ) { error_log( 'JSON Decode Error: ' . json_last_error_msg() . '. ID: ' . $product_id . '. Body: ' . $body ); return '<div style="background-color: #ffcccc; padding: 10px; border: 1px solid red;">【データエラー】ID ' \ . $product_id . ' の取得データの解析に失敗しました。</div>'; } if ( ! is_object( $product_data ) ) { return '<p style="color: red;">指定された商品ID ' . $product_id . ' の商品が見つかりません。</p>'; } // ---------------------------------------------------- // HTMLレンダリング // ---------------------------------------------------- $name = esc_html( $product_data->name ); $price = number_format( floatval( $product_data->price ) ); $permalink = esc_url( $product_data->permalink ); $image_url = ! empty( $product_data->images ) ? esc_url( $product_data->images[0]->src ) : ''; $stock_status = esc_html( $product_data->stock_status ); ob_start(); ?> <a href="<?php echo $permalink; ?>" target="_blank" style="text-decoration: none; color: #0073aa;"> <div class="custom-woo-product product-id-<?php echo $product_id; ?>" style="border: 1px solid #ddd; padding: 1\ 5px; margin: 15px 0; background-color:aliceblue;"> <h3 style="font-size:1.5em;"><?php echo $name; ?></h3> <?php if ( $image_url ): ?> <div class="product-image" style="margin-bottom: 10px;"> <img src="<?php echo $image_url; ?>" alt="<?php echo $name; ?>" loading="lazy" style="max-width: 80\ %; max-height: 300px;height: auto;"> </div> <?php endif; ?> <div class="product-details"> <p class="product-price" style="font-weight: bold; font-size: 1.2em;">参考価格: ¥<?php echo $price; ?><\ span style="color:#bbb; font-size:0.8em;">(別途消費税、送料がかかります)</span></p> <p class="product-stock <?php echo $stock_status; ?>" style="color: <?php echo ( $stock_status === 'ins\ tock' ? 'green' : '#999' ); ?>;"> 在庫: <?php echo ( $stock_status === 'instock' ? 'あり' : 'なし' ); ?> </p> </div> </div> </a> <?php return ob_get_clean(); } |
HTMLレンダリングブロックは適宜書き換えてください。
編集ポイント
6行目のAuthorは適当に自分の名前に書き換えます。
17行目の第一引数の「shop」が実際に使用するショートコードになります。
46~48行目がwp-config.phpに定義した認証キーとホストネーム情報を読み込むところです。大文字部分を1字たりとも相違なくあちらと揃える必要があります。
66行目でカードよりも前に1回だけ出力したいHTML(この場合は見出し)を定義しています。
151~170行目あたりがカードのHTMLです。面倒くさいのでスタイルシートも直書きしてますが、カードを繰り返したくさん出力するならきちんとCSSファイルで定義するべきでしょう。
また元々Geminiが生成したコードでは取得内容を1時間キャッシュする仕様だったんですが、ウチの場合は必要ないし、むしろ価格や在庫はリアルタイムに反映してほしいので無効化してあります。ところどころにその残骸があるかも。
ともあれ、これで、[shop id="998,999"]などと書くだけで、
こちらで注文できます
TOYOTA新型クラウン向けスペアホール→タイプDスイッチアダプター

参考価格: ¥1,800(別途消費税、送料がかかります)
在庫: あり
セキュリティカメラAnker Eufy SoloCam S340用雨樋/ポールマウント

参考価格: ¥2,273(別途消費税、送料がかかります)
在庫: あり
が出るようになりました。