WordPressで「一度作成してどこでも公開」

公開: 2022-03-10
簡単な要約↬COPE (「一度作成、どこでも公開」)という用語は、すべての信頼できる唯一の情報源を持つことにより、コンテンツをさまざまな出力(Webサイト、AMPサイト、電子メール、アプリなど)に公開するための方法論です。 。 WordPressを使用してCOPEを実装する方法を見てみましょう。

COPEは、コンテンツをWebサイト、電子メール、アプリなどのさまざまなメディアに公開するために必要な作業量を削減するための戦略です。 NPRによって最初に開拓されたこの製品は、さまざまなメディアすべてに使用できるコンテンツの信頼できる唯一の情報源を確立することで、その目標を達成します。

各メディアには独自の要件があるため、どこでも機能するコンテンツを用意することは簡単な作業ではありません。 たとえば、HTMLはWebのコンテンツの印刷には有効ですが、この言語はiOS / Androidアプリには無効です。 同様に、Web用のHTMLにクラスを追加できますが、これらは電子メール用のスタイルに変換する必要があります。

この難問の解決策は、フォームをコンテンツから分離することです。コンテンツの表示と意味を分離する必要があり、意味のみが唯一の正しい情報源として使用されます。 次に、プレゼンテーションを別のレイヤー(選択したメディアに固有)に追加できます。

たとえば、次のHTMLコードの場合、 <p>は主にWebに適用されるHTMLタグであり、属性class="align-center"はプレゼンテーションです(要素を「中央に」配置することは、画面ベースのメディアですが、Amazon Alexaなどのオーディオベースのメディアではありません):

 <p class="align-center">Hello world!</p>

したがって、このコンテンツを信頼できる唯一の情報源として使用することはできません。また、次のJSONコードなど、プレゼンテーションから意味を分離する形式に変換する必要があります。

 { content: "Hello world!", placement: "center", type: "paragraph" }

このコードは、Webで使用するHTMLコードをもう一度再作成し、他のメディアに適切な形式を取得できるため、コンテンツの信頼できる唯一の情報源として使用できます。

ジャンプした後もっと! 以下を読み続けてください↓

なぜWordPressなのか

WordPressは、いくつかの理由からCOPE戦略を実装するのに理想的です。

  • 用途が広いです。
    WordPressデータベースモデルは、固定された厳格なコンテンツモデルを定義していません。 それどころか、それは汎用性のために作成され、メタフィールドを使用してさまざまなコンテンツモデルを作成できるようにしました。これにより、投稿とカスタム投稿タイプ、ユーザー、コメント、分類法の4つの異なるエンティティの追加データを保存できます。タグとカテゴリ)。
  • 強力です。
    WordPressはCMS(コンテンツ管理システム)として輝いており、そのプラグインエコシステムにより、新しい機能を簡単に追加できます。
  • それは広く行き渡っています。
    ウェブサイトの1/3がWordPressで実行されていると推定されています。 次に、Webで作業しているかなりの数の人々が、WordPressを知っており、使用することができます。 開発者だけでなく、ブロガー、セールスマン、マーケティングスタッフなどもいます。 そうすれば、技術的なバックグラウンドに関係なく、多くのさまざまな利害関係者が、信頼できる唯一の情報源として機能するコンテンツを作成できるようになります。
  • ヘッドレスです。
    ヘッドレスは、コンテンツをプレゼンテーション層から切り離す機能であり、COPEを実装するための基本的な機能です(異なるメディアにデータをフィードできるようにするため)。

    バージョン4.7からWPREST APIをコアに組み込んだ後、そしてバージョン5.0でGutenbergがリリースされてから(多くのREST APIエンドポイントを実装する必要があった)、WordPressはヘッドレスCMSと見なすことができます。ほとんどのWordPressコンテンツが任意のスタック上に構築された任意のアプリケーションからRESTAPIを介してアクセスできます。

    さらに、最近作成されたWPGraphQLはWordPressとGraphQLを統合し、このますます人気が高まっているAPIを使用してWordPressから任意のアプリケーションにコンテンツをフィードできるようにします。 最後に、私自身のプロジェクトPoPは最近、WordPressデータをREST、GraphQL、またはPoPネイティブ形式としてエクスポートできるWordPress用のAPIの実装を追加しました。
  • ブロックの概念に基づいているため、COPEの実装を大幅に支援するブロックベースのエディターであるGutenbergがあります(以下のセクションで説明します)。

情報を表すブロブとブロック

BLOBは、データベースにまとめて保存される単一の情報単位です。 たとえば、ブロブに依存して情報を格納するCMSで以下のブログ投稿を作成すると、ブログ投稿のコンテンツが同じコンテンツを含む単一のデータベースエントリに格納されます。

 <p>Look at this wonderful tango:</p> <figure> <iframe width="951" height="535" src="https://www.youtube.com/embed/sxm3Xyutc1s" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> <figcaption>An exquisite tango performance</figcaption> </figure>

理解できるように、このブログ投稿からの重要な情報(段落の内容、URL、Youtubeビデオの寸法と属性など)には簡単にアクセスできません。それらのいずれかを取得したい場合HTMLコードを解析して抽出する必要がありますが、これは理想的なソリューションとはほど遠いものです。

ブロックの動作は異なります。 情報をブロックのリストとして表すことにより、よりセマンティックでアクセスしやすい方法でコンテンツを保存できます。 各ブロックは、そのタイプに依存する可能性のある独自のコンテンツと独自のプロパティを伝達します(たとえば、段落またはビデオですか?)。

たとえば、上記のHTMLコードは、次のようなブロックのリストとして表すことができます。

 { [ type: "paragraph", content: "Look at this wonderful tango:" ], [ type: "embed", provider: "Youtube", url: "https://www.youtube.com/embed/sxm3Xyutc1s", width: 951, height: 535, frameborder: 0, allowfullscreen: true, allow: "accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture", caption: "An exquisite tango performance" ] }

このように情報を表現することで、あらゆるデータを簡単に使用し、表示する必要のある特定のメディアに適合させることができます。 たとえば、ブログ投稿からすべての動画を抽出してカーエンターテインメントシステムに表示する場合は、すべての情報ブロックを繰り返し、 type="embed"provider="Youtube"の動画を選択して、それらからのURL。 同様に、Apple Watchでビデオを表示する場合は、ビデオのサイズを気にする必要がないため、属性のwidthheightを簡単に無視できます。

グーテンベルクがブロックを実装する方法

WordPressバージョン5.0より前は、WordPressはblobを使用して投稿コンテンツをデータベースに保存していました。 バージョン5.0以降、WordPressにはブロックベースのエディターであるGutenbergが同梱されており、上記のコンテンツを処理するための拡張された方法が可能になります。これは、COPEの実装に向けたブレークスルーを表しています。 残念ながら、グーテンベルクはこの特定のユースケース向けに設計されておらず、情報の表現はブロックについて説明したものとは異なるため、対処する必要のあるいくつかの不便が生じます。

まず、上記のブログ投稿がグーテンベルクを通じてどのように保存されるかを垣間見てみましょう。

 <!-- wp:paragraph --> <p>Look at this wonderful tango:</p> <!-- /wp:paragraph --> <!-- wp:core-embed/youtube {"url":"https://www.youtube.com/embed/sxm3Xyutc1s","type":"rich","providerNameSlug":"embed-handler","className":"wp-embed-aspect-16-9 wp-has-aspect-ratio"} --> <figure class="wp-block-embed-youtube wp-block-embed is-type-rich is-provider-embed-handler wp-embed-aspect-16-9 wp-has-aspect-ratio"> <div class="wp-block-embed__wrapper"> https://www.youtube.com/embed/sxm3Xyutc1s </div> <figcaption>An exquisite tango performance</figcaption> </figure> <!-- /wp:core-embed/youtube -->

このコードから、次のことがわかります。

ブロックは同じデータベースエントリにすべて一緒に保存されます

上記のコードには2つのブロックがあります。

 <!-- wp:paragraph --> <p>Look at this wonderful tango:</p> <!-- /wp:paragraph -->
 <!-- wp:core-embed/youtube {"url":"https://www.youtube.com/embed/sxm3Xyutc1s","type":"rich","providerNameSlug":"embed-handler","className":"wp-embed-aspect-16-9 wp-has-aspect-ratio"} --> <figure class="wp-block-embed-youtube wp-block-embed is-type-rich is-provider-embed-handler wp-embed-aspect-16-9 wp-has-aspect-ratio"> <div class="wp-block-embed__wrapper"> https://www.youtube.com/embed/sxm3Xyutc1s </div> <figcaption>An exquisite tango performance</figcaption> </figure> <!-- /wp:core-embed/youtube -->

データベースに独自のエントリがあり、IDを介して直接参照できるグローバル(「再利用可能」とも呼ばれる)ブロックを除いて、すべてのブロックはブログ投稿のテーブルwp_postsのエントリに一緒に保存されます。

したがって、特定のブロックの情報を取得するには、最初にコンテンツを解析し、すべてのブロックを相互に分離する必要があります。 便利なことに、WordPressにはこれを行うための関数parse_blocks($content)が用意されています。 この関数は、ブログ投稿のコンテンツ(HTML形式)を含む文字列を受け取り、含まれているすべてのブロックのデータを含むJSONオブジェクトを返します。

ブロックタイプと属性はHTMLコメントを通じて伝達されます

各ブロックは、開始タグ<!-- wp:{block-type} {block-attributes-encoded-as-JSON} -->と終了タグ<!-- /wp:{block-type} -->で区切られます。 <!-- /wp:{block-type} -->これ(HTMLコメント)は、Webサイトに表示するときにこの情報が表示されないようにします。 ただし、HTMLコメントが表示され、文字化けしたコンテンツとして表示される可能性があるため、ブログ投稿を別のメディアに直接表示することはできません。 ただし、これは大したことではありません。関数parse_blocks($content)を使用してコンテンツを解析した後、HTMLコメントが削除され、ブロックデータをJSONオブジェクトとして直接操作できるためです。

HTMLを含むブロック

段落ブロックの内容には、 "Look at this wonderful tango:"ではなく、 "<p>Look at this wonderful tango:</p>"が含まれています。 したがって、他のメディアには役立たないHTMLコード(タグ<p>および</p> )が含まれているため、たとえばPHP関数strip_tags($content)を使用して削除する必要があります。

タグを削除する場合、タグ<strong><em> (画面ベースのメディアにのみ適用される対応する<b><i>の代わりに)などのセマンティック情報を明示的に伝達するHTMLタグを保持できます。他のすべてのタグを削除します。 これは、セマンティックタグが他のメディアでも適切に解釈される可能性が高いためです(たとえば、Amazon Alexaはタグ<strong><em>を認識し、テキストを読むときにそれに応じて音声とイントネーションを変更できます)。 これを行うには、許可されたタグを含む2番目のパラメーターを指定してstrip_tags関数を呼び出し、便宜上、ラッピング関数内に配置します。

 function strip_html_tags($content) { return strip_tags($content, '<strong><em>'); }

ビデオのキャプションは、属性としてではなくHTML内に保存されます

Youtubeビデオブロックに見られるように、キャプション"An exquisite tango performance"はHTMLコード(タグ<figcaption />で囲まれています)内に保存されますが、JSONエンコードされた属性オブジェクト内には保存されません。 結果として、キャプションを抽出するには、たとえば正規表現を使用してブロックコンテンツを解析する必要があります。

 function extract_caption($content) { $matches = []; preg_match('/<figcaption>(.*?)<\/figcaption>/', $content, $matches); if ($caption = $matches[1]) { return strip_html_tags($caption); } return null; }

これは、グーテンベルクブロックからすべてのメタデータを抽出するために克服しなければならないハードルです。 これはいくつかのブロックで発生します。 すべてのメタデータが属性として保存されるわけではないため、最初にこれらのメタデータを特定してから、HTMLコンテンツを解析して、ブロックごとおよびピースごとに抽出する必要があります。

COPEに関しては、これは本当に最適なソリューションを持つための無駄なチャンスを表しています。 代替オプションは、情報を複製し、HTML内と属性の両方として保存し、DRY( D on't R epeat Y ourself)の原則に違反するため、理想的ではないと主張することもできます。 ただし、この違反はすでに発生しています。たとえば、属性classNameには、HTML属性classの下のコンテンツ内にも出力される値"wp-embed-aspect-16-9 wp-has-aspect-ratio"が含まれています。

グーテンベルクを介してコンテンツを追加する
グーテンベルクによるコンテンツの追加(大プレビュー)

COPEの実装

注: WordPressプラグインのブロックメタデータとして、以下で説明するすべてのコードを含むこの機能をリリースしました。 COPEのパワーを味わうことができるように、インストールして試してみてください。 ソースコードは、このGitHubリポジトリで入手できます。

ブロックの内部表現がどのように見えるかがわかったので、Gutenbergを介してCOPEの実装に進みましょう。 手順には、次の手順が含まれます。

  1. 関数parse_blocks($content)はネストされたレベルのJSONオブジェクトを返すため、最初にこの構造を単純化する必要があります。
  2. すべてのブロックを繰り返し、それぞれについて、メタデータの断片を識別して抽出し、プロセスで中程度の不可知論的な形式に変換します。 応答に追加される属性は、ブロックタイプによって異なります。
  3. 最終的に、API(REST / GraphQL / PoP)を介してデータを利用できるようにします。

これらの手順を1つずつ実装していきましょう。

1.JSONオブジェクトの構造を単純化する

関数parse_blocks($content)から返されるJSONオブジェクトはネストされたアーキテクチャであり、通常のブロックのデータは最初のレベルに表示されますが、参照される再利用可能なブロックのデータはありません(参照するブロックのデータのみが追加されます)。ネストされたブロック(他のブロック内に追加される)およびグループ化されたブロック(複数のブロックをグループ化できる場合)のデータは、1つ以上のサブレベルの下に表示されます。 このアーキテクチャでは、投稿コンテンツ内のすべてのブロックからのブロックデータを処理することが困難になります。これは、一方の側では一部のデータが欠落しており、もう一方の側では、データがいくつのレベルにあるかが事前にわからないためです。 さらに、ブロックのペアごとに配置されたブロック仕切りがあり、コンテンツは含まれていません。これは無視しても問題ありません。

たとえば、単純なブロック、グローバルブロック、単純なブロックを含むネストされたブロック、および単純なブロックのグループをこの順序で含む投稿から取得される応答は、次のとおりです。

 [ // Simple block { "blockName": "core/image", "attrs": { "id": 70, "sizeSlug": "large" }, "innerBlocks": [], "innerHTML": "\n<figure class=\"wp-block-image size-large\"><img src=\"https://localhost/wp-content/uploads/2017/12/sandwich-1024x614.jpg\" alt=\"\" class=\"wp-image-70\"/><figcaption>This is a normal block</figcaption></figure>\n", "innerContent": [ "\n<figure class=\"wp-block-image size-large\"><img src=\"https://localhost/wp-content/uploads/2017/12/sandwich-1024x614.jpg\" alt=\"\" class=\"wp-image-70\"/><figcaption>This is a normal block</figcaption></figure>\n" ] }, // Empty block divider { "blockName": null, "attrs": [], "innerBlocks": [], "innerHTML": "\n\n", "innerContent": [ "\n\n" ] }, // Reference to reusable block { "blockName": "core/block", "attrs": { "ref": 218 }, "innerBlocks": [], "innerHTML": "", "innerContent": [] }, // Empty block divider { "blockName": null, "attrs": [], "innerBlocks": [], "innerHTML": "\n\n", "innerContent": [ "\n\n" ] }, // Nested block { "blockName": "core/columns", "attrs": [], // Contained nested blocks "innerBlocks": [ { "blockName": "core/column", "attrs": [], // Contained nested blocks "innerBlocks": [ { "blockName": "core/image", "attrs": { "id": 69, "sizeSlug": "large" }, "innerBlocks": [], "innerHTML": "\n<figure class=\"wp-block-image size-large\"><img src=\"https://localhost/wp-content/uploads/2017/12/espresso-1024x614.jpg\" alt=\"\" class=\"wp-image-69\"/></figure>\n", "innerContent": [ "\n<figure class=\"wp-block-image size-large\"><img src=\"https://localhost/wp-content/uploads/2017/12/espresso-1024x614.jpg\" alt=\"\" class=\"wp-image-69\"/></figure>\n" ] } ], "innerHTML": "\n<div class=\"wp-block-column\"></div>\n", "innerContent": [ "\n<div class=\"wp-block-column\">", null, "</div>\n" ] }, { "blockName": "core/column", "attrs": [], // Contained nested blocks "innerBlocks": [ { "blockName": "core/paragraph", "attrs": [], "innerBlocks": [], "innerHTML": "\n<p>This is how I wake up every morning</p>\n", "innerContent": [ "\n<p>This is how I wake up every morning</p>\n" ] } ], "innerHTML": "\n<div class=\"wp-block-column\"></div>\n", "innerContent": [ "\n<div class=\"wp-block-column\">", null, "</div>\n" ] } ], "innerHTML": "\n<div class=\"wp-block-columns\">\n\n</div>\n", "innerContent": [ "\n<div class=\"wp-block-columns\">", null, "\n\n", null, "</div>\n" ] }, // Empty block divider { "blockName": null, "attrs": [], "innerBlocks": [], "innerHTML": "\n\n", "innerContent": [ "\n\n" ] }, // Block group { "blockName": "core/group", "attrs": [], // Contained grouped blocks "innerBlocks": [ { "blockName": "core/image", "attrs": { "id": 71, "sizeSlug": "large" }, "innerBlocks": [], "innerHTML": "\n<figure class=\"wp-block-image size-large\"><img src=\"https://localhost/wp-content/uploads/2017/12/coffee-1024x614.jpg\" alt=\"\" class=\"wp-image-71\"/><figcaption>First element of the group</figcaption></figure>\n", "innerContent": [ "\n<figure class=\"wp-block-image size-large\"><img src=\"https://localhost/wp-content/uploads/2017/12/coffee-1024x614.jpg\" alt=\"\" class=\"wp-image-71\"/><figcaption>First element of the group</figcaption></figure>\n" ] }, { "blockName": "core/paragraph", "attrs": [], "innerBlocks": [], "innerHTML": "\n<p>Second element of the group</p>\n", "innerContent": [ "\n<p>Second element of the group</p>\n" ] } ], "innerHTML": "\n<div class=\"wp-block-group\"><div class=\"wp-block-group__inner-container\">\n\n</div></div>\n", "innerContent": [ "\n<div class=\"wp-block-group\"><div class=\"wp-block-group__inner-container\">", null, "\n\n", null, "</div></div>\n" ] } ]

より良い解決策は、すべてのデータを最初のレベルに置くことです。これにより、すべてのブロックデータを反復処理するロジックが大幅に簡素化されます。 したがって、これらの再利用可能/ネスト/グループ化されたブロックのデータをフェッチし、それを第1レベルにも追加する必要があります。 上記のJSONコードでわかるように、次のようになります。

  • 空の仕切りブロックには、値がNULLの属性"blockName"があります
  • 再利用可能なブロックへの参照は、 $block["attrs"]["ref"]を介して定義されます。
  • ネストされたブロックとグループブロックは、 $block["innerBlocks"]の下に含まれるブロックを定義します

したがって、次のPHPコードは、空の仕切りブロックを削除し、再利用可能/ネスト/グループ化されたブロックを識別して、それらのデータを最初のレベルに追加し、すべてのサブレベルからすべてのデータを削除します。

 /** * Export all (Gutenberg) blocks' data from a WordPress post */ function get_block_data($content, $remove_divider_block = true) { // Parse the blocks, and convert them into a single-level array $ret = []; $blocks = parse_blocks($content); recursively_add_blocks($ret, $blocks); // Maybe remove blocks without name if ($remove_divider_block) { $ret = remove_blocks_without_name($ret); } // Remove 'innerBlocks' property if it exists (since that code was copied to the first level, it is currently duplicated) foreach ($ret as &$block) { unset($block['innerBlocks']); } return $ret; } /** * Remove the blocks without name, such as the empty block divider */ function remove_blocks_without_name($blocks) { return array_values(array_filter( $blocks, function($block) { return $block['blockName']; } )); } /** * Add block data (including global and nested blocks) into the first level of the array */ function recursively_add_blocks(&$ret, $blocks) { foreach ($blocks as $block) { // Global block: add the referenced block instead of this one if ($block['attrs']['ref']) { $ret = array_merge( $ret, recursively_render_block_core_block($block['attrs']) ); } // Normal block: add it directly else { $ret[] = $block; } // If it contains nested or grouped blocks, add them too if ($block['innerBlocks']) { recursively_add_blocks($ret, $block['innerBlocks']); } } } /** * Function based on `render_block_core_block` */ function recursively_render_block_core_block($attributes) { if (empty($attributes['ref'])) { return []; } $reusable_block = get_post($attributes['ref']); if (!$reusable_block || 'wp_block' !== $reusable_block->post_type) { return []; } if ('publish' !== $reusable_block->post_status || ! empty($reusable_block->post_password)) { return []; } return get_block_data($reusable_block->post_content); }

投稿コンテンツ( $post->post_content )をパラメーターとして渡す関数get_block_data($content)を呼び出すと、次の応答が得られます。

 [[ { "blockName": "core/image", "attrs": { "id": 70, "sizeSlug": "large" }, "innerHTML": "\n<figure class=\"wp-block-image size-large\"><img src=\"https://localhost/wp-content/uploads/2017/12/sandwich-1024x614.jpg\" alt=\"\" class=\"wp-image-70\"/><figcaption>This is a normal block</figcaption></figure>\n", "innerContent": [ "\n<figure class=\"wp-block-image size-large\"><img src=\"https://localhost/wp-content/uploads/2017/12/sandwich-1024x614.jpg\" alt=\"\" class=\"wp-image-70\"/><figcaption>This is a normal block</figcaption></figure>\n" ] }, { "blockName": "core/paragraph", "attrs": [], "innerHTML": "\n<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>\n", "innerContent": [ "\n<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>\n" ] }, { "blockName": "core/columns", "attrs": [], "innerHTML": "\n<div class=\"wp-block-columns\">\n\n</div>\n", "innerContent": [ "\n<div class=\"wp-block-columns\">", null, "\n\n", null, "</div>\n" ] }, { "blockName": "core/column", "attrs": [], "innerHTML": "\n<div class=\"wp-block-column\"></div>\n", "innerContent": [ "\n<div class=\"wp-block-column\">", null, "</div>\n" ] }, { "blockName": "core/image", "attrs": { "id": 69, "sizeSlug": "large" }, "innerHTML": "\n<figure class=\"wp-block-image size-large\"><img src=\"https://localhost/wp-content/uploads/2017/12/espresso-1024x614.jpg\" alt=\"\" class=\"wp-image-69\"/></figure>\n", "innerContent": [ "\n<figure class=\"wp-block-image size-large\"><img src=\"https://localhost/wp-content/uploads/2017/12/espresso-1024x614.jpg\" alt=\"\" class=\"wp-image-69\"/></figure>\n" ] }, { "blockName": "core/column", "attrs": [], "innerHTML": "\n<div class=\"wp-block-column\"></div>\n", "innerContent": [ "\n<div class=\"wp-block-column\">", null, "</div>\n" ] }, { "blockName": "core/paragraph", "attrs": [], "innerHTML": "\n<p>This is how I wake up every morning</p>\n", "innerContent": [ "\n<p>This is how I wake up every morning</p>\n" ] }, { "blockName": "core/group", "attrs": [], "innerHTML": "\n<div class=\"wp-block-group\"><div class=\"wp-block-group__inner-container\">\n\n</div></div>\n", "innerContent": [ "\n<div class=\"wp-block-group\"><div class=\"wp-block-group__inner-container\">", null, "\n\n", null, "</div></div>\n" ] }, { "blockName": "core/image", "attrs": { "id": 71, "sizeSlug": "large" }, "innerHTML": "\n<figure class=\"wp-block-image size-large\"><img src=\"https://localhost/wp-content/uploads/2017/12/coffee-1024x614.jpg\" alt=\"\" class=\"wp-image-71\"/><figcaption>First element of the group</figcaption></figure>\n", "innerContent": [ "\n<figure class=\"wp-block-image size-large\"><img src=\"https://localhost/wp-content/uploads/2017/12/coffee-1024x614.jpg\" alt=\"\" class=\"wp-image-71\"/><figcaption>First element of the group</figcaption></figure>\n" ] }, { "blockName": "core/paragraph", "attrs": [], "innerHTML": "\n<p>Second element of the group</p>\n", "innerContent": [ "\n<p>Second element of the group</p>\n" ] } ]

厳密には必要ではありませんが、新しい関数get_block_data($content)の結果を出力するREST APIエンドポイントを作成すると非常に役立ちます。これにより、特定の投稿に含まれるブロックとその内容を簡単に理解できます。構造化。 以下のコードは、そのようなエンドポイントを/wp-json/block-metadata/v1/data/{POST_ID}の下に追加します。

 /** * Define REST endpoint to visualize a post's block data */ add_action('rest_api_init', function () { register_rest_route('block-metadata/v1', 'data/(?P \d+)', [ 'methods' => 'GET', 'callback' => 'get_post_blocks' ]); }); function get_post_blocks($request) { $post = get_post($request['post_id']); if (!$post) { return new WP_Error('empty_post', 'There is no post with this ID', array('status' => 404)); } $block_data = get_block_data($post->post_content); $response = new WP_REST_Response($block_data); $response->set_status(200); return $response; } /** * Define REST endpoint to visualize a post's block data */ add_action('rest_api_init', function () { register_rest_route('block-metadata/v1', 'data/(?P \d+)', [ 'methods' => 'GET', 'callback' => 'get_post_blocks' ]); }); function get_post_blocks($request) { $post = get_post($request['post_id']); if (!$post) { return new WP_Error('empty_post', 'There is no post with this ID', array('status' => 404)); } $block_data = get_block_data($post->post_content); $response = new WP_REST_Response($block_data); $response->set_status(200); return $response; }

実際の動作を確認するには、この投稿のデータをエクスポートするこのリンクを確認してください。

2.すべてのブロックメタデータを中程度の不可知論的な形式に抽出する

この段階では、COPEに適さないHTMLコードを含むブロックデータがあります。 したがって、ブロックを中程度に依存しない形式に変換するために、各ブロックの非セマンティックHTMLタグを削除する必要があります。

ブロックタイプごとに抽出する必要のある属性を決定できます(たとえば、 "paragraph"ブロックのテキスト配置プロパティ、 "youtube embed"ブロックのビデオURLプロパティなどを抽出します)。 。

前に見たように、すべての属性が実際にブロック属性として保存されるのではなく、ブロックの内部コンテンツ内に保存されるため、これらの状況では、これらのメタデータを抽出するために正規表現を使用してHTMLコンテンツを解析する必要があります。

WordPressコアを介して出荷されたすべてのブロックを検査した後、次のブロックのメタデータを抽出しないことにしました。

"core/columns"
"core/column"
"core/cover"
これらは画面ベースのメディアにのみ適用され、(ネストされたブロックであるため)処理が困難です。
"core/html" これはWebでのみ意味があります。
"core/table"
"core/button"
"core/media-text"
中程度の不可知論的な方法でデータを表現する方法や、それが理にかなっているのかどうかさえわかりませんでした。

これにより、次のブロックが残ります。そのために、メタデータの抽出に進みます。

  • 'core/paragraph'
  • 'core/image'
  • 'core-embed/youtube' (すべての'core-embed'ブロックの代表として)
  • 'core/heading'
  • 'core/gallery'
  • 'core/list'
  • 'core/audio'
  • 'core/file'
  • 'core/video'
  • 'core/code'
  • 'core/preformatted'
  • 'core/quote''core/pullquote'
  • 'core/verse'

メタデータを抽出するために、各ブロックのブロックデータ(つまり、以前に実装した関数get_block_dataからの出力)とブロックタイプ(プロパティ"blockName"で提供)に応じて配列を受け取る関数get_block_metadata($block_data)を作成します。 )、必要な属性とそれらを抽出する方法を決定します。

 /** * Process all (Gutenberg) blocks' metadata into a medium-agnostic format from a WordPress post */ function get_block_metadata($block_data) { $ret = []; foreach ($block_data as $block) { $blockMeta = null; switch ($block['blockName']) { case ...: $blockMeta = ... break; case ...: $blockMeta = ... break; ... } if ($blockMeta) { $ret[] = [ 'blockName' => $block['blockName'], 'meta' => $blockMeta, ]; } } return $ret; }

各ブロックタイプのメタデータを1つずつ抽出していきましょう。

“core/paragraph”

コンテンツからHTMLタグを削除し、末尾のブレークラインを削除するだけです。

 case 'core/paragraph': $blockMeta = [ 'content' => trim(strip_html_tags($block['innerHTML'])), ]; break;

'core/image'

ブロックには、アップロードされたメディアファイルを参照するIDがあるか、そうでない場合は、 <img src="...">の下から画像ソースを抽出する必要があります。 いくつかの属性(caption、linkDestination、link、alignment)はオプションです。

 case 'core/image': $blockMeta = []; // If inserting the image from the Media Manager, it has an ID if ($block['attrs']['id'] && $img = wp_get_attachment_image_src($block['attrs']['id'], $block['attrs']['sizeSlug'])) { $blockMeta['img'] = [ 'src' => $img[0], 'width' => $img[1], 'height' => $img[2], ]; } elseif ($src = extract_image_src($block['innerHTML'])) { $blockMeta['src'] = $src; } if ($caption = extract_caption($block['innerHTML'])) { $blockMeta['caption'] = $caption; } if ($linkDestination = $block['attrs']['linkDestination']) { $blockMeta['linkDestination'] = $linkDestination; if ($link = extract_link($block['innerHTML'])) { $blockMeta['link'] = $link; } } if ($align = $block['attrs']['align']) { $blockMeta['align'] = $align; } break;

関数extract_image_srcextract_captionextract_linkを作成することは理にかなっています。これらの正規表現は、いくつかのブロックで何度も使用されるためです。 グーテンベルクのキャプションにはリンク( <a href="..."> )を含めることができますが、 strip_html_tagsを呼び出すと、これらはキャプションから削除されることに注意してください。

残念ながら、Web以外のプラットフォームでのリンクを保証することはできないため、この方法は避けられません。 したがって、コンテンツはさまざまなメディアで使用できるため普遍性を増していますが、特異性も失われているため、特定のプラットフォーム用に作成およびカスタマイズされたコンテンツと比較して品質が低くなります。

 function extract_caption($innerHTML) { $matches = []; preg_match('/<figcaption>(.*?)<\/figcaption>/', $innerHTML, $matches); if ($caption = $matches[1]) { return strip_html_tags($caption); } return null; } function extract_link($innerHTML) { $matches = []; preg_match('/<a href="(.*?)">(.*?)<\/a>>', $innerHTML, $matches); if ($link = $matches[1]) { return $link; } return null; } function extract_image_src($innerHTML) { $matches = []; preg_match('/<img src="(.*?)"/', $innerHTML, $matches); if ($src = $matches[1]) { return $src; } return null; }

'core-embed/youtube'

ブロック属性からビデオURLを取得し、HTMLコンテンツ(存在する場合)からそのキャプションを抽出するだけです。

 case 'core-embed/youtube': $blockMeta = [ 'url' => $block['attrs']['url'], ]; if ($caption = extract_caption($block['innerHTML'])) { $blockMeta['caption'] = $caption; } break;

'core/heading'

ヘッダーサイズ(h1、h2、…、h6)と見出しテキストはどちらも属性ではないため、これらはHTMLコンテンツから取得する必要があります。 ヘッダーのHTMLタグを返す代わりに、 size属性は単に同等の表現であることに注意してください。これは、より不可知論的であり、Web以外のプラットフォームに適しています。

 case 'core/heading': $matches = []; preg_match('/<h[1-6])>(.*?)<\/h([1-6])>/', $block['innerHTML'], $matches); $sizes = [ null, 'xxl', 'xl', 'l', 'm', 'sm', 'xs', ]; $blockMeta = [ 'size' => $sizes[$matches[1]], 'heading' => $matches[2], ]; break;

'core/gallery'

残念ながら、画像ギャラリーの場合、これらは属性ではないため、各画像からキャプションを抽出できませんでした。単純な正規表現によるキャプションの抽出は失敗する可能性があります。最初と3番目の要素にキャプションがあるが、 2つ目は、どのキャプションがどの画像に対応しているかわかりません(そして、複雑な正規表現を作成するために時間を費やしていません)。 同様に、以下のロジックでは、常に"full"画像サイズを取得していますが、そうである必要はなく、より適切なサイズを推測する方法がわかりません。

 case 'core/gallery': $imgs = []; foreach ($block['attrs']['ids'] as $img_id) { $img = wp_get_attachment_image_src($img_id, 'full'); $imgs[] = [ 'src' => $img[0], 'width' => $img[1], 'height' => $img[2], ]; } $blockMeta = [ 'imgs' => $imgs, ]; break;

'core/list'

<li>要素をアイテムの配列に変換するだけです。

 case 'core/list': $matches = []; preg_match_all('/<li>(.*?)<\/li>/', $block['innerHTML'], $matches); if ($items = $matches[1]) { $blockMeta = [ 'items' => array_map('strip_html_tags', $items), ]; } break;

'core/audio'

対応するアップロードされたメディアファイルのURLを取得します。

 case 'core/audio': $blockMeta = [ 'src' => wp_get_attachment_url($block['attrs']['id']), ]; break;

'core/file'

ファイルのURLは属性ですが、そのテキストは内部コンテンツから抽出する必要があります。

 case 'core/file': $href = $block['attrs']['href']; $matches = []; preg_match('/<a href="'.str_replace('/', '\/', $href).'">(.*?)<\/a>/', $block['innerHTML'], $matches); $blockMeta = [ 'href' => $href, 'text' => strip_html_tags($matches[1]), ]; break;

'core/video'

ビデオのURLとすべてのプロパティを取得して、正規表現を介してビデオを再生する方法を構成します。 グーテンベルクがこれらのプロパティがコードに出力される順序を変更すると、この正規表現は機能しなくなり、ブロック属性を介してメタデータを直接追加しないという問題の1つが明らかになります。

 case 'core/video': $matches = []; preg_match('/ 

'core/code'

Simply extract the code from within <code /> .

 case 'core/code': $matches = []; preg_match('/<code>(.*?)<\/code>/is', $block['innerHTML'], $matches); $blockMeta = [ 'code' => $matches[1], ]; break;

'core/preformatted'

Similar to <code /> , but we must watch out that Gutenberg hardcodes a class too.

 case 'core/preformatted': $matches = []; preg_match('/<pre class="wp-block-preformatted">(.*?)<\/pre>/is', $block['innerHTML'], $matches); $blockMeta = [ 'text' => strip_html_tags($matches[1]), ]; break;

'core/quote' and 'core/pullquote'

We must convert all inner <p /> tags to their equivalent generic "\n" character.

 case 'core/quote': case 'core/pullquote': $matches = []; $regexes = [ 'core/quote' => '/<blockquote class=\"wp-block-quote\">(.*?)<\/blockquote>/', 'core/pullquote' => '/<figure class=\"wp-block-pullquote\"><blockquote>(.*?)<\/blockquote><\/figure>/', ]; preg_match($regexes[$block['blockName']], $block['innerHTML'], $matches); if ($quoteHTML = $matches[1]) { preg_match_all('/<p>(.*?)<\/p>/', $quoteHTML, $matches); $blockMeta = [ 'quote' => strip_html_tags(implode('\n', $matches[1])), ]; preg_match('/<cite>(.*?)<\/cite>/', $quoteHTML, $matches); if ($cite = $matches[1]) { $blockMeta['cite'] = strip_html_tags($cite); } } break;

'core/verse'

Similar situation to <pre /> .

 case 'core/verse': $matches = []; preg_match('/<pre class="wp-block-verse">(.*?)<\/pre>/is', $block['innerHTML'], $matches); $blockMeta = [ 'text' => strip_html_tags($matches[1]), ]; break;

3. Exporting Data Through An API

Now that we have extracted all block metadata, we need to make it available to our different mediums, through an API. WordPress has access to the following APIs:

  • REST, through the WP REST API (integrated in WordPress core)
  • GraphQL, through WPGraphQL
  • PoP, through its implementation for WordPress

Let's see how to export the data through each of them.

残り

The following code creates endpoint /wp-json/block-metadata/v1/metadata/{POST_ID} which exports all block metadata for a specific post:

 /** * Define REST endpoints to export the blocks' metadata for a specific post */ add_action('rest_api_init', function () { register_rest_route('block-metadata/v1', 'metadata/(?P \d+)', [ 'methods' => 'GET', 'callback' => 'get_post_block_meta' ]); }); function get_post_block_meta($request) { $post = get_post($request['post_id']); if (!$post) { return new WP_Error('empty_post', 'There is no post with this ID', array('status' => 404)); } $block_data = get_block_data($post->post_content); $block_metadata = get_block_metadata($block_data); $response = new WP_REST_Response($block_metadata); $response->set_status(200); return $response; } /** * Define REST endpoints to export the blocks' metadata for a specific post */ add_action('rest_api_init', function () { register_rest_route('block-metadata/v1', 'metadata/(?P \d+)', [ 'methods' => 'GET', 'callback' => 'get_post_block_meta' ]); }); function get_post_block_meta($request) { $post = get_post($request['post_id']); if (!$post) { return new WP_Error('empty_post', 'There is no post with this ID', array('status' => 404)); } $block_data = get_block_data($post->post_content); $block_metadata = get_block_metadata($block_data); $response = new WP_REST_Response($block_metadata); $response->set_status(200); return $response; }

To see it working, this link (corresponding to this blog post) displays the metadata for blocks of all the types analyzed earlier on.

GraphQL (Through WPGraphQL)

GraphQL works by setting-up schemas and types which define the structure of the content, from which arises this API's power to fetch exactly the required data and nothing else. Setting-up schemas works very well when the structure of the object has a unique representation.

In our case, however, the metadata returned by a new field "block_metadata" (which calls our newly-created function get_block_metadata ) depends on the specific block type, so the structure of the response can vary wildly; GraphQL provides a solution to this issue through a Union type, allowing to return one among a set of different types. However, its implementation for all different variations of the metadata structure has proved to be a lot of work, and I quit along the way .

As an alternative (not ideal) solution, I decided to provide the response by simply encoding the JSON object through a new field "jsonencoded_block_metadata" :

 /** * Define WPGraphQL field "jsonencoded_block_metadata" */ add_action('graphql_register_types', function() { register_graphql_field( 'Post', 'jsonencoded_block_metadata', [ 'type' => 'String', 'description' => __('Post blocks encoded as JSON', 'wp-graphql'), 'resolve' => function($post) { $post = get_post($post->ID); $block_data = get_block_data($post->post_content); $block_metadata = get_block_metadata($block_data); return json_encode($block_metadata); } ] ); });

ポップ

Note: This functionality is available on its own GitHub repo.

The final API is called PoP, which is a little-known project I've been working on for several years now. I have recently converted it into a full-fledged API, with the capacity to produce a response compatible with both REST and GraphQL, and which even benefits from the advantages from these 2 APIs, at the same time: no under/over-fetching of data, like in GraphQL, while being cacheable on the server-side and not susceptible to DoS attacks, like REST. It offers a mix between the two of them: REST-like endpoints with GraphQL-like queries.

The block metadata is made available through the API through the following code:

 class PostFieldValueResolver extends AbstractDBDataFieldValueResolver { public static function getClassesToAttachTo(): array { return array(\PoP\Posts\FieldResolver::class); } public function resolveValue(FieldResolverInterface $fieldResolver, $resultItem, string $fieldName, array $fieldArgs = []) { $post = $resultItem; switch ($fieldName) { case 'block-metadata': $block_data = \Leoloso\BlockMetadata\Data::get_block_data($post->post_content); $block_metadata = \Leoloso\BlockMetadata\Metadata::get_block_metadata($block_data); // Filter by blockName if ($blockName = $fieldArgs['blockname']) { $block_metadata = array_filter( $block_metadata, function($block) use($blockName) { return $block['blockName'] == $blockName; } ); } return $block_metadata; } return parent::resolveValue($fieldResolver, $resultItem, $fieldName, $fieldArgs); } }

To see it in action, this link displays the block metadata (+ ID, title and URL of the post, and the ID and name of its author, a la GraphQL) for a list of posts.

さらに、GraphQL引数と同様に、クエリはフィールド引数を介してカスタマイズできるため、特定のプラットフォームに適したデータのみを取得できます。 たとえば、すべての投稿に追加されたすべてのYouTube動画を抽出する場合は、このリンクのように、エンドポイントURLのフィールドblock-metadataに修飾子(blockname:core-embed/youtube)を追加できます。 または、特定の投稿からすべての画像を抽出する場合は、この他のリンク| id | titleのように修飾子(blockname:core/image)を追加できます。

結論

COPE(“ Create Once、Publish Everywhere”)戦略は、単一のソースを作成することにより、さまざまなメディア(Web、電子メール、アプリ、ホームアシスタント、バーチャルリアリティなど)で実行する必要のある複数のアプリケーションを作成するために必要な作業量を削減するのに役立ちます私たちのコンテンツの真実の。 WordPressに関しては、コンテンツ管理システムとして常に輝いてきましたが、COPE戦略の実装は歴史的に課題であることが証明されています。

ただし、最近のいくつかの開発により、WordPressにこの戦略を実装することがますます実現可能になっています。 一方では、WP REST APIのコアへの統合以来、そしてさらに顕著にGutenbergの発売以来、ほとんどのWordPressコンテンツはAPIを介してアクセス可能であり、本物のヘッドレスシステムになっています。 一方、Gutenberg(新しいデフォルトのコンテンツエディター)はブロックベースであり、ブログ投稿内のすべてのメタデータにAPIから簡単にアクセスできます。

結果として、WordPressにCOPEを実装するのは簡単です。 この記事では、その方法を説明しました。関連するすべてのコードは、いくつかのリポジトリから入手できるようになっています。 このソリューションは最適ではありませんが(HTMLコードの解析が多く含まれるため)、それでもかなりうまく機能し、その結果、アプリケーションを複数のプラットフォームにリリースするために必要な労力を大幅に削減できます。 それへの称賛!