من اكثر الامور التي تغيرت في السنوات الماضية هي طريقة البحث عن المعلومة فبعدما كنا نعتمد بشكل كامل على محركات البحث و نحاول الكثير حتى يتم يظهر موقعنا في اعلى نتائج البحث، اصبحت الناس الآن تستخدم الذكاء الصنعي للحصول على المعلومات المطلوبة و بالتالي اصبح عدم ظهور موقعك ضمن اجوبة اي سؤال يتم ذكره للذكاء الصنعي فكأن موقعك لم يوجد على الاطلاق.
قامت شركة Semrush بدراسة عن مصدر الزوار للمواقع و تنبأوا بأنه في عام ٢٠٢٨ سيكون الزوار القادمين من عن طريق الذكاء الصنعي ستتجاوز تلك التي سيكون مصدرها محركات البحث.
و باعتبار ان الوردبريس هو من اكثر البرامج المستخدمة في نشر المعلومات و البيانات فمن الطبيعي ان يتم البحث عن حل لهذا الامر و الذي يقدم افضل نتيجة باقل وقت و دون اي جهد اضافي, من خلال الاستفادة من الترويسة Accept لمعرفة الصيغة التي يطلبها العميل. كما هو الحال عندما تقوم بطلب رابط معين مثلا
const response = await fetch("https://example.org/post", {
headers: {
Accept: "text/markdown",
},
});
وفي هذه الحالة يقوم الخادم بإرجاع الاستجابة مع الترويسة Content-Type: text/markdown.
عندما يرسل العميل Accept: text/markdown فإنه يتوقع الحصول على المحتوى بصيغة Markdown. لذلك بدلاً من إعادة صفحة HTML كاملةً، سنستخرج محتوى المقال أو الصفحة فقط ثم نحوّله إلى Markdown. وهكذا لا تُرسَل عناصر القالب، مثل التنقل والتصميم، في الاستجابة أصلاً.
لكن للأسف الوردبريس بشكل طبيعي لا يقوم بمثل هذا الامر، و لكن هنالك اضافتان موجودتان على الموقع الرئيسي للاضافات تسمح بمثل هذا الامر و تقوم بحذف كل شيء غير اساسي و لا يفيد الذكاء الصنعي. و سنقوم بكتابة الاضافة الثالثة الخاصة بنا، بهدف فهم الموضوع و كيف يمكننا معالجته.
➜ curl -L -H "Accept: text/markdown" https://wordpress.test/hello-world
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name='robots' content='noindex, nofollow' />
<title>Hello world! – WordPress Plugins</title>
<link rel="alternate" type="application/rss+xml" title="WordPress Plugins » Feed" href="https://wordpress.test/feed/" />
<link rel="alternate" type="application/rss+xml" title="WordPress Plugins » Comments Feed" href="https://wordpress.test/comments/feed/" />
<link rel="alternate" type="application/rss+xml" title="WordPress Plugins » Hello world! Comments Feed" href="https://wordpress.test/hello-world/feed/" />
<link rel="alternate" title="oEmbed (JSON)" type="application/json+oembed" href="https://wordpress.test/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fwordpress.test%2Fhello-world%2F" />
<link rel="alternate" title="oEmbed (XML)" type="text/xml+oembed" href="https://wordpress.test/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fwordpress.test%2Fhello-world%2F&format=xml" />
<style id="wp-img-auto-sizes-contain-inline-css">
img:is([sizes=auto i],[sizes^="auto," i]){contain-intrinsic-size:3000px 1500px}
/*# sourceURL=wp-img-auto-sizes-contain-inline-css */
</style>
<style id="wp-block-site-title-inline-css">
.wp-block-site-title {
box-sizing: border-box;
}
.wp-block-site-title :where(a) {
color: inherit;
font-family: inherit;
font-size: inherit;
font-style: inherit;
font-weight: inherit;
letter-spacing: inherit;
line-height: inherit;
text-decoration: inherit;
}
/*# sourceURL=https://wordpress.test/wp/wp-includes/blocks/site-title/style.css */
</style>
<style id="wp-block-page-list-inline-css">
.wp-block-navigation .wp-block-page-list {
display: flex;
flex-direction: var(--navigation-layout-direction, initial);
justify-content: var(--navigation-layout-justify, initial);
align-items: var(--navigation-layout-align, initial);
flex-wrap: var(--navigation-layout-wrap, wrap);
background-color: inherit;
}
.wp-block-navigation .wp-block-navigation-item {
background-color: inherit;
}
كما تشاهد فبشكل عادي و بدون اي تعديل يقوم الوردبريس باعادة الصفحة بشكل كامل.
المشكلة الأساسية
عند طلب رابط ما فان ما يتم ارجاعه بشكل كامل هو صفحة الـ HTML كاملة و التي يستطيع اي متصفح فهمها و بالتالي اظهارها لنا، لكن كما ذكرنا فالذكاء الصنعي لا يهتم لهذه الامور و انما يهتم للنص الموجود وخاصة أن جزءاً كبيراً من عناصر HTML يتعلق بالعرض والتنسيق وليس بالمحتوى نفسه، مما يزيد عدد الوحدات النصية (Tokens) .دون إضافة قيمة كبيرة للنموذج
الحل للمشكلة
كما ذكرنا سابقا الحل يكون ببناء اضافة للوردبريس تقوم باعتراض الطلب و ارجاع النص بعد تحويله لنص كتابي يستهلك عدد بسيط من الوحدات النصية (Tokens).
المتطلبات
حتى نستطيع كتابة الاضافة لابد لنا من معرفة كيف يقوم وردبريس بتفسير و معالجة اي طلب لأي صفحة، و سأحاول قدر الامكان شرح الموضوع حسب مقدرتي، لكني انصح بمراجعة هذا الفيديو الذي يشرح الموضوع The WordPress request lifecycle، ففهم هذه النقطة ضروري لتحديد الخطاف Hook .الأنسب للاستخدام
دورة معالجة الطلب في ووردبريس
اي طلب للصفحات الخارجية لموقع وردبريس يتم معالجته بملف الـ index.php، طبعا هنا القصد هو اي طلب للموقع ليس له علاقة بقسم الإدارة. و تكون الدورة منذ طلب الرابط و حتى ارجاع المحتوى للمستخدم كالتالي، مع الغاء الكثير من الامور و التركيز على الامور الاساسية لنا:
sequenceDiagram
autonumber
participant Client as المستخدم
participant WordPress as وردبريس
participant TemplateLoader as template-loader.php
participant Theme as التصميم
Client->>WordPress: طلب مقالة
WordPress->>WordPress: احضار معلومات عن المقالة\الصفحة
WordPress->>TemplateLoader: قراءة template-loader.php
TemplateLoader->>TemplateLoader: template_redirect تنفيذ خطاف
TemplateLoader->>Theme: تحميل التصميم
Theme-->>Client: ارجاع محتويات الصفحة مع الاوسمة
عندما تقوم بطلب اي رابط لموقع يعتمد على الوردبريس، فان طلبك يعالج بشكل اساسي من قبل ملف index.php لكن اذا ما قمت باستعراض محتويات الملف فستجدها بسيطة فقط سطرين.
define( 'WP_USE_THEMES', true );
require __DIR__ . '/wp-blog-header.php';
يقوم هذا الملف بطلب الملف wp-blog-header.php بشكل داخلي و يظهر خطأ كبير في حال لم يوجد هذا الملف و هذا الملف يقوم بثلاث عمليات:
- طلب الملف
wp-load.phpليكون متوفرا و الذي يقوم بتوفير كل مكاتب وردبريس - تهيئة استعلام وردبريس
WordPress query - طلب الملف
template-loader.php
بشكل مبسط جداً، يقوم التابع wp() بقراءة ما تم ارساله مع الطلب URL Parameters لتحديد ما يجب قرائته من قواعد البيانات و ماهو السجل المطلوب و يعيده إلنيا.
و بذلك ننتقل للمرحلة الثانية و هي الملف template-loader.php و الذي اول ما يقوم به في حال ان المتحول الثابت WP_USE_THEMES تم تعينه فيقوم بتنفيذ الخطاف البرمجي template_redirect. و بعد ذلك يعيد كل شيء ليتم ارساله للمستخدم.
النص البرمجي
حتى نستطيع الرد بالشكل المطلوب يجب علينا اولا استخدام مكتبة تقوم بتحويل اوسام HTML إلى Markdown و سنقوم باستخدام html-to-markdown.
mkdir wp-markdown-plugin && cd wp-markdown-plugin
composer require league/html-to-markdown
و هذه هي البداية فقط، حيث أننا الآن نمتلك ما نريد للبدء الحقيقي بكتابة النص البرمجي، و الخطوة الاولى تكون من خلال كتابة ملف الاضافة و الذي يجب ان يكون بنفس اسم المجلد اي wp-markdown-plugin.php و اضافة بعض المعلومات المبسطة و المطلوبة من قبل وردبريس، و لن اقوم بشرحها حيث ان التركيز هو على النص البرمجي اكثر من متطلبات الوردبريس و التي من السهل فهمها حيث تعبر كل جملة عن الغاية منها:
<?php
/**
* Plugin Name: WP Markdown Plugin
* Description: Serve clean Markdown versions of post content when Accept: text/markdown header is present.
* Version: 1.0.0
* Author: Zaher Ghaibeh
* Author URI: https://arabic.dev
* License: MIT
* Requires PHP: 7.4
*/
if (!defined('ABSPATH')) {
exit;
}
// Autoload Composer dependencies
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
require_once __DIR__ . '/vendor/autoload.php';
}
use League\HTMLToMarkdown\HtmlConverter;
use League\HTMLToMarkdown\Converter\TableConverter;
الآن و بعدها نريد أن نقوم بكتابة النص البرمجي الذي سيتم تنفيذه عن طلب الخطاف الاساسي لنا و هو template_redirec و هذا الخطاف من نوع action، و هو نوع الخطاف الذي يتم تنفيذه في وقت محدد من الزمان.
و سنقوم ضمن هذا الخطاف بمايلي:
بداية لا نريد لأي طلب ان يتم معالجته لذلك سنقوم فقط بالإجابة على فقط طلبات الـ GET
// Only GET requests — agents fetch URLs, nothing else.
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
return;
}
التأكد من ان الطلب لم يأتي من مدير للموقع أو من طلب JSON أو أن المطلوب ليس بنوع ذو محتوى وحيد.
// Only on singular content (posts, pages, CPTs)
if (!is_singular() || is_admin() || wp_is_json_request()) {
return;
}
التأكد من أن الترويسة Accept تحتوي على القيمة المطلوبة لنا
// Check for text/markdown in Accept header
$accept = sanitize_text_field(wp_unslash($_SERVER['HTTP_ACCEPT'] ?? ''));
if (stripos($accept, 'text/markdown') === false) {
return;
}
طبعا باعتبار أننا لا نستطيع الوثوق بكل ما يتم إرساله لنا فلابد من القيام ببعض عمليات التأكد من صحة القيمة المرسلة.
كمرحلة احترازية نقوم بالتأكد من أن المكتبة التي نريد موجودة، و الإفتراض انها موجودة مع الإضافة:
if (!class_exists(HtmlConverter::class)) {
return;
}
المرحلة ما قبل الأخيرة تكون بالتأكد من معالجة الطلب من خلال معالجة المحتوى الخاص بالمقال أو الصفحة حتى تصبح متكاملة بأوسمة HTML:
$post = get_post();
// Do not disclose password-protected content.
if (post_password_required($post)) {
return;
}
// Get post content and run through the_content filters
// https://developer.wordpress.org/reference/hooks/the_content/#usage
// This processes blocks, shortcodes, wpautop, etc.
$html = apply_filters('the_content', $post->post_content);
ما تبقى هو عبارة عن معالجة للمحتوى حتى يكون مناسبا لتحويل محتوى الـ HTML إلى Markdown و يمكن التوسع بشكل أكبر بها من خلال قراءة التعليمات الخاصة بالمكتبة المستخدمة هنا
// Convert to Markdown
$converter = new League\HTMLToMarkdown\HtmlConverter([
'header_style' => 'atx',
'strip_tags' => true,
]);
// Enable table conversion — disabled by default, Gutenberg table blocks
// would otherwise degrade to plain text.
$converter->getEnvironment()->addConverter(new TableConverter());
$markdown = $converter->convert($html);
// Clean up whitespace
$markdown = preg_replace('/\h+$/m', '', $markdown);
$markdown = preg_replace('/(\r?\n){3,}/', "\n\n", $markdown);
$markdown = trim($markdown);
// Build response
$title = sprintf("# %s\n\n", get_the_title());
$meta = sprintf("*%s — %s*\n\n", get_the_date(), get_permalink());
$markdown = $title . $meta . $markdown;
// Prepend frontmatter-style metadata
$frontmatter = sprintf(
"---\ntitle: %s\ndate: %s\nslug: %s\ntype: %s\n---\n\n",
json_encode(get_the_title()),
get_the_date('c'),
$post->post_name,
get_post_type()
);
$markdown = $frontmatter . $markdown;
و بعد الانتهاء من سيصبح لدينا نص برمجي كالتالي:
<?php
/**
* Plugin Name: WP Markdown Plugin
* Description: Serve clean Markdown versions of post content when Accept: text/markdown header is present.
* Version: 1.0.0
* Author: Zaher Ghaibeh
* Author URI: https://arabic.dev
* License: MIT
* Requires PHP: 7.4
*/
if (!defined('ABSPATH')) {
exit;
}
// Autoload Composer dependencies
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
require_once __DIR__ . '/vendor/autoload.php';
}
use League\HTMLToMarkdown\Converter\TableConverter;
use League\HTMLToMarkdown\HtmlConverter;
add_action('template_redirect', function () {
// Only on singular content (posts, pages, CPTs)
if (!is_singular() || is_admin() || wp_is_json_request()) {
return;
}
// Only GET requests — agents fetch URLs, nothing else.
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
return;
}
// Check for text/markdown in Accept header.
if (stripos(wp_unslash($_SERVER['HTTP_ACCEPT'] ?? ''), 'text/markdown') === false) {
return;
}
// Bail if converter isn't available
if (!class_exists(HtmlConverter::class)) {
return;
}
$post = get_post();
// Do not disclose password-protected content.
if (post_password_required($post)) {
return;
}
// Get post content and run through the_content filters
// This processes blocks, shortcodes, wpautop, etc.
$html = apply_filters('the_content', $post->post_content);
// Convert to Markdown
$converter = new HtmlConverter([
'header_style' => 'atx',
'strip_tags' => true,
]);
// Enable table conversion — disabled by default, Gutenberg table blocks
// would otherwise degrade to plain text.
$converter->getEnvironment()->addConverter(new TableConverter());
$markdown = $converter->convert($html);
// Clean up whitespace
$markdown = preg_replace('/\h+$/m', '', $markdown);
$markdown = preg_replace('/(\r?\n){3,}/', "\n\n", $markdown);
$markdown = trim($markdown);
// Build response
$title = sprintf("# %s\n\n", get_the_title());
$meta = sprintf("*%s — %s*\n\n", get_the_date(), get_permalink());
$markdown = $title . $meta . $markdown;
// Prepend frontmatter-style metadata
$frontmatter = sprintf(
"---\ntitle: %s\ndate: %s\nslug: %s\ntype: %s\n---\n\n",
json_encode(get_the_title()),
get_the_date('c'),
$post->post_name,
get_post_type()
);
$markdown = $frontmatter . $markdown;
// Send headers and response
header('Content-Type: text/markdown; charset=utf-8');
header('Vary: Accept');
header('X-Content-Type-Options: nosniff');
exit($markdown);
});
و طلب اي صفحة من الموقع سيصبح كالتالي:
➜ curl -L -H "Accept: text/markdown" https://wordpress.test/hello-world
---
title: "Hello world!"
date: 2026-05-21T12:14:16+00:00
slug: hello-world
type: post
---
# Hello world!
*May 21, 2026 — https://wordpress.test/hello-world/*
Welcome to WordPress. This is your first post. Edit or delete it, then start writing!