اتخطّى للمحتوى

المرحلة 02 · البيانات والواجهةدرس 11 من 2124 دقيقة قراءةآخر تحديث:

Lesson 11 / 21

SEOمن الأدمن للكود.

متجر سريع ومنتجات حلوة مش كفاية لو Google مش لاقيك. هنشوف Magento بيدّيك إيه جاهز للـ SEO من لوحة الأدمن، وإيه اللي لازم تكتبه بنفسك في الكود: الروابط، الـ canonical، الـ meta، الـ robots.txt، الـ sitemap، والـ structured data.

● Admin + Code URLs · Canonical Sitemap · Robots JSON-LD · hreflang

الـ SEO في Magento 2 بيتظبط من الأدمن ومن الكود. من الأدمن: روابط من غير index.php، و canonical tags، و meta لكل منتج و category، و robots.txt، و XML sitemap، و redirects 301. ومن الكود: noindex لصفحات الفلاتر بـ plugin، و JSON-LD و hreflang، لأن Magento Open Source مابيعملهمش لوحده.

// الفكرة الكبيرة قبل ما تبدأ الـ SEO في Magento تلات طبقات: إعدادات عامة في الأدمن بتتطبّق على المتجر كله، وبيانات كل صفحة (منتج، category، CMS page)، والكود للحاجات اللي الأدمن مابيعملهاش أصلًا. الإعدادات اللي هنا متراجعة على Magento Open Source 2.4.x، واللي بيختلف بين الإصدارات مكتوب جنبه.
◆ Part 1 — من لوحة الأدمن
01

الصورة الكاملة

قبل ما تلمس أي إعداد: إيه اللي Magento بيعمله لوحده، وإيه اللي محتاج شغل.

الحاجة
بتتعمل منين
روابط من غير index.php
الأدمن + إعداد الـ web server
Canonical للمنتجات والـ categories
الأدمن — مقفول افتراضيًا
Meta title و description
الأدمن — قيمة افتراضية للمتجر، وقيمة لكل صفحة
robots.txt و meta robots
الأدمن — من إعدادات الـ Design
XML Sitemap
الأدمن + الـ cron
Redirects 301
تلقائي لما الـ URL Key يتغيّر، ويدوي من URL Rewrites
Open Graph و microdata لصفحة المنتج
جاهز في الـ core
noindex لصفحات الفلاتر
كود — مش موجود
JSON-LD
كود — مش موجود
hreflang
كود — مش موجود في Open Source
تشبيه الأدمن زي يافطة المحل وعنوانه: لازم يكونوا واضحين ومايتكرّروش. الكود هو ترتيب الرفوف من جوّه: بيخلّي اللي داخل (Google) يفهم كل رف فيه إيه، ويعرف أنهي ممرّات مالهاش لازمة.
i
الكلام هنا على Magento Open Source. الـ Adobe Commerce مبني على نفس الأساس، بس ممكن يكون فيه خصائص زيادة مش موجودة في الكود المفتوح.
• • •
02

الروابط (URLs)

روابط نظيفة وثابتة، ولكل صفحة رابط واحد بس.

StoresSettingsConfigurationGeneralWeb
الإعداد
اعمل إيه
Search Engine Optimization › Use Web Server Rewrites
Yes. بيشيل index.php من الروابط. الافتراضي No.
Url Options › Auto-redirect to Base URL
Yes (301 Moved Permanently). الافتراضي بيحوّل بـ 302، والـ 301 هو اللي بيقول لـ Google إن التحويل دائم.
Base URLs (Secure) › Use Secure URLs on Storefront
Yes، بعد ما يكون الـ Secure Base URL بـ https، عشان المتجر كله يشتغل HTTPS.
!
Use Web Server Rewrites محتاج السيرفر يكون مظبوط للـ rewrites (الـ .htaccess في Apache أو إعداد nginx اللي جاي مع Magento). لو فعّلته والسيرفر مش جاهز، الروابط هتطلع 404.
نفس الإعدادات من الـ CLI
terminalbash
bin/magento config:set web/seo/use_rewrites 1
bin/magento config:set web/url/redirect_to_base 301
bin/magento config:set web/secure/use_in_frontend 1
bin/magento cache:flush
i
إعدادَي Enable HTTP Strict Transport Security (HSTS) وUpgrade Insecure Requests موجودين في نفس القسم، بس مش هيظهروا غير لما Use Secure URLs on Storefront وUse Secure URLs in Admin يكونوا الاتنين Yes.
StoresSettingsConfigurationCatalogCatalogSearch Engine Optimization
الإعداد
الافتراضي والتوصية
Product URL Suffix و Category URL Suffix
الافتراضي .html. تقدر تسيبه أو تشيله، بس قرّر قبل ما الموقع يتفهرس — تغييره بعدين بيغيّر كل الروابط.
Use Categories Path for Product URLs
الافتراضي No وسيبه كده. الـ Yes بيخلّي نفس المنتج ليه رابط مختلف حسب الـ category اللي جيت منها.
Generate "category/product" URL Rewrites
الافتراضي بقى No من 2.4.6 (كان Yes قبلها). الـ Yes بيولّد rewrite لكل منتج في كل category، وجدول url_rewrite بيكبر جدًا.
Create Permanent Redirect for URLs if URL Key Changed
الافتراضي Yes وسيبه. لما تغيّر الـ URL Key، الرابط القديم بيتحوّل 301 للجديد.
Apply transliteration for product URL
موجود من 2.4.8. بيتحكّم في تحويل حروف الـ URL key لحروف لاتينية — مهم لو أسامي منتجاتك بالعربي.
تشبيه الرابط زي عنوان البيت. لو البيت ليه تلات عناوين، البوسطجي (Google) هيحتار يودّي الجوابات فين، والزيارات والروابط اللي جاية للبيت هتتوزّع على تلات عناوين بدل ما تتجمّع في واحد.
!
أي تغيير في شكل الروابط بعد ما الموقع يتفهرس معناه إن كل رابط قديم لازم يتحوّل 301 للجديد. عشان كده الإعدادات دي تتظبط مرة واحدة في أول المشروع.
• • •
03

الـ Canonical Tags

لما نفس المحتوى يظهر على كذا رابط، قول لـ Google أنهي فيهم الأصل.

إيه هو

tag في الـ <head> شكله <link rel="canonical" href="…">. بيقول لمحرّك البحث: "الصفحة دي نسخة، والأصل هو الرابط ده". في متجر Magento النسخ بتيجي من الفلاتر، والترتيب، ورابط المنتج اللي فيه اسم الـ category.

StoresSettingsConfigurationCatalogCatalogSearch Engine Optimization
  • Use Canonical Link Meta Tag For Categories — الافتراضي No، خلّيه Yes.
  • Use Canonical Link Meta Tag For Products — الافتراضي No، خلّيه Yes.
بيشاور على إيه

المنتج: رابطه من غير الـ category. الـ category: رابطها من غير parameters الفلاتر. ومن 2.4.8 بيحتفظ بـ ?p=N في صفحات الترقيم، فكل صفحة ترقيم ليها canonical لنفسها بدل ما تشاور على الصفحة الأولى.

Magento/Catalog/Block/Category/View.php (core)php
// _prepareLayout(): how core adds the category canonical
// (before 2.4.8 the URL was simply $category->getUrl())
if ($this->_categoryHelper->canUseCanonicalTag()) {
    $this->pageConfig->addRemotePageAsset(
        $this->_categoryHelper->getCanonicalUrl($category->getUrl()),
        'canonical',
        ['attributes' => ['rel' => 'canonical']]
    );
}
terminalbash
bin/magento config:set catalog/seo/category_canonical_tag 1
bin/magento config:set catalog/seo/product_canonical_tag 1
bin/magento cache:flush
ASKلو فعّلت الـ canonical، لسه محتاج noindex للفلاتر؟
غالبًا أيوه. الـ canonical إشارة Google ممكن مياخدش بيها، وصفحات الفلاتر بتفضل بتتزحف وتاكل من وقت الزحف. الـ noindex توجيه صريح. الاتنين بيكمّلوا بعض، وهنعمل الـ noindex في الجزء التاني.
• • •
04

الـ Meta: افتراضي ولكل صفحة

العنوان والوصف اللي بيظهروا في نتيجة البحث.

ContentDesignConfigurationStore ViewHTML Head
إيه هو

لكل website أو store view بتحط قيم افتراضية، بتتستخدم لما الصفحة ملهاش قيمة خاصة بيها.

الحقل
بيعمل إيه
Default Page Title
العنوان لو الصفحة ملهاش title.
Page Title Prefix و Page Title Suffix
بيتضافوا لعناوين الصفحات، زي | اسم المتجر. خلّيهم قصيرين عشان مياكلوش من العنوان اللي بيظهر في النتايج.
Default Meta Description
وصف احتياطي. الأحسن إن كل صفحة مهمة يبقى ليها وصف خاص بيها.
Default Meta Keywords
Google مابيستخدمهاش في ترتيب النتايج.
Scripts and Style Sheets
كود بيتحط في الـ head، زي meta tag التحقق من Google Search Console.
CatalogProductsProductSearch Engine Optimization
  • URL Key — الجزء بتاع المنتج في الرابط. قصير ومقروء.
  • Meta Title — حوالي 50–60 حرف، والكلمة المهمة في الأول.
  • Meta Description — حوالي 150–160 حرف، ومختلف لكل منتج.
  • Meta Keywords — مش مهمة لـ Google، متضيّعش فيها وقت.
  • Create Permanent Redirect for old URL — لما تغيّر الـ URL Key، سيبها متعلّمة عشان الرابط القديم يتحوّل 301.
لكل لغة

الحقول دي على مستوى الـ store view. غيّر الـ Scope من أعلى صفحة المنتج للـ store view اللي عايزه، شيل علامة Use Default Value، واكتب قيم اللغة دي. نفس القسم موجود في Catalog › Categories.

الصفحة
ملاحظة
CMS Page
من Content › Elements › Pages. الـ page مش بتتترجم على مستوى الـ store view: لو عندك لغتين، اعمل صفحتين واربط كل واحدة بالـ store view بتاعها.
صور المنتج
اضغط على الصورة في Images And Videos واكتب Alt Text. بيتحفظ لكل store view.
StoresSettingsConfigurationCatalogCatalogProduct Fields Auto-Generation
الحقل
الافتراضي
Mask for Meta Title
{{name}}
Mask for Meta Keywords
{{name}}
Mask for Meta Description
{{name}} {{description}}
Mask for SKU
{{name}}
بتعمل إيه

بتملا حقول الـ meta تلقائيًا من بيانات المنتج. تقدر تغيّرها لحاجة زي {{name}} | اسم المتجر.

!
الـ masks دي JavaScript في فورم الأدمن بس: بتشتغل على المنتجات الجديدة، وبتقف أول ما تكتب بإيدك في الحقل. مابتأثرش على المنتجات اللي جاية من الـ import أو الـ API — دي لازم تملا الـ meta بتاعتها بنفسك.
• • •
05

robots.txt و Default Robots

إيه اللي محرّكات البحث تزحف عليه، وإيه اللي يتفهرس.

ContentDesignConfigurationWebsiteSearch Engine Robots
الحقل
بيعمل إيه
Default Robots
قيمة <meta name="robots"> لكل الصفحات: INDEX, FOLLOW (الافتراضي)، NOINDEX, FOLLOW، INDEX, NOFOLLOW، أو NOINDEX, NOFOLLOW.
Edit custom instruction of robots.txt File
محتوى ملف robots.txt. وزرار Reset to Defaults بيرجّع القيم الافتراضية.
إزاي الملف بيتقدّم

مفيش ملف حقيقي. موديول Magento_Robots عنده router بيلقط الطلب على /robots.txt ويولّد المحتوى من الإعداد ده (بيتقرا على مستوى الـ website). ولو فعّلت إضافة الـ sitemap (القسم الجاي)، Magento_Sitemap بيضيف سطور Sitemap: تحته.

custom instruction of robots.txttext
User-agent: *
Disallow: /checkout/
Disallow: /customer/
Disallow: /catalogsearch/
Disallow: /wishlist/
!
لو فيه ملف pub/robots.txt حقيقي على السيرفر، الـ web server بيقدّمه قبل ما الطلب يوصل لـ Magento (إعداد nginx والـ .htaccess بيقدّموا الملفات الموجودة الأول)، وإعدادات الأدمن بتتجاهل من غير أي رسالة. امسح الملف.
  • متقفلش ملفات الـ CSS والـ JS والصور في robots.txt — Google محتاجهم عشان يشوف الصفحة زي المستخدم.
  • robots.txt بيمنع الزحف مش الفهرسة. لو عايز تشيل صفحة من النتايج استخدم noindex، ومتقفلهاش في robots.txt، وإلا Google مش هيشوف الـ noindex أصلًا.
  • على الـ staging: خلّي Default Robots NOINDEX, NOFOLLOW — وبرضه احميه بباسورد.
• • •
06

الـ XML Sitemap

خريطة بكل روابطك المهمة، بتتحدّث لوحدها بالـ cron.

الخطوة 1 — اعمل الـ sitemap
MarketingSEO & SearchSite Map

ضيف sitemap جديد: اسم الملف (زي sitemap.xml)، المسار اللي هيتحفظ فيه، والـ store view. احفظه وولّده. لو عندك كذا store view، اعمل sitemap لكل واحد.

الخطوة 2 — الإعدادات والتحديث التلقائي
StoresSettingsConfigurationCatalogXML Sitemap
المجموعة
المهم فيها
Categories / Products / CMS Pages Options
Frequency وPriority لكل نوع. Google بيتجاهل القيمتين دول، فمتصرفش وقت عليهم.
Products Options › Add Images into Sitemap
None أو Base Only أو All (الافتراضي All).
Generation Settings › Enabled
Yes عشان الـ cron يعيد التوليد. الافتراضي No. ومعاه Start Time و Frequency.
Sitemap File Limits
أقصى عدد روابط في الملف (50,000) وأقصى حجم (10MB). لما تعدّيهم Magento بيقسّم الملف.
Search Engine Submission Settings › Enable Submission to Robots.txt
Yes عشان سطر Sitemap: يتضاف لـ robots.txt. الافتراضي No.
i
الـ cron job اسمه sitemap_generate، وبيعيد توليد الـ sitemaps اللي عملتها بالفعل في الخطوة 1 بس. ومفيش أمر bin/magento جاهز لتوليد الـ sitemap. ومن 2.4.9 فيه Generation Method تختار فيه Batch (Memory Optimized) للكتالوجات الكبيرة.
بعد ما يتولّد: ضيف رابط الـ sitemap في Google Search Console، واتأكد إن الـ cron شغّال على السيرفر.
• • •
07

URL Rewrites والـ Redirects

لما رابط يتغيّر أو تنقل من منصة قديمة، من غير ما تخسر ترتيبك.

MarketingSEO & SearchURL RewritesAdd URL Rewrite

أول اختيار Create URL Rewrite: Custom، أو For Category، أو For Product، أو For CMS page. بعدها:

الحقل
معناه
Store
الـ store view اللي الـ rewrite شغّال عليه.
Request Path
الرابط اللي الزائر طالبه (القديم)، زي old-shop/iphone-15.php.
Target Path
الرابط اللي يروح له.
Redirect Type
No: الرابط يفضل زي ما هو والمحتوى ييجي من الـ target. Temporary (302): تحويل مؤقت. Permanent (301): تحويل دائم — ده اللي بتستخدمه للـ SEO.
  • التلقائي: لما تغيّر الـ URL Key لمنتج أو category وخيار الـ permanent redirect متعلّم، Magento بيضيف الـ 301 لوحده.
  • بعد migration: اعمل ملف بالروابط القديمة والجديدة، وكل رابط قديم له traffic أو backlinks ياخد 301 مباشر لأقرب صفحة جديدة.
  • تجنّب السلاسل: A → B → C بتضيّع وقت الزحف. خلّي A يروح لـ C على طول.
!
مفيش أمر في الـ core بيعيد توليد جدول url_rewrite. لو اتبوّظ، الحلول بتبقى script مخصوص أو extension — عشان كده خد backup قبل أي تعديل جماعي في الروابط.
• • •
◆ Part 2 — من الكود
08

الـ head من Layout XML و PHP

للصفحات اللي إنت عاملها في موديولك، أو لما القيمة محتاجة تتحسب.

إمتى

صفحة custom في موديولك (زي صفحة البراندات) والعنوان والوصف ثابتين. الـ <head> في الـ layout بيقبل title وmeta وlink وcss وscript وremove وattribute.

view/frontend/layout/brands_index_index.xmlxml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <head>
        <title>كل البراندات</title>
        <meta name="description" content="تصفّح كل البراندات المتاحة في المتجر."/>
        <meta name="robots" content="INDEX,FOLLOW"/>
    </head>
</page>
i
عايز Open Graph من الـ XML؟ الـ <meta> في الـ XSD مفيهوش attribute اسمه property. اكتب name="og:title"، وMagento بيطلّع أي اسم بيبدأ بـ og: على شكل property تلقائيًا.
!
الـ canonical فيه دومين. لو كتبته ثابت في الـ XML، هيشاور على الـ production وإنت على الـ staging. ابنيه في PHP زي الكارت الجاي.
إيه هو

Magento\Framework\View\Page\Config هو اللي ماسك الـ head. بتوصله من الـ result page في الـ controller، أو من $this->pageConfig جوّه أي block بيورّث من Template (في _prepareLayout()، زي ما الـ core بيعمل الـ canonical بتاع الـ category).

Controller/Index/Index.phpphp
<?php
declare(strict_types=1);

namespace Vendor\Brands\Controller\Index;

use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\UrlInterface;
use Magento\Framework\View\Result\Page;
use Magento\Framework\View\Result\PageFactory;

class Index implements HttpGetActionInterface
{
    public function __construct(
        private readonly PageFactory $pageFactory,
        private readonly UrlInterface $url
    ) {
    }

    public function execute(): Page
    {
        $page = $this->pageFactory->create();
        $config = $page->getConfig();

        $config->getTitle()->set('كل البراندات');
        $config->setDescription('تصفّح كل البراندات المتاحة في المتجر.');
        $config->setRobots('INDEX,FOLLOW');
        $config->addRemotePageAsset(
            $this->url->getUrl('brands'),
            'canonical',
            ['attributes' => ['rel' => 'canonical']]
        );

        return $page;
    }
}
الـ method
بتعمل إيه
getTitle()->set() / prepend() / append()
عنوان الصفحة (الـ Prefix والـ Suffix بتوع الأدمن بيتضافوا عليه).
setDescription() / setKeywords() / setRobots()
اختصارات لـ setMetadata().
setMetadata($name, $content)
أي meta tag، والـ content بيتعمله escape.
addRemotePageAsset($url, $contentType, $properties)
بيضيف <link> زي الـ canonical.
• • •
09

noindex لصفحات الفلاتر

الـ layered navigation بيولّد آلاف الروابط لنفس المحتوى.

المشكلة

كل تركيبة فلاتر رابط جديد: ?color=49، ?color=49&size=167، ومعاهم الترتيب. Magento مفيهوش noindex جاهز للصفحات دي. الـ canonical (لو مفعّل) بيشاور على الـ category من غير فلاتر، بس زي ما قلنا ده إشارة مش أمر.

الحل

plugin من نوع after على Magento\Catalog\Controller\Category\View::execute(). لو الرابط فيه query parameters غير الترقيم، نحط NOINDEX,FOLLOW: Google مايفهرسش الصفحة، بس يكمّل ماشي في روابط المنتجات اللي فيها.

etc/frontend/di.xmlxml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Catalog\Controller\Category\View">
        <plugin name="vendor_seo_noindex_filtered_category"
                type="Vendor\Seo\Plugin\NoindexFilteredCategory"/>
    </type>
</config>
Plugin/NoindexFilteredCategory.phpphp
<?php
declare(strict_types=1);

namespace Vendor\Seo\Plugin;

use Magento\Catalog\Controller\Category\View;
use Magento\Framework\App\Request\Http;
use Magento\Framework\View\Result\Page;

class NoindexFilteredCategory
{
    /** Query params that don't make a different page (pagination). */
    private const ALLOWED_PARAMS = ['p'];

    public function __construct(private readonly Http $request)
    {
    }

    public function afterExecute(View $subject, $result)
    {
        // execute() can also return a redirect or a 404 forward
        if (!$result instanceof Page) {
            return $result;
        }

        $params = array_keys($this->request->getQuery()->toArray());
        if (array_diff($params, self::ALLOWED_PARAMS)) {
            $result->getConfig()->setRobots('NOINDEX,FOLLOW');
        }

        return $result;
    }
}
ليه ده بيشتغل

الـ controller بيرجّع الـ Page من غير ما يرسمها. الرسم بيحصل بعد ما execute() تخلص، فالـ robots اللي حطيناه في الـ after plugin هو اللي بيطلع في الـ HTML. والموديول نفسه (registration.php وmodule.xml) زي درس هيكل الـ Module.

i
القايمة ALLOWED_PARAMS عدّلها حسب متجرك. الترتيب (product_list_order) مثلًا بيعمل نسخ لنفس الصفحة، فسيبه يتعمله noindex.
• • •
10

Structured Data و hreflang

تفهّم Google المنتج نفسه، وتقوله أنهي لغة لأنهي جمهور.

  • Microdata (schema.org) على صفحة المنتج: Product والاسم والـ SKU والوصف والصورة والسعر (Offer)، والتقييمات من موديول الـ Review. جاية من templates الـ modules مش من ملفات Luma.
  • Open Graph لصفحة المنتج: og:title وog:image وog:description وog:url والسعر.
  • JSON-LD: مفيش خالص في الـ core.
!
لو الـ theme بتاعك عامل override للـ templates دي، ممكن الـ microdata تكون اتشالت من غير ما تاخد بالك. واختار طريقة واحدة للـ Product data: متطلّعش microdata و JSON-LD بقيم مختلفة لنفس المنتج. اختبر الصفحة في Rich Results Test.
الفكرة

template صغير جوّه head.additional. خلّي بالك: head.additional block مش container، فبنستخدم referenceBlock. والبيانات بتتجهّز في ViewModel عشان الـ template يفضل نظيف.

view/frontend/layout/catalog_product_view.xmlxml
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="head.additional">
            <block class="Magento\Catalog\Block\Product\View"
                   name="vendor.seo.product.jsonld"
                   template="Vendor_Seo::product/json-ld.phtml">
                <arguments>
                    <argument name="view_model" xsi:type="object">Vendor\Seo\ViewModel\ProductJsonLd</argument>
                </arguments>
            </block>
        </referenceBlock>
    </body>
</page>
ViewModel/ProductJsonLd.phpphp
<?php
declare(strict_types=1);

namespace Vendor\Seo\ViewModel;

use Magento\Catalog\Helper\Image;
use Magento\Catalog\Model\Product;
use Magento\Framework\Pricing\PriceCurrencyInterface;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\Framework\View\Element\Block\ArgumentInterface;

class ProductJsonLd implements ArgumentInterface
{
    public function __construct(
        private readonly Json $json,
        private readonly Image $imageHelper,
        private readonly PriceCurrencyInterface $priceCurrency
    ) {
    }

    public function getJson(Product $product): string
    {
        $price = $product->getPriceInfo()->getPrice('final_price')->getAmount()->getValue();

        return $this->json->serialize([
            '@context' => 'https://schema.org',
            '@type' => 'Product',
            'name' => $product->getName(),
            'sku' => $product->getSku(),
            'url' => $product->getProductUrl(),
            'image' => $this->imageHelper->init($product, 'product_page_image_large')->getUrl(),
            'description' => strip_tags((string) $product->getData('short_description')),
            'offers' => [
                '@type' => 'Offer',
                'price' => number_format((float) $price, 2, '.', ''),
                'priceCurrency' => $this->priceCurrency->getCurrency()->getCode(),
                'availability' => $product->isSalable()
                    ? 'https://schema.org/InStock'
                    : 'https://schema.org/OutOfStock',
                'url' => $product->getProductUrl(),
            ],
        ]);
    }
}
view/frontend/templates/product/json-ld.phtmlphp
<?php
/** @var \Magento\Catalog\Block\Product\View $block */
/** @var \Vendor\Seo\ViewModel\ProductJsonLd $viewModel */
$viewModel = $block->getData('view_model');
?>
<script type="application/ld+json"><?= /* @noEscape */ $viewModel->getJson($block->getProduct()) ?></script>
  • السعر من الـ Pricing framework (final_price) مش getPrice()، عشان يطابق اللي ظاهر للزبون.
  • الـ @noEscape هنا آمن لأن الـ Json serializer بيعمل escape لـ /، فمفيش نص يقدر يقفل الـ </script>.
!
المثال ده لمنتج simple. الـ configurable والـ bundle ليهم أسعار متعدّدة ومحتاجين AggregateOffer بـ lowPrice وhighPrice. والسعر في الـ JSON-LD لازم يطابق الصفحة (العملة والضريبة)، وإلا Google بيتجاهل البيانات.
إيه هو

لو عندك store view عربي وstore view إنجليزي (درس Website و Store و Store View)، كل صفحة لازم تقول لـ Google عن نسخها التانية: <link rel="alternate" hreflang="ar-EG" href="…">. Magento Open Source مابيعملش ده.

الصعب فيه

الـ URL Key ممكن يختلف من store view للتاني، فمينفعش تغيّر اللغة في الرابط وخلاص. لازم تجيب رابط نفس المنتج من جدول الـ rewrites لكل store view.

ViewModel/ProductHreflang.phpphp
public function getAlternates(Product $product): array
{
    $alternates = [];
    $websiteId = (int) $this->storeManager->getStore()->getWebsiteId();

    /** @var \Magento\Store\Model\Store $store */
    foreach ($this->storeManager->getStores() as $store) {
        if ((int) $store->getWebsiteId() !== $websiteId || !$store->isActive()) {
            continue;
        }
        $rewrites = $this->urlFinder->findAllByData([
            UrlRewrite::ENTITY_TYPE => 'product',
            UrlRewrite::ENTITY_ID => (int) $product->getId(),
            UrlRewrite::STORE_ID => (int) $store->getId(),
            UrlRewrite::REDIRECT_TYPE => 0,
        ]);
        foreach ($rewrites as $rewrite) {
            if (!$rewrite->getMetadata()) { // skip URLs that include a category path
                $locale = (string) $this->scopeConfig->getValue(
                    'general/locale/code', ScopeInterface::SCOPE_STORE, $store->getId()
                );
                $alternates[str_replace('_', '-', $locale)] = $store->getBaseUrl() . $rewrite->getRequestPath();
                break;
            }
        }
    }

    return $alternates; // ['ar-EG' => 'https://…', 'en-US' => 'https://…']
}
view/frontend/templates/product/hreflang.phtmlphp
<?php
/** @var \Magento\Catalog\Block\Product\View $block */
/** @var \Magento\Framework\Escaper $escaper */
$viewModel = $block->getData('view_model');
foreach ($viewModel->getAlternates($block->getProduct()) as $lang => $href): ?>
<link rel="alternate" hreflang="<?= $escaper->escapeHtmlAttr($lang) ?>" href="<?= $escaper->escapeUrl($href) ?>">
<?php endforeach; ?>
  • بيتسجّل بنفس طريقة الـ JSON-LD: block في head.additional ومعاه الـ ViewModel. والـ ViewModel بياخد StoreManagerInterface وUrlFinderInterface وScopeConfigInterface في الـ constructor.
  • كل صفحة لازم تشاور على نفسها كمان (الـ loop بيشمل الـ store الحالي)، والنسخ لازم تشاور على بعض في الاتجاهين.
  • ضيف hreflang="x-default" للنسخة اللي تظهر لأي لغة مش عندك.
  • نفس الفكرة للـ categories (entity_type = category). الـ CMS pages مختلفة لأن كل لغة page منفصلة، فمحتاج تربطهم بنفسك.
• • •
◆ Part 3 — الأداء والمراجعة
11

الأداء جزء من الـ SEO

سرعة الصفحة داخلة في تجربة المستخدم اللي Google بيقيسها.

  • Production mode: bin/magento deploy:mode:set production. الـ developer mode بطيء ومش معمول للزوار.
  • Full Page Cache بـ Varnish: من Stores › Settings › Configuration › Advanced › System › Full Page Cache اختار Varnish في الـ Caching Application.
  • الصور: صور صفحات الـ categories والـ widgets فيها loading="lazy" جاهز من 2.4.0، بس الـ gallery في صفحة المنتج لأ. ضغط الصور وحجمها عليك.
الـ minify من الـ CLI

في الـ production mode قسم الـ Developer بيختفي من الأدمن، فبتظبط الإعدادات من الـ CLI:

terminalbash
bin/magento config:set dev/js/minify_files 1
bin/magento config:set dev/css/minify_files 1
bin/magento config:set dev/template/minify_html 1
bin/magento config:set dev/js/move_script_to_bottom 1
bin/magento config:set dev/css/use_css_critical_path 1
bin/magento setup:static-content:deploy
bin/magento cache:flush
الإعداد
ملاحظة
dev/js/merge_files و dev/css/merge_css_files
دمج الملفات. مع HTTP/2 ساعات مابيفيدش، فقيس قبل وبعد.
dev/js/enable_js_bundling
الـ bundling المدمج ممكن يطلع ملفات كبيرة جدًا. متفعّلوش غير بعد ما تقيس.
dev/js/defer_non_critical
موجود من 2.4.9: تأجيل الـ scripts غير الأساسية في الـ head.
i
بعد أي تغيير، قيس بـ PageSpeed Insights على صفحة category وصفحة منتج حقيقيين، مش الصفحة الرئيسية بس.
• • •
12

Checklist وأسئلة الإنترفيو

راجع عليها قبل ما تسلّم أي متجر، وجاوب بيها في الإنترفيو.

  • Use Web Server Rewrites = Yes، ومفيش index.php في أي رابط.
  • المتجر كله HTTPS، وAuto-redirect to Base URL = 301.
  • Use Categories Path for Product URLs = No، والـ canonical مفعّل للمنتجات والـ categories.
  • Meta Title و Meta Description لكل منتج و category مهمين، ولكل store view.
  • Alt Text للصور الأساسية.
  • Default Robots = INDEX, FOLLOW على الـ production، وNOINDEX, NOFOLLOW على الـ staging.
  • مفيش ملف pub/robots.txt بيغطّي على إعدادات الأدمن.
  • XML Sitemap متولّد، والـ cron شغّال، ومضاف لـ robots.txt و Search Console.
  • Redirects 301 لكل الروابط القديمة المهمة، ومفيش سلاسل.
  • noindex لصفحات الفلاتر.
  • الـ structured data بيعدّي Rich Results Test من غير أخطاء.
  • hreflang لو المتجر فيه أكتر من لغة.
  • Production mode + Varnish + minify، ومقاس بـ PageSpeed Insights.
Q1إزاي تشيل index.php من روابط Magento؟
الإجابة: تفعّل Use Web Server Rewrites من General › Web › Search Engine Optimization (web/seo/use_rewrites)، وتتأكد إن السيرفر مظبوط للـ rewrites (.htaccess في Apache أو إعداد nginx)، وبعدها تمسح الكاش.
Q2منتج بيظهر على كذا رابط. تحل الـ duplicate content إزاي؟
الإجابة: Use Categories Path for Product URLs = No عشان الرابط الأساسي يبقى من غير category، وتفعّل Use Canonical Link Meta Tag For Products عشان أي نسخة تشاور على الرابط ده. ولو الإصدار أقدم من 2.4.6، راجع Generate "category/product" URL Rewrites لأن افتراضيه كان Yes.
Q3غيّرت الـ URL Key لمنتج. الرابط القديم بيحصله إيه؟
الإجابة: لو Create Permanent Redirect for URLs if URL Key Changed مفعّل (الافتراضي) والـ checkbox متعلّم في صفحة المنتج، Magento بيضيف rewrite بـ 301 من الرابط القديم للجديد في جدول url_rewrite، فالترتيب والروابط الخارجية مابيضيعوش.
Q4عدّلت robots.txt من الأدمن ومفيش حاجة اتغيّرت. ليه؟
الإجابة: غالبًا فيه ملف pub/robots.txt حقيقي، والـ web server بيقدّمه قبل ما الطلب يوصل لـ Magento. في الحالة العادية robots.txt بيتولّد من Magento_Robots. الحل تمسح الملف وتمسح الكاش.
Q5إزاي تمنع فهرسة صفحات الـ layered navigation؟
الإجابة: مفيش حاجة جاهزة. بعمل after plugin على Magento\Catalog\Controller\Category\View::execute(): لو النتيجة Page والرابط فيه فلاتر، أحط NOINDEX,FOLLOW. بينجح لأن الصفحة بتترسم بعد ما execute تخلص. ومعاه الـ canonical للـ categories.
Q6إزاي تضيف structured data من غير ما تلمس الـ theme؟
الإجابة: layout update في catalog_product_view.xml بيضيف block جوّه head.additional (بـ referenceBlock لأنه block)، والـ JSON بيتبني في ViewModel. قبلها بتأكد إن الـ microdata اللي جاية من الـ core مابتتعارضش معاه، وبختبر في Rich Results Test.
Q7ليه مابتعتمدش على Meta Keywords؟
الإجابة: لأن Google مابيستخدمهاش في ترتيب النتايج. الوقت الأحسن يتصرف على Meta Title و Description مختلفين لكل صفحة، و Alt Text للصور، والـ canonical، والمحتوى نفسه.

الـ SEO في Magento ✓

دلوقتي عارف إيه اللي بيتظبط من الأدمن (الروابط، الـ canonical، الـ meta، robots.txt، الـ sitemap، والـ redirects)، وإيه اللي بيتكتب في الكود (الـ head، الـ noindex للفلاتر، الـ JSON-LD، والـ hreflang).

الخلاصة: رابط واحد لكل صفحة، ومحتوى واضح لكل رابط، والباقي تفاصيل. ظبّط الأساس في أول المشروع، وراجع الـ checklist قبل كل إطلاق.

// magento 2 · seo · admin + code

خلّصت الدرس؟علّمه عشان تتابع تقدّمك في الكورس.

كاتب الدرس

Abdulrahman Masoud

عندك سؤال على الدرس أو محتاج مساعدة في مشروع Magento؟ كلّمني.

أسئلة شائعة

إزاي أشيل index.php من روابط Magento 2؟

فعّل Use Web Server Rewrites من Stores › Settings › Configuration › General › Web › Search Engine Optimization (المسار web/seo/use_rewrites). لازم كمان السيرفر يكون مظبوط للـ rewrites (ملف .htaccess في Apache أو إعداد nginx)، وبعدها امسح الكاش.

إزاي أفعّل الـ canonical tag في Magento 2؟

من Stores › Settings › Configuration › Catalog › Catalog › Search Engine Optimization فعّل Use Canonical Link Meta Tag For Products و Use Canonical Link Meta Tag For Categories، والاتنين مقفولين افتراضيًا. الـ canonical بتاع المنتج بيشاور على رابطه من غير الـ category، وبتاع الـ category على رابطها من غير parameters الفلاتر.

ليه تعديل robots.txt من أدمن Magento مش بيظهر؟

غالبًا فيه ملف pub/robots.txt حقيقي على السيرفر، والـ web server بيقدّمه قبل ما الطلب يوصل لـ Magento فالإعدادات بتتجاهل. امسح الملف عشان Magento يولّد robots.txt من Content › Design › Configuration › Search Engine Robots.

إزاي أعمل XML Sitemap في Magento 2 وأخليه يتحدّث لوحده؟

اعمل الـ sitemap من Marketing › SEO & Search › Site Map وولّده، وبعدين فعّل Generation Settings › Enabled من Stores › Settings › Configuration › Catalog › XML Sitemap. الـ cron sitemap_generate بيعيد توليد الـ sitemaps الموجودة بس، ومفيش أمر bin/magento جاهز للتوليد.

إزاي أمنع Google يفهرس صفحات الفلاتر في Magento 2؟

Magento مفيهوش noindex جاهز لصفحات الـ layered navigation. الحل after plugin على Magento\Catalog\Controller\Category\View::execute() يحط NOINDEX,FOLLOW لما يكون في الرابط فلاتر، مع تفعيل الـ canonical للـ categories.

هل Magento 2 بيطلّع structured data جاهز لصفحة المنتج؟

صفحة المنتج فيها schema.org microdata جاهز (الاسم والـ SKU والسعر والتقييمات) من templates موديولات الـ Catalog والـ Review، وكمان Open Graph tags. لكن مفيش JSON-LD في الـ core، فلو عايزه بتضيفه بـ block جوّه head.additional.

هل Meta Keywords مهمة للـ SEO في Magento؟

Google مابيستخدمش meta keywords في ترتيب النتائج، فمتضيّعش وقت فيها. ركّز على Meta Title و Meta Description مختلفين لكل منتج و category، وعلى Alt Text للصور.