“สร้างครั้งเดียว เผยแพร่ได้ทุกที่” ด้วย WordPress

เผยแพร่แล้ว: 2022-03-10
สรุปโดยย่อ ↬ คำว่า COPE (“สร้างครั้งเดียว เผยแพร่ทุกที่”) เป็นวิธีการสำหรับเผยแพร่เนื้อหาของเราไปยังเอาต์พุตต่างๆ (เว็บไซต์ ไซต์ AMP อีเมล แอป และอื่นๆ) โดยมีแหล่งความจริงเพียงแหล่งเดียวสำหรับทั้งหมด . มาสำรวจวิธีการใช้ COPE โดยใช้ WordPress

COPE เป็นกลยุทธ์ในการลดปริมาณงานที่จำเป็นในการเผยแพร่เนื้อหาของเราไปยังสื่อต่างๆ เช่น เว็บไซต์ อีเมล แอป และอื่นๆ NPR เป็นผู้บุกเบิกครั้งแรก โดยบรรลุเป้าหมายด้วยการสร้างแหล่งความจริงเพียงแหล่งเดียวสำหรับเนื้อหาซึ่งสามารถใช้กับสื่อต่างๆ ได้ทั้งหมด

การมีเนื้อหาที่ใช้งานได้ทุกที่ไม่ใช่งานง่าย เนื่องจากสื่อแต่ละประเภทจะมีข้อกำหนดของตัวเอง ตัวอย่างเช่น แม้ว่า HTML จะใช้ได้สำหรับการพิมพ์เนื้อหาสำหรับเว็บ แต่ภาษานี้ใช้ไม่ได้กับแอป iOS/Android ในทำนองเดียวกัน เราสามารถเพิ่มคลาสลงใน HTML สำหรับเว็บได้ แต่ต้องแปลงเป็นสไตล์สำหรับอีเมล

ทางออกของปริศนานี้คือการแยกรูปแบบออกจากเนื้อหา: การนำเสนอและความหมายของเนื้อหาต้องแยกจากกัน และใช้ความหมายเป็นแหล่งความจริงเพียงแหล่งเดียว จากนั้นคุณสามารถเพิ่มงานนำเสนอในเลเยอร์อื่นได้ (เฉพาะสำหรับสื่อที่เลือก)

ตัวอย่างเช่น จากโค้ด HTML ต่อไปนี้ <p> คือแท็ก HTML ที่ใช้กับเว็บเป็นส่วนใหญ่ และแอตทริบิวต์ class="align-center" คือการนำเสนอ (การวางองค์ประกอบ "ที่กึ่งกลาง" นั้นเหมาะสมสำหรับ สื่อที่ใช้หน้าจอ แต่ไม่ใช่สำหรับสื่อที่ใช้เสียง เช่น Amazon Alexa):

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

ดังนั้น เนื้อหาชิ้นนี้ไม่สามารถใช้เป็นแหล่งความจริงเพียงแหล่งเดียวได้ และจะต้องแปลงเป็นรูปแบบที่แยกความหมายออกจากการนำเสนอ เช่น ส่วนต่อไปนี้ของโค้ด JSON:

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

โค้ดชิ้นนี้สามารถใช้เป็นแหล่งข้อมูลจริงเพียงแหล่งเดียวสำหรับเนื้อหา เนื่องจากเราสามารถสร้างโค้ด HTML ขึ้นใหม่อีกครั้งเพื่อใช้สำหรับเว็บ และจัดหารูปแบบที่เหมาะสมสำหรับสื่ออื่นๆ

เพิ่มเติมหลังกระโดด! อ่านต่อด้านล่าง↓

ทำไมต้อง WordPress

WordPress เหมาะที่จะใช้กลยุทธ์ COPE เนื่องจากสาเหตุหลายประการ:

  • มันอเนกประสงค์
    โมเดลฐานข้อมูล WordPress ไม่ได้กำหนดรูปแบบเนื้อหาที่คงที่และเข้มงวด ตรงกันข้าม มันถูกสร้างมาเพื่อความอเนกประสงค์ ทำให้สามารถสร้างโมเดลเนื้อหาที่หลากหลายผ่านการใช้ฟิลด์เมตา ซึ่งช่วยให้สามารถจัดเก็บชิ้นส่วนของข้อมูลเพิ่มเติมสำหรับเอนทิตีที่แตกต่างกันสี่แบบ: โพสต์และประเภทโพสต์แบบกำหนดเอง ผู้ใช้ ความคิดเห็น และการจัดหมวดหมู่ ( แท็กและหมวดหมู่)
  • มันมีพลัง
    WordPress โดดเด่นเป็น CMS (ระบบการจัดการเนื้อหา) และระบบนิเวศของปลั๊กอินช่วยให้เพิ่มฟังก์ชันใหม่ได้อย่างง่ายดาย
  • เป็นที่แพร่หลาย
    คาดว่า 1/3 ของเว็บไซต์ทำงานบน WordPress จากนั้นผู้คนจำนวนมากที่ทำงานบนเว็บรู้และสามารถใช้งานได้ เช่น WordPress ไม่เพียงแค่นักพัฒนาเท่านั้น แต่ยังรวมถึงบล็อกเกอร์ พนักงานขาย เจ้าหน้าที่การตลาด และอื่นๆ จากนั้น ผู้มีส่วนได้ส่วนเสียหลายราย ไม่ว่าจะมีพื้นฐานทางเทคนิคอย่างไร ก็จะสามารถผลิตเนื้อหาที่ทำหน้าที่เป็นแหล่งความจริงเพียงแหล่งเดียว
  • มันไม่มีหัว
    ความโง่เขลาคือความสามารถในการแยกเนื้อหาออกจากเลเยอร์การนำเสนอ และเป็นคุณลักษณะพื้นฐานสำหรับการนำ COPE ไปใช้ (เพื่อให้สามารถป้อนข้อมูลไปยังสื่อที่ต่างกันได้)

    นับตั้งแต่การรวม WP REST API เข้ากับคอร์ตั้งแต่เวอร์ชัน 4.7 และชัดเจนยิ่งขึ้นตั้งแต่เปิดตัว Gutenberg ในเวอร์ชัน 5.0 (ซึ่งต้องติดตั้ง REST API endpoints จำนวนมาก) WordPress ถือเป็น CMS ที่ไม่มีส่วนหัว เนื่องจากเนื้อหา WordPress ส่วนใหญ่ สามารถเข้าถึงได้ผ่าน REST API โดยแอปพลิเคชันใด ๆ ที่สร้างบนสแต็กใดก็ได้

    นอกจากนี้ WPGraphQL ที่เพิ่งสร้างใหม่ยังรวม WordPress และ GraphQL เข้าไว้ด้วยกัน ทำให้สามารถฟีดเนื้อหาจาก WordPress ไปยังแอปพลิเคชันใดก็ได้โดยใช้ API ที่ได้รับความนิยมเพิ่มขึ้นนี้ ในที่สุด โครงการ PoP ของฉันเองได้เพิ่มการใช้งาน API สำหรับ WordPress ซึ่งอนุญาตให้ส่งออกข้อมูล WordPress เป็นรูปแบบดั้งเดิม REST, GraphQL หรือ PoP
  • มี Gutenberg ซึ่งเป็นตัวแก้ไขแบบบล็อกที่ช่วยในการใช้งาน COPE อย่างมาก เนื่องจากเป็นไปตามแนวคิดของบล็อก (ตามที่อธิบายไว้ในส่วนด้านล่าง)

Blobs กับบล็อกเพื่อเป็นตัวแทนของข้อมูล

Blob คือหน่วยข้อมูลเดียวที่จัดเก็บไว้ในฐานข้อมูล ตัวอย่างเช่น การเขียนบล็อกโพสต์ด้านล่างบน CMS ที่ใช้ blobs ในการจัดเก็บข้อมูลจะจัดเก็บเนื้อหาบล็อกโพสต์ในรายการฐานข้อมูลเดียว ซึ่งมีเนื้อหาเหมือนกัน:

 <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 เราไม่จำเป็นต้องสนใจเกี่ยวกับขนาดของวิดีโอ ดังนั้นเราจึงสามารถละเว้น width และ height แอตทริบิวต์ได้โดยตรง

Gutenberg นำบล็อกไปใช้อย่างไร

ก่อน WordPress เวอร์ชัน 5.0 WordPress ใช้ blobs เพื่อจัดเก็บเนื้อหาโพสต์ในฐานข้อมูล เริ่มตั้งแต่เวอร์ชัน 5.0 เป็นต้นไป WordPress มาพร้อมกับ Gutenberg ซึ่งเป็นตัวแก้ไขแบบบล็อก ซึ่งช่วยให้สามารถประมวลผลเนื้อหาที่กล่าวถึงข้างต้นได้ดีขึ้น ซึ่งแสดงถึงความก้าวหน้าในการนำ COPE ไปใช้ น่าเสียดายที่ Gutenberg ไม่ได้ออกแบบมาสำหรับกรณีการใช้งานเฉพาะนี้ และการแสดงข้อมูลของ Gutenberg นั้นแตกต่างไปจากที่อธิบายไว้สำหรับการบล็อก ส่งผลให้เกิดความไม่สะดวกหลายประการที่เราจะต้องจัดการ

ก่อนอื่นเรามาดูว่าโพสต์บนบล็อกที่อธิบายข้างต้นได้รับการบันทึกผ่าน 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 -->

จากโค้ดชิ้นนี้ เราสามารถสังเกตได้ดังต่อไปนี้:

บล็อกถูกบันทึกไว้ทั้งหมดในรายการฐานข้อมูลเดียวกัน

โค้ดด้านบนมีสองช่วงตึก:

 <!-- 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} --> ซึ่ง (เป็นความคิดเห็น HTML) ทำให้มั่นใจได้ว่าข้อมูลนี้จะไม่ปรากฏให้เห็นเมื่อแสดงบนเว็บไซต์ อย่างไรก็ตาม เราไม่สามารถแสดงบล็อกโพสต์โดยตรงบนสื่ออื่น เนื่องจากความคิดเห็น HTML อาจมองเห็นได้ โดยปรากฏเป็นเนื้อหาที่อ่านไม่ออก นี่ไม่ใช่เรื่องใหญ่ เนื่องจากหลังจากแยกวิเคราะห์เนื้อหาผ่านฟังก์ชัน parse_blocks($content) ความคิดเห็น HTML จะถูกลบออก และเราสามารถดำเนินการโดยตรงกับข้อมูลบล็อกเป็นวัตถุ JSON

บล็อกมี HTML

บล็อกย่อหน้ามี "<p>Look at this wonderful tango:</p>" เป็นเนื้อหา แทนที่จะเป็น "Look at this wonderful tango:" ดังนั้นจึงมีโค้ด HTML (แท็ก <p> และ </p> ) ซึ่งไม่มีประโยชน์สำหรับสื่ออื่น ดังนั้นจึงต้องลบออก เช่น ผ่านฟังก์ชัน PHP strip_tags($content)

เมื่อลอกแท็ก เราสามารถเก็บแท็ก HTML เหล่านั้นไว้ซึ่งสื่อความหมายอย่างชัดแจ้ง เช่น แท็ก <strong> และ <em> (แทนที่จะเป็นแท็ก <b> และ <i> ที่ใช้กับสื่อบนหน้าจอเท่านั้น) และ ลบแท็กอื่นๆ ทั้งหมด เนื่องจากมีโอกาสสูงที่แท็กเชิงความหมายสามารถตีความได้อย่างถูกต้องสำหรับสื่ออื่นๆ เช่นกัน (เช่น Amazon Alexa สามารถจดจำแท็ก <strong> และ <em> และเปลี่ยนเสียงและน้ำเสียงตามนั้นเมื่ออ่านข้อความ) ในการทำเช่นนี้ เราเรียกใช้ฟังก์ชัน strip_tags ด้วยพารามิเตอร์ตัวที่ 2 ที่มีแท็กที่อนุญาต และวางไว้ในฟังก์ชันการตัดคำเพื่อความสะดวก:

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

นี่เป็นอุปสรรคที่เราต้องเอาชนะเพื่อดึงข้อมูลเมตาทั้งหมดออกจากบล็อก Gutenberg สิ่งนี้เกิดขึ้นในหลายช่วงตึก เนื่องจากไม่ได้บันทึกทุกส่วนของข้อมูลเมตาเป็นแอตทริบิวต์ เราต้องระบุก่อนว่าส่วนใดเป็นข้อมูลเมตาเหล่านี้ จากนั้นจึงแยกวิเคราะห์เนื้อหา HTML เพื่อแยกออกเป็นบล็อกต่อบล็อกและทีละชิ้น

เกี่ยวกับ COPE ถือเป็นการเสียโอกาสที่จะมีวิธีแก้ปัญหาที่เหมาะสมที่สุด อาจเป็นที่ถกเถียงกันอยู่ว่าตัวเลือกอื่นไม่เหมาะเช่นกัน เนื่องจากมันจะทำซ้ำข้อมูล โดยจัดเก็บไว้ภายใน HTML และเป็นแอตทริบิวต์ ซึ่งละเมิดหลักการ DRY ( D on't R epeat Y ourself) อย่างไรก็ตาม การละเมิดนี้เกิดขึ้นแล้ว: ตัวอย่างเช่น แอตทริบิวต์ className มีค่า "wp-embed-aspect-16-9 wp-has-aspect-ratio" ซึ่งพิมพ์อยู่ภายในเนื้อหาด้วย ภายใต้ class แอตทริบิวต์ HTML

การเพิ่มเนื้อหาผ่าน Gutenberg
การเพิ่มเนื้อหาผ่าน Gutenberg (ตัวอย่างขนาดใหญ่)

การดำเนินการตาม COPE

หมายเหตุ: ฉันได้เผยแพร่ฟังก์ชันนี้ รวมทั้งโค้ดทั้งหมดที่อธิบายไว้ด้านล่าง เป็นปลั๊กอิน WordPress บล็อกข้อมูลเมตา คุณสามารถติดตั้งและเล่นกับมันได้ เพื่อให้คุณได้สัมผัสกับพลังของ COPE ซอร์สโค้ดมีอยู่ใน repo GitHub นี้

ตอนนี้เรารู้แล้วว่าการแสดงภายในของบล็อกเป็นอย่างไร มาดำเนินการ COPE ผ่าน Gutenberg กัน ขั้นตอนจะเกี่ยวข้องกับขั้นตอนต่อไปนี้:

  1. เนื่องจากฟังก์ชัน parse_blocks($content) ส่งคืนอ็อบเจ็กต์ JSON ที่มีระดับที่ซ้อนกัน เราจึงต้องลดความซับซ้อนของโครงสร้างนี้ก่อน
  2. เราทำซ้ำบล็อกทั้งหมด และสำหรับแต่ละส่วน ระบุชิ้นส่วนของข้อมูลเมตาและแยกออก แปลงให้เป็นรูปแบบไม่เชื่อเรื่องพระเจ้าระดับกลางในกระบวนการ แอตทริบิวต์ใดที่เพิ่มในการตอบกลับอาจแตกต่างกันไปขึ้นอยู่กับประเภทของบล็อก
  3. ในที่สุด เราก็ทำให้ข้อมูลพร้อมใช้งานผ่าน API (REST/GraphQL/PoP)

มาทำตามขั้นตอนเหล่านี้กัน

1. ลดความซับซ้อนของโครงสร้างของวัตถุ JSON

ออบเจ็กต์ JSON ที่ส่งคืนจากฟังก์ชัน parse_blocks($content) มีสถาปัตยกรรมแบบซ้อน ซึ่งข้อมูลสำหรับบล็อกปกติจะปรากฏที่ระดับแรก แต่ข้อมูลสำหรับบล็อกที่ใช้ซ้ำได้อ้างอิงหายไป (เพิ่มเฉพาะข้อมูลสำหรับบล็อกอ้างอิงเท่านั้น) และข้อมูลสำหรับบล็อกที่ซ้อนกัน (ซึ่งถูกเพิ่มภายในบล็อกอื่น) และสำหรับบล็อกที่จัดกลุ่ม (ซึ่งหลายบล็อกสามารถจัดกลุ่มเข้าด้วยกัน) จะปรากฏภายใต้ระดับย่อย 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" ] } ]

ทางออกที่ดีกว่าคือการมีข้อมูลทั้งหมดในระดับแรก ดังนั้นตรรกะในการวนซ้ำผ่านข้อมูลบล็อกทั้งหมดจึงง่ายขึ้นอย่างมาก ดังนั้น เราต้องดึงข้อมูลสำหรับบล็อกที่ใช้ซ้ำได้/ซ้อนกัน/จัดกลุ่มเหล่านี้ และให้เพิ่มในระดับแรกด้วย ดังที่เห็นในโค้ด JSON ด้านบน:

  • บล็อกตัวแบ่งว่างมีแอตทริบิวต์ "blockName" ที่มีค่า NULL
  • การอ้างอิงถึงบล็อกที่ใช้ซ้ำได้ถูกกำหนดผ่าน $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); }

เรียกใช้ฟังก์ชัน get_block_data($content) ผ่านเนื้อหาโพสต์ ( $post->post_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" ] } ]

แม้ว่าจะไม่จำเป็นอย่างยิ่ง แต่ก็มีประโยชน์มากในการสร้างจุดสิ้นสุด REST API เพื่อแสดงผลลัพธ์ของฟังก์ชันใหม่ของเรา get_block_data($content) ซึ่งจะช่วยให้เราเข้าใจได้ง่ายว่าบล็อกใดบ้างที่มีอยู่ในโพสต์เฉพาะและเป็นอย่างไร โครงสร้าง โค้ดด้านล่างเพิ่มจุดสิ้นสุดดังกล่าวภายใต้ /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. การแยกข้อมูลเมตาที่ถูกบล็อกทั้งหมดให้อยู่ในรูปแบบที่ไม่เชื่อเรื่องพระเจ้าปานกลาง

ในขั้นตอนนี้ เรามีข้อมูลบล็อกที่มีโค้ด HTML ซึ่งไม่เหมาะสำหรับ COPE ดังนั้น เราต้องตัดแท็ก HTML ที่ไม่มีความหมายสำหรับแต่ละบล็อกเพื่อแปลงเป็นรูปแบบไม่เชื่อเรื่องพระเจ้าระดับกลาง

เราสามารถตัดสินใจได้ว่าแอตทริบิวต์ใดที่ต้องแยกออกเป็นประเภทบล็อกตามประเภทบล็อก (เช่น แยกคุณสมบัติการจัดตำแหน่งข้อความสำหรับบล็อก "paragraph" , คุณสมบัติ URL ของวิดีโอสำหรับบล็อก "youtube embed" เป็นต้น) .

ดังที่เราเห็นในตอนต้น ไม่ได้บันทึกแอตทริบิวต์ทั้งหมดเป็นแอตทริบิวต์ของบล็อกจริง ๆ แต่ภายในเนื้อหาภายในของบล็อก ดังนั้น สำหรับสถานการณ์เหล่านี้ เราจะต้องแยกวิเคราะห์เนื้อหา HTML โดยใช้นิพจน์ทั่วไปเพื่อแยกส่วนข้อมูลเมตาเหล่านั้น

หลังจากตรวจสอบบล็อคทั้งหมดที่ส่งผ่านคอร์ของ WordPress ฉันตัดสินใจที่จะไม่แยกข้อมูลเมตาสำหรับสิ่งต่อไปนี้:

"core/columns"
"core/column"
"core/cover"
สิ่งเหล่านี้ใช้เฉพาะกับสื่อบนหน้าจอและ (การบล็อกที่ซ้อนกัน) นั้นยากต่อการจัดการ
"core/html" อันนี้เหมาะสมสำหรับเว็บเท่านั้น
"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_metadata($block_data) ซึ่งรับอาร์เรย์ที่มีข้อมูลบล็อกสำหรับแต่ละบล็อก (เช่น ผลลัพธ์จากฟังก์ชันที่เราใช้งานก่อนหน้านี้ get_block_data ) และขึ้นอยู่กับประเภทของบล็อก (ให้ไว้ภายใต้คุณสมบัติ "blockName" ) ตัดสินใจว่าแอตทริบิวต์ใดที่จำเป็นและจะแยกได้อย่างไร:

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

มาดำเนินการแยกข้อมูลเมตาสำหรับบล็อกแต่ละประเภท ทีละรายการ:

“core/paragraph”

เพียงลบแท็ก HTML ออกจากเนื้อหา และลบเส้นแบ่งส่วนท้าย

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

'core/image'

บล็อกอาจมี ID ที่อ้างอิงถึงไฟล์สื่อที่อัปโหลด หรือหากไม่มี จะต้องแยกแหล่งที่มาของรูปภาพจาก <img src="..."> แอตทริบิวต์หลายรายการ (คำอธิบายภาพ, linkDestination, ลิงก์, การจัดตำแหน่ง) เป็นทางเลือก

 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_src , extract_caption และ extract_link เนื่องจากนิพจน์ทั่วไปจะถูกใช้ครั้งแล้วครั้งเล่าในหลายช่วงตึก โปรดสังเกตว่าคำอธิบายภาพใน Gutenberg สามารถมีลิงก์ได้ ( <a href="..."> ) อย่างไรก็ตาม เมื่อเรียก strip_html_tags เหล่านี้จะถูกลบออกจากคำอธิบายภาพ

แม้ว่าจะเป็นเรื่องน่าเศร้า แต่ฉันพบว่าการปฏิบัตินี้หลีกเลี่ยงไม่ได้ เนื่องจากเราไม่สามารถรับประกันลิงก์ที่จะทำงานในแพลตฟอร์มที่ไม่ใช่เว็บได้ ดังนั้น แม้ว่าเนื้อหาจะมีความเป็นสากล เนื่องจากสามารถใช้กับสื่อต่างๆ ได้ แต่ก็สูญเสียความเฉพาะเจาะจงไปด้วย ดังนั้นคุณภาพของเนื้อหาจึงด้อยกว่าเมื่อเปรียบเทียบกับเนื้อหาที่สร้างและปรับแต่งสำหรับแพลตฟอร์มเฉพาะ

 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 เป็นเพียงการแทนค่าที่เทียบเท่า ซึ่งไม่เชื่อเรื่องพระเจ้ามากกว่าและเหมาะสมกว่าสำหรับแพลตฟอร์มที่ไม่ใช่เว็บ

 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'

ขออภัย สำหรับแกลเลอรีรูปภาพ ฉันไม่สามารถแยกคำอธิบายภาพออกจากแต่ละภาพได้ เนื่องจากสิ่งเหล่านี้ไม่ใช่แอตทริบิวต์ และการดึงผ่านนิพจน์ทั่วไปอย่างง่ายอาจล้มเหลว: หากมีคำบรรยายสำหรับองค์ประกอบที่หนึ่งและสาม แต่ไม่มีสำหรับ อันที่สอง ฉันจะไม่รู้ว่าคำอธิบายภาพใดตรงกับรูปภาพใด (และฉันไม่ได้อุทิศเวลาเพื่อสร้าง regex ที่ซับซ้อน) ในทำนองเดียวกัน ในตรรกะด้านล่าง ฉันมักจะเรียกขนาดรูปภาพ "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 ของวิดีโอและคุณสมบัติทั้งหมดเพื่อกำหนดค่าวิธีการเล่นวิดีโอผ่านนิพจน์ทั่วไป หาก Gutenberg เคยเปลี่ยนลำดับการพิมพ์คุณสมบัติเหล่านี้ในโค้ด regex นี้จะหยุดทำงาน ซึ่งพิสูจน์ให้เห็นถึงปัญหาประการหนึ่งของการไม่เพิ่มข้อมูลเมตาโดยตรงผ่านแอตทริบิวต์ของบล็อก

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

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.

นอกจากนี้ คล้ายกับอาร์กิวเมนต์ GraphQL แบบสอบถามของเราสามารถปรับแต่งผ่านอาร์กิวเมนต์ฟิลด์ ทำให้สามารถรับเฉพาะข้อมูลที่เหมาะสมสำหรับแพลตฟอร์มเฉพาะ ตัวอย่างเช่น หากเราต้องการแยกวิดีโอ Youtube ทั้งหมดที่เพิ่มไปยังโพสต์ทั้งหมด เราสามารถเพิ่มตัวแก้ไข (blockname:core-embed/youtube) ลงในข้อมูล block-metadata ฟิลด์ใน URL ปลายทางได้ เช่นเดียวกับในลิงก์นี้ หรือถ้าเราต้องการแยกรูปภาพทั้งหมดจากโพสต์ใดโพสต์หนึ่ง เราสามารถเพิ่มตัวแก้ไข (blockname:core/image) เช่นเดียวกับในลิงก์อื่นนี้|id|title)

บทสรุป

กลยุทธ์ COPE ("สร้างครั้งเดียว เผยแพร่ได้ทุกที่") ช่วยให้เราลดปริมาณงานที่จำเป็นในการสร้างแอปพลิเคชันต่างๆ ที่ต้องทำงานบนสื่อต่างๆ (เว็บ อีเมล แอป ผู้ช่วยในบ้าน ความเป็นจริงเสมือน ฯลฯ) โดยการสร้างแหล่งเดียว ของความจริงสำหรับเนื้อหาของเรา เกี่ยวกับ WordPress แม้ว่ามันจะโดดเด่นในฐานะระบบจัดการเนื้อหา แต่การใช้กลยุทธ์ COPE ได้พิสูจน์แล้วในอดีตว่าเป็นสิ่งที่ท้าทาย

อย่างไรก็ตาม การพัฒนาล่าสุดสองสามอย่างทำให้มีความเป็นไปได้มากขึ้นที่จะใช้กลยุทธ์นี้สำหรับ WordPress ด้านหนึ่ง เนื่องจากการผสานเข้ากับแกนหลักของ WP REST API และที่เด่นชัดมากขึ้นตั้งแต่เปิดตัว Gutenberg เนื้อหา WordPress ส่วนใหญ่สามารถเข้าถึงได้ผ่าน API ทำให้เป็นระบบที่ไม่มีหัวอย่างแท้จริง ในอีกด้านหนึ่ง Gutenberg (ซึ่งเป็นตัวแก้ไขเนื้อหาเริ่มต้นใหม่) เป็นแบบบล็อก ทำให้ API เข้าถึงข้อมูลเมตาทั้งหมดภายในบล็อกโพสต์ได้อย่างง่ายดาย

ด้วยเหตุนี้ การใช้ COPE สำหรับ WordPress จึงไม่ซับซ้อน ในบทความนี้ เราได้เห็นวิธีการดำเนินการดังกล่าวแล้ว และโค้ดที่เกี่ยวข้องทั้งหมดได้รับการเผยแพร่ผ่านที่เก็บหลายแห่ง แม้ว่าโซลูชันจะไม่เหมาะสมที่สุด (เนื่องจากต้องมีการแยกวิเคราะห์โค้ด HTML จำนวนมาก) แต่ก็ยังใช้งานได้ดี ผลที่ตามมาคือความพยายามที่จำเป็นในการเผยแพร่แอปพลิเคชันของเราไปยังหลายแพลตฟอร์มสามารถลดลงได้อย่างมาก รุ่งโรจน์ไปที่นั่น!