“Buat Sekali, Publikasikan Di Mana Saja” Dengan WordPress

Diterbitkan: 2022-03-10
Ringkasan cepat Istilah COPE (“Buat Sekali, Publikasikan Di Mana Saja”) adalah metodologi untuk memublikasikan konten kami ke berbagai keluaran (situs web, situs AMP, email, aplikasi, dan sebagainya) dengan memiliki satu sumber kebenaran untuk semuanya . Mari kita jelajahi bagaimana menerapkan COPE menggunakan WordPress.

COPE adalah strategi untuk mengurangi jumlah pekerjaan yang diperlukan untuk mempublikasikan konten kami ke berbagai media, seperti situs web, email, aplikasi, dan lainnya. Pertama kali dipelopori oleh NPR, ia mencapai tujuannya dengan menetapkan satu sumber kebenaran untuk konten yang dapat digunakan untuk semua media yang berbeda.

Memiliki konten yang berfungsi di mana-mana bukanlah tugas yang sepele karena setiap media akan memiliki persyaratannya sendiri. Misalnya, HTML valid untuk mencetak konten untuk web, bahasa ini tidak valid untuk aplikasi iOS/Android. Demikian pula, kami dapat menambahkan kelas ke HTML kami untuk web, tetapi ini harus dikonversi ke gaya untuk email.

Solusi dari teka-teki ini adalah dengan memisahkan bentuk dari isi: Penyajian dan makna isi harus dipisahkan, dan hanya arti yang digunakan sebagai satu-satunya sumber kebenaran. Presentasi kemudian dapat ditambahkan di lapisan lain (khusus untuk media yang dipilih).

Misalnya, dengan potongan kode HTML berikut, <p> adalah tag HTML yang sebagian besar berlaku untuk web, dan atribut class="align-center" adalah presentasi (menempatkan elemen "di tengah" masuk akal untuk media berbasis layar, tetapi tidak untuk media berbasis audio seperti Amazon Alexa):

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

Oleh karena itu, konten ini tidak dapat digunakan sebagai satu sumber kebenaran, dan harus diubah menjadi format yang memisahkan makna dari presentasi, seperti potongan kode JSON berikut:

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

Potongan kode ini dapat digunakan sebagai sumber kebenaran tunggal untuk konten karena dari situ kita dapat membuat ulang kode HTML untuk digunakan di web, dan mendapatkan format yang sesuai untuk media lain.

Lebih banyak setelah melompat! Lanjutkan membaca di bawah ini

Mengapa WordPress

WordPress sangat ideal untuk menerapkan strategi COPE karena beberapa alasan:

  • Ini serbaguna.
    Model database WordPress tidak mendefinisikan model konten yang kaku dan kaku; sebaliknya, itu dibuat untuk keserbagunaan, memungkinkan untuk membuat model konten yang bervariasi melalui penggunaan bidang meta, yang memungkinkan penyimpanan potongan data tambahan untuk empat entitas yang berbeda: posting dan jenis posting kustom, pengguna, komentar, dan taksonomi ( tag dan kategori).
  • Ini sangat kuat.
    WordPress bersinar sebagai CMS (Content Management System), dan ekosistem pluginnya memungkinkan untuk menambahkan fungsionalitas baru dengan mudah.
  • Ini tersebar luas.
    Diperkirakan 1/3 dari situs web berjalan di WordPress. Kemudian, cukup banyak orang yang bekerja di web mengetahui dan dapat menggunakannya, yaitu WordPress. Bukan hanya developer tapi juga blogger, salesman, staf marketing, dan lain sebagainya. Kemudian, banyak pemangku kepentingan yang berbeda, apa pun latar belakang teknisnya, akan mampu menghasilkan konten yang bertindak sebagai satu-satunya sumber kebenaran.
  • Ini tanpa kepala.
    Headless adalah kemampuan untuk memisahkan konten dari lapisan presentasi, dan ini adalah fitur mendasar untuk menerapkan COPE (untuk dapat memasukkan data ke media yang berbeda).

    Sejak memasukkan WP REST API ke dalam inti mulai dari versi 4.7, dan lebih nyata lagi sejak peluncuran Gutenberg di versi 5.0 (yang banyak titik akhir REST API harus diimplementasikan), WordPress dapat dianggap sebagai CMS tanpa kepala, karena sebagian besar konten WordPress dapat diakses melalui REST API oleh aplikasi apa pun yang dibangun di atas tumpukan apa pun.

    Selain itu, WPGraphQL yang baru dibuat mengintegrasikan WordPress dan GraphQL, memungkinkan untuk memasukkan konten dari WordPress ke aplikasi apa pun menggunakan API yang semakin populer ini. Akhirnya, proyek saya sendiri PoP baru-baru ini menambahkan implementasi API untuk WordPress yang memungkinkan untuk mengekspor data WordPress sebagai format asli REST, GraphQL atau PoP.
  • Ia memiliki Gutenberg , editor berbasis blok yang sangat membantu implementasi COPE karena didasarkan pada konsep blok (seperti yang dijelaskan pada bagian di bawah).

Gumpalan Versus Blok Untuk Mewakili Informasi

Blob adalah satu unit informasi yang disimpan bersama-sama dalam database. Misalnya, menulis posting blog di bawah ini pada CMS yang mengandalkan gumpalan untuk menyimpan informasi akan menyimpan konten posting blog pada satu entri database — yang berisi konten yang sama:

 <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>

Seperti yang dapat diapresiasi, potongan informasi penting dari posting blog ini (seperti konten dalam paragraf, dan URL, dimensi dan atribut video Youtube) tidak mudah diakses: Jika kita ingin mengambil salah satunya sendiri, kita perlu mengurai kode HTML untuk mengekstraknya — yang jauh dari solusi ideal.

Blok bertindak berbeda. Dengan merepresentasikan informasi sebagai daftar blok, kita dapat menyimpan konten dengan cara yang lebih semantik dan dapat diakses. Setiap blok menyampaikan kontennya sendiri dan propertinya sendiri yang dapat bergantung pada jenisnya (mis. apakah itu mungkin paragraf atau video?).

Misalnya, kode HTML di atas dapat direpresentasikan sebagai daftar blok seperti ini:

 { [ 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" ] }

Melalui cara merepresentasikan informasi ini, kita dapat dengan mudah menggunakan setiap bagian data dengan sendirinya, dan mengadaptasinya untuk media tertentu di mana ia harus ditampilkan. Misalnya, jika kita ingin mengekstrak semua video dari posting blog untuk ditampilkan di sistem hiburan mobil, kita cukup mengulangi semua blok informasi, pilih yang dengan type="embed" dan provider="Youtube" , dan ekstrak URL dari mereka. Demikian pula, jika kita ingin menampilkan video di Apple Watch, kita tidak perlu peduli dengan dimensi video, sehingga kita dapat mengabaikan atribut width dan height secara langsung.

Bagaimana Gutenberg Mengimplementasikan Blok

Sebelum WordPress versi 5.0, WordPress menggunakan blob untuk menyimpan konten posting di database. Mulai dari versi 5.0, WordPress dikirimkan dengan Gutenberg, editor berbasis blok, memungkinkan cara yang disempurnakan untuk memproses konten yang disebutkan di atas, yang merupakan terobosan menuju penerapan COPE. Sayangnya, Gutenberg belum dirancang untuk kasus penggunaan khusus ini, dan representasi informasinya berbeda dengan yang baru saja dijelaskan untuk blok, menghasilkan beberapa ketidaknyamanan yang perlu kita tangani.

Mari kita lihat sekilas bagaimana posting blog yang dijelaskan di atas disimpan melalui Gutenberg:

 <!-- 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 -->

Dari potongan kode ini, kita dapat membuat pengamatan berikut:

Blok Disimpan Bersama-sama Dalam Entri Basis Data yang Sama

Ada dua blok dalam kode di atas:

 <!-- 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 -->

Dengan pengecualian blok global (juga disebut “dapat digunakan kembali”), yang memiliki entri sendiri dalam database dan dapat dirujuk langsung melalui ID mereka, semua blok disimpan bersama dalam entri entri blog di tabel wp_posts .

Oleh karena itu, untuk mengambil informasi untuk blok tertentu, pertama-tama kita perlu mengurai konten dan mengisolasi semua blok satu sama lain. Mudahnya, WordPress menyediakan fungsi parse_blocks($content) untuk melakukan hal ini. Fungsi ini menerima string yang berisi konten posting blog (dalam format HTML), dan mengembalikan objek JSON yang berisi data untuk semua blok yang dimuat.

Jenis Blok Dan Atribut Disampaikan Melalui Komentar HTML

Setiap blok dibatasi dengan tag awal <!-- wp:{block-type} {block-attributes-encoded-as-JSON} --> dan tag penutup <!-- /wp:{block-type} --> yang (menjadi komentar HTML) memastikan bahwa informasi ini tidak akan terlihat saat ditampilkan di situs web. Namun, kami tidak dapat menampilkan entri blog secara langsung di media lain, karena komentar HTML mungkin terlihat, muncul sebagai konten kacau. Ini bukan masalah besar, karena setelah mem-parsing konten melalui fungsi parse_blocks($content) , komentar HTML dihapus dan kita dapat beroperasi langsung dengan data blok sebagai objek JSON.

Blok Berisi HTML

Blok paragraf memiliki "<p>Look at this wonderful tango:</p>" sebagai kontennya, alih-alih "Look at this wonderful tango:" . Oleh karena itu, berisi kode HTML (tag <p> dan </p> ) yang tidak berguna untuk media lain, dan karena itu harus dihapus, misalnya melalui fungsi PHP strip_tags($content) .

Saat menghapus tag, kita dapat menyimpan tag HTML yang secara eksplisit menyampaikan informasi semantik, seperti tag <strong> dan <em> (sebagai ganti dari <b> dan <i> yang hanya berlaku untuk media berbasis layar), dan hapus semua tag lainnya. Ini karena ada kemungkinan besar bahwa tag semantik dapat ditafsirkan dengan benar untuk media lain juga (misalnya Amazon Alexa dapat mengenali tag <strong> dan <em> , dan mengubah suara dan intonasinya sesuai saat membaca sepotong teks). Untuk melakukan ini, kami memanggil fungsi strip_tags dengan parameter ke-2 yang berisi tag yang diizinkan, dan menempatkannya di dalam fungsi pembungkus untuk kenyamanan:

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

Keterangan Video Disimpan Dalam HTML Dan Bukan Sebagai Atribut

Seperti yang dapat dilihat di blok video Youtube, keterangan "An exquisite tango performance" disimpan di dalam kode HTML (diapit oleh tag <figcaption /> ) tetapi tidak di dalam objek atribut yang dikodekan JSON. Akibatnya, untuk mengekstrak teks, kita perlu mengurai konten blok, misalnya melalui ekspresi reguler:

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

Ini adalah rintangan yang harus kita atasi untuk mengekstrak semua metadata dari blok Gutenberg. Ini terjadi pada beberapa blok; karena tidak semua bagian metadata disimpan sebagai atribut, pertama-tama kita harus mengidentifikasi bagian mana dari metadata ini, dan kemudian mengurai konten HTML untuk mengekstraknya berdasarkan blok demi blok dan bagian demi bagian.

Mengenai COPE, ini merupakan kesempatan yang terbuang untuk memiliki solusi yang benar-benar optimal. Dapat dikatakan bahwa opsi alternatif juga tidak ideal, karena akan menduplikasi informasi, menyimpannya baik di dalam HTML maupun sebagai atribut, yang melanggar prinsip DRY ( D on't R epeat Y ourself). Namun, pelanggaran ini sudah terjadi: Misalnya, atribut className berisi nilai "wp-embed-aspect-16-9 wp-has-aspect-ratio" , yang juga dicetak di dalam konten, di bawah class atribut HTML .

Menambahkan konten melalui Gutenberg
Menambahkan konten melalui Gutenberg (Pratinjau besar)

Menerapkan COPE

Catatan: Saya telah merilis fungsionalitas ini, termasuk semua kode yang dijelaskan di bawah ini, sebagai plugin WordPress Block Metadata. Anda dipersilakan untuk menginstalnya dan memainkannya sehingga Anda bisa merasakan kekuatan COPE. Kode sumber tersedia di repo GitHub ini.

Sekarang kita tahu seperti apa representasi bagian dalam dari sebuah blok, mari kita lanjutkan untuk mengimplementasikan COPE melalui Gutenberg. Prosedur ini akan melibatkan langkah-langkah berikut:

  1. Karena fungsi parse_blocks($content) mengembalikan objek JSON dengan level bersarang, kita harus menyederhanakan struktur ini terlebih dahulu.
  2. Kami mengulangi semua blok dan, untuk masing-masing, mengidentifikasi potongan metadata dan mengekstraknya, mengubahnya menjadi format agnostik menengah dalam prosesnya. Atribut mana yang ditambahkan ke respons dapat bervariasi tergantung pada jenis blok.
  3. Kami akhirnya membuat data tersedia melalui API (REST/GraphQL/PoP).

Mari kita terapkan langkah-langkah ini satu per satu.

1. Menyederhanakan Struktur Objek JSON

Objek JSON yang dikembalikan dari fungsi parse_blocks($content) memiliki arsitektur bersarang, di mana data untuk blok normal muncul di tingkat pertama, tetapi data untuk blok referensi yang dapat digunakan kembali tidak ada (hanya data untuk blok referensi yang ditambahkan), dan data untuk blok bersarang (yang ditambahkan di dalam blok lain) dan untuk blok yang dikelompokkan (di mana beberapa blok dapat dikelompokkan bersama) muncul di bawah 1 atau lebih sublevel. Arsitektur ini mempersulit pemrosesan data blok dari semua blok dalam konten posting, karena di satu sisi ada beberapa data yang hilang, dan di sisi lain kita tidak tahu apriori di bawah berapa banyak level data berada. Selain itu, ada pembagi blok yang ditempatkan setiap pasangan blok, tidak berisi konten, yang dapat diabaikan dengan aman.

Misalnya, respons yang diperoleh dari posting yang berisi blok sederhana, blok global, blok bersarang yang berisi blok sederhana, dan sekelompok blok sederhana, dalam urutan itu, adalah sebagai berikut:

 [ // 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" ] } ]

Solusi yang lebih baik adalah memiliki semua data di tingkat pertama, sehingga logika untuk beralih melalui semua data blok sangat disederhanakan. Oleh karena itu, kita harus mengambil data untuk blok yang dapat digunakan kembali/bersarang/dikelompokkan ini, dan menambahkannya pada level pertama juga. Seperti yang dapat dilihat pada kode JSON di atas:

  • Blok pembagi kosong memiliki atribut "blockName" dengan nilai NULL
  • Referensi ke blok yang dapat digunakan kembali didefinisikan melalui $block["attrs"]["ref"]
  • Blok bersarang dan grup menentukan blok yang dikandungnya di bawah $block["innerBlocks"]

Oleh karena itu, kode PHP berikut menghapus blok pembagi kosong, mengidentifikasi blok yang dapat digunakan kembali/bersarang/dikelompokkan dan menambahkan datanya ke level pertama, dan menghapus semua data dari semua sublevel:

 /** * 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); }

Memanggil fungsi get_block_data($content) dengan meneruskan konten postingan ( $post->post_content ) sebagai parameter, sekarang kita mendapatkan respons berikut:

 [[ { "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" ] } ]

Meskipun tidak sepenuhnya diperlukan, akan sangat membantu untuk membuat titik akhir REST API untuk menampilkan hasil dari fungsi baru kita get_block_data($content) , yang akan memungkinkan kita untuk dengan mudah memahami blok apa yang terdapat dalam postingan tertentu, dan bagaimana blok tersebut tersusun. Kode di bawah ini menambahkan titik akhir seperti itu di bawah /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; }

Untuk melihatnya beraksi, lihat tautan ini yang mengekspor data untuk pos ini.

2. Mengekstrak Semua Metadata Blok Menjadi Format Medium-Agnostik

Pada tahap ini, kami memiliki data blok yang berisi kode HTML yang tidak sesuai untuk COPE. Oleh karena itu, kita harus menghapus tag HTML non-semantik untuk setiap blok untuk mengubahnya menjadi format medium-agnostik.

Kita dapat memutuskan atribut mana yang harus diekstraksi berdasarkan tipe blok berdasarkan tipe blok (misalnya, mengekstrak properti perataan teks untuk blok "paragraph" , properti URL video untuk blok "youtube embed" , dan seterusnya) .

Seperti yang kita lihat sebelumnya, tidak semua atribut sebenarnya disimpan sebagai atribut blok tetapi di dalam konten dalam blok, oleh karena itu, untuk situasi ini, kita perlu mengurai konten HTML menggunakan ekspresi reguler untuk mengekstrak potongan metadata tersebut.

Setelah memeriksa semua blok yang dikirimkan melalui inti WordPress, saya memutuskan untuk tidak mengekstrak metadata untuk yang berikut:

"core/columns"
"core/column"
"core/cover"
Ini hanya berlaku untuk media berbasis layar dan (menjadi blok bersarang) sulit untuk ditangani.
"core/html" Yang ini hanya masuk akal untuk web.
"core/table"
"core/button"
"core/media-text"
Saya tidak tahu bagaimana merepresentasikan data mereka dengan cara agnostik menengah atau apakah itu masuk akal.

Ini meninggalkan saya dengan blok berikut, yang saya akan melanjutkan untuk mengekstrak metadata mereka:

  • 'core/paragraph'
  • 'core/image'
  • 'core-embed/youtube' (sebagai perwakilan dari semua blok 'core-embed' )
  • 'core/heading'
  • 'core/gallery'
  • 'core/list'
  • 'core/audio'
  • 'core/file'
  • 'core/video'
  • 'core/code'
  • 'core/preformatted'
  • 'core/quote' & 'core/pullquote'
  • 'core/verse'

Untuk mengekstrak metadata, kami membuat fungsi get_block_metadata($block_data) yang menerima larik dengan data blok untuk setiap blok (yaitu output dari fungsi get_block_data yang kami implementasikan sebelumnya) dan, tergantung pada jenis blok (disediakan di bawah properti "blockName" ), memutuskan atribut apa yang diperlukan dan cara mengekstraknya:

 /** * 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; }

Mari lanjutkan mengekstrak metadata untuk setiap jenis blok, satu per satu:

“core/paragraph”

Cukup hapus tag HTML dari konten, dan hapus breakline tambahan.

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

'core/image'

Blok memiliki ID yang merujuk ke file media yang diunggah atau, jika tidak, sumber gambar harus diekstraksi dari <img src="..."> . Beberapa atribut (caption, linkDestination, link, alignment) adalah opsional.

 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;

Masuk akal untuk membuat fungsi extract_image_src , extract_caption dan extract_link karena ekspresi regulernya akan digunakan berulang kali untuk beberapa blok. Harap perhatikan bahwa keterangan di Gutenberg dapat berisi tautan ( <a href="..."> ), namun, saat memanggil strip_html_tags , ini dihapus dari keterangan.

Meskipun disesalkan, saya menemukan praktik ini tidak dapat dihindari, karena kami tidak dapat menjamin tautan untuk berfungsi di platform non-web. Oleh karena itu, meskipun konten mendapatkan universalitas karena dapat digunakan untuk media yang berbeda, ia juga kehilangan kekhususan, sehingga kualitasnya lebih buruk dibandingkan dengan konten yang dibuat dan disesuaikan untuk platform tertentu.

 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'

Cukup ambil URL video dari atribut blok, dan ekstrak keterangannya dari konten HTML, jika ada.

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

'core/heading'

Baik ukuran header (h1, h2, …, h6) dan teks heading bukanlah atribut, jadi ini harus diperoleh dari konten HTML. Harap perhatikan bahwa, alih-alih mengembalikan tag HTML untuk header, atribut size hanyalah representasi yang setara, yang lebih agnostik dan lebih masuk akal untuk platform non-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'

Sayangnya, untuk galeri gambar saya tidak dapat mengekstrak teks dari setiap gambar, karena ini bukan atribut, dan mengekstraknya melalui ekspresi reguler sederhana dapat gagal: Jika ada teks untuk elemen pertama dan ketiga, tetapi tidak ada untuk yang kedua, maka saya tidak akan tahu teks mana yang sesuai dengan gambar mana (dan saya belum mencurahkan waktu untuk membuat regex yang kompleks). Demikian juga, dalam logika di bawah ini saya selalu mengambil ukuran gambar "full" , namun, ini tidak harus terjadi, dan saya tidak mengetahui bagaimana ukuran yang lebih tepat dapat disimpulkan.

 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'

Cukup ubah elemen <li> menjadi larik item.

 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'

Dapatkan URL file media yang diunggah terkait.

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

'core/file'

Sedangkan URL file adalah atribut, teksnya harus diekstraksi dari konten dalam.

 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'

Dapatkan URL video dan semua properti untuk mengonfigurasi cara video diputar melalui ekspresi reguler. Jika Gutenberg pernah mengubah urutan properti ini dicetak dalam kode, maka regex ini akan berhenti bekerja, membuktikan salah satu masalah tidak menambahkan metadata secara langsung melalui blok atribut.

 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.

REST

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); } ] ); });

PoP

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.

Selain itu, mirip dengan argumen GraphQL, kueri kami dapat dikustomisasi melalui argumen bidang, memungkinkan untuk mendapatkan hanya data yang masuk akal untuk platform tertentu. Misalnya, jika kita ingin mengekstrak semua video Youtube yang ditambahkan ke semua posting, kita dapat menambahkan pengubah (blockname:core-embed/youtube) ke bidang block-metadata di URL titik akhir, seperti di tautan ini. Atau jika kita ingin mengekstrak semua gambar dari postingan tertentu, kita bisa menambahkan modifier (blockname:core/image) seperti di link lain|id|title).

Kesimpulan

Strategi COPE (“Buat Sekali, Terbitkan Di Mana Saja”) membantu kami menurunkan jumlah pekerjaan yang diperlukan untuk membuat beberapa aplikasi yang harus berjalan di media yang berbeda (web, email, aplikasi, asisten rumah, virtual reality, dll) dengan membuat satu sumber kebenaran untuk konten kami. Mengenai WordPress, meskipun selalu bersinar sebagai Sistem Manajemen Konten, menerapkan strategi COPE secara historis terbukti menjadi tantangan.

Namun, beberapa perkembangan terakhir membuatnya semakin layak untuk menerapkan strategi ini untuk WordPress. Di satu sisi, sejak integrasi ke dalam inti WP REST API, dan lebih nyata lagi sejak peluncuran Gutenberg, sebagian besar konten WordPress dapat diakses melalui API, menjadikannya sistem tanpa kepala yang asli. Di sisi lain, Gutenberg (yang merupakan editor konten default baru) berbasis blok, membuat semua metadata di dalam postingan blog mudah diakses oleh API.

Akibatnya, menerapkan COPE untuk WordPress sangatlah mudah. Dalam artikel ini, kita telah melihat bagaimana melakukannya, dan semua kode yang relevan telah tersedia melalui beberapa repositori. Meskipun solusinya tidak optimal (karena melibatkan banyak penguraian kode HTML), solusi ini masih bekerja dengan cukup baik, dengan konsekuensi bahwa upaya yang diperlukan untuk merilis aplikasi kita ke berbagai platform dapat sangat dikurangi. Pujian untuk itu!