"أنشئ مرة واحدة ، انشر في كل مكان" باستخدام 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) ، ويمكّن النظام البيئي للمكونات الإضافية من إضافة وظائف جديدة بسهولة.
  • إنه واسع الانتشار.
    تشير التقديرات إلى أن ثلث مواقع الويب تعمل على WordPress. بعد ذلك ، هناك عدد كبير من الأشخاص الذين يعملون على الويب يعرفون ويستطيعون استخدامه ، مثل WordPress. ليس فقط المطورين ولكن أيضًا المدونين والبائعين وموظفي التسويق وما إلى ذلك. بعد ذلك ، سيتمكن العديد من أصحاب المصلحة المختلفين ، بغض النظر عن خلفيتهم التقنية ، من إنتاج المحتوى الذي يعمل كمصدر وحيد للحقيقة.
  • إنه مقطوع الرأس.
    انعدام الرأس هو القدرة على فصل المحتوى عن طبقة العرض ، وهي ميزة أساسية لتنفيذ COPE (لتكون قادرًا على تغذية البيانات إلى وسائط غير متشابهة).

    منذ دمج WP REST API في النواة بدءًا من الإصدار 4.7 ، وبشكل أكثر وضوحًا منذ إطلاق Gutenberg في الإصدار 5.0 (حيث كان يجب تنفيذ الكثير من نقاط نهاية REST API) ، يمكن اعتبار WordPress نظام إدارة محتوى بدون رأس ، نظرًا لأن معظم محتوى WordPress يمكن الوصول إليها من خلال REST API بواسطة أي تطبيق مبني على أي مكدس.

    بالإضافة إلى ذلك ، فإن WPGraphQL الذي تم إنشاؤه مؤخرًا يدمج WordPress و GraphQL ، مما يتيح تغذية المحتوى من WordPress إلى أي تطبيق باستخدام واجهة برمجة التطبيقات الشائعة بشكل متزايد. أخيرًا ، أضاف مشروع PoP الخاص بي مؤخرًا تطبيقًا لواجهة برمجة تطبيقات لـ WordPress والذي يسمح بتصدير بيانات WordPress إما بتنسيقات REST أو GraphQL أو PoP الأصلية.
  • يحتوي على Gutenberg ، وهو محرر قائم على الكتلة يساعد بشكل كبير في تنفيذ COPE لأنه يعتمد على مفهوم الكتل (كما هو موضح في الأقسام أدناه).

النقط مقابل كتل لتمثيل المعلومات

النقطة الثنائية الكبيرة هي وحدة واحدة من المعلومات يتم تخزينها معًا في قاعدة البيانات. على سبيل المثال ، ستؤدي كتابة منشور المدونة أدناه على نظام إدارة المحتوى الذي يعتمد على الكتل الكبيرة لتخزين المعلومات إلى تخزين محتوى منشور المدونة في إدخال قاعدة بيانات واحد - يحتوي على نفس المحتوى:

 <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 بطريقة مباشرة.

كيف ينفذ جوتنبرج الكتل

قبل الإصدار 5.0 من WordPress ، استخدم WordPress blobs لتخزين محتوى المنشور في قاعدة البيانات. بدءًا من الإصدار 5.0 ، يأتي WordPress مع Gutenberg ، وهو محرر قائم على الكتلة ، مما يتيح طريقة محسّنة لمعالجة المحتوى المذكور أعلاه ، وهو ما يمثل تقدمًا نحو تنفيذ COPE. لسوء الحظ ، لم يتم تصميم 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 -->

باستثناء الكتل العامة (تسمى أيضًا "قابلة لإعادة الاستخدام") ، والتي لها إدخال خاص بها في قاعدة البيانات ويمكن الرجوع إليها مباشرة من خلال معرفاتها ، يتم حفظ جميع الكتل معًا في إدخال منشور المدونة في الجدول 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 مع معلمة ثانية تحتوي على العلامات المسموح بها ، ونضعها داخل دالة التفاف للراحة:

 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 نفسه). ومع ذلك ، فقد حدث هذا الانتهاك بالفعل: على سبيل المثال ، تحتوي السمة className على القيمة "wp-embed-aspect-16-9 wp-has-aspect-ratio" ، والتي تتم طباعتها داخل المحتوى أيضًا ، ضمن class سمة HTML.

إضافة محتوى من خلال جوتنبرج
إضافة محتوى من خلال Gutenberg (معاينة كبيرة)

تنفيذ COPE

ملاحظة: لقد أصدرت هذه الوظيفة ، بما في ذلك جميع الكود الموصوف أدناه ، كمكوِّن إضافي لـ WordPress Block Metadata. نرحب بتثبيته واللعب به حتى تتمكن من تذوق قوة COPE. كود المصدر متاح في GitHub repo.

الآن بعد أن عرفنا كيف يبدو التمثيل الداخلي للكتلة ، دعنا ننتقل إلى تنفيذ COPE من خلال Gutenberg. سيتضمن الإجراء الخطوات التالية:

  1. نظرًا لأن الدالة parse_blocks($content) تُرجع كائن JSON بمستويات متداخلة ، يجب علينا أولاً تبسيط هذه البنية.
  2. نقوم بتكرار جميع الكتل ، ولكل منها ، نحدد أجزاء البيانات الوصفية الخاصة بها ونستخرجها ، ونحولها إلى تنسيق متوسط ​​الحياد في العملية. يمكن أن تختلف السمات التي تمت إضافتها إلى الاستجابة وفقًا لنوع الكتلة.
  3. أخيرًا نجعل البيانات متاحة من خلال واجهة برمجة التطبيقات (REST / GraphQL / PoP).

دعنا ننفذ هذه الخطوات واحدة تلو الأخرى.

1. تبسيط بنية كائن JSON

كائن JSON الذي تم إرجاعه من دالة parse_blocks($content) له بنية متداخلة ، حيث تظهر بيانات الكتل العادية في المستوى الأول ، لكن البيانات الخاصة بالكتلة المرجعية القابلة لإعادة الاستخدام مفقودة (تتم إضافة بيانات كتلة المرجع فقط) ، وتظهر بيانات الكتل المتداخلة (التي تمت إضافتها داخل الكتل الأخرى) والكتل المجمعة (حيث يمكن تجميع عدة كتل معًا) تحت مستوى فرعي واحد أو أكثر. تجعل هذه البنية من الصعب معالجة بيانات الحظر من جميع الكتل في محتوى المنشور ، نظرًا لأن بعض البيانات مفقودة من جانب ، ومن ناحية أخرى لا نعرف مسبقًا ضمن عدد بيانات المستويات الموجودة. بالإضافة إلى ذلك ، يوجد حاجز كتلة يوضع كل زوج من الكتل ، لا يحتوي على محتوى ، والذي يمكن تجاهله بأمان.

على سبيل المثال ، الاستجابة التي تم الحصول عليها من منشور يحتوي على كتلة بسيطة ، وكتلة عامة ، وكتلة متداخلة تحتوي على كتلة بسيطة ، ومجموعة من الكتل البسيطة ، بهذا الترتيب ، هي كما يلي:

 [ // 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'

يحتوي الحظر إما على معرف يشير إلى ملف وسائط تم تحميله أو ، إذا لم يكن كذلك ، يجب استخراج مصدر الصورة من ضمن <img src="..."> . العديد من السمات (التسمية التوضيحية ، الارتباط ، الارتباط ، المحاذاة) اختيارية.

 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 بتغيير الترتيب الذي تُطبع به هذه الخصائص في الكود ، فحينئذٍ سيتوقف هذا التعبير العادي عن العمل ، مما يدل على إحدى مشكلات عدم إضافة البيانات الوصفية مباشرةً من خلال سمات الكتلة.

 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) كما هو الحال في هذا الرابط الآخر | معرف | العنوان).

خاتمة

تساعدنا استراتيجية COPE ("الإنشاء مرة واحدة ، والنشر في كل مكان") على تقليل حجم العمل المطلوب لإنشاء العديد من التطبيقات التي يجب أن تعمل على وسائط مختلفة (الويب ، والبريد الإلكتروني ، والتطبيقات ، والمساعدين المنزليين ، والواقع الافتراضي ، وما إلى ذلك) من خلال إنشاء مصدر واحد الحقيقة لمحتوانا. فيما يتعلق بـ WordPress ، على الرغم من أنه يتألق دائمًا كنظام إدارة محتوى ، إلا أن تنفيذ استراتيجية COPE أثبت تاريخياً أنه يمثل تحديًا.

ومع ذلك ، فإن بعض التطورات الأخيرة جعلت من الممكن بشكل متزايد تنفيذ هذه الاستراتيجية في WordPress. من ناحية ، منذ الاندماج في جوهر WP REST API ، وبشكل أكثر وضوحًا منذ إطلاق Gutenberg ، يمكن الوصول إلى معظم محتوى WordPress من خلال واجهات برمجة التطبيقات ، مما يجعله نظامًا حقيقيًا بدون رأس. على الجانب الآخر ، فإن Gutenberg (وهو محرر المحتوى الافتراضي الجديد) قائم على الكتلة ، مما يجعل جميع البيانات الوصفية داخل منشور مدونة يمكن الوصول إليها بسهولة من خلال واجهات برمجة التطبيقات.

نتيجة لذلك ، يعد تطبيق COPE لـ WordPress أمرًا سهلاً. في هذه المقالة ، رأينا كيفية القيام بذلك ، وتم توفير جميع التعليمات البرمجية ذات الصلة من خلال عدة مستودعات. على الرغم من أن الحل ليس هو الأمثل (نظرًا لأنه يتضمن الكثير من تحليل تعليمات HTML البرمجية) ، إلا أنه لا يزال يعمل بشكل جيد إلى حد ما ، ونتيجة لذلك يمكن تقليل الجهد المطلوب لإصدار تطبيقاتنا على منصات متعددة بشكل كبير. مجد لذلك!