SEOمن الأدمن للكود.
متجر سريع ومنتجات حلوة مش كفاية لو Google مش لاقيك. هنشوف Magento بيدّيك إيه جاهز للـ SEO من لوحة الأدمن، وإيه اللي لازم تكتبه بنفسك في الكود: الروابط، الـ canonical، الـ meta، الـ robots.txt، الـ sitemap، والـ structured data.
الـ SEO في Magento 2 بيتظبط من الأدمن ومن الكود. من الأدمن: روابط من غير index.php، و canonical tags، و meta لكل منتج و category، و robots.txt، و XML sitemap، و redirects 301. ومن الكود: noindex لصفحات الفلاتر بـ plugin، و JSON-LD و hreflang، لأن Magento Open Source مابيعملهمش لوحده.
الصورة الكاملة
قبل ما تلمس أي إعداد: إيه اللي Magento بيعمله لوحده، وإيه اللي محتاج شغل.
الروابط (URLs)
روابط نظيفة وثابتة، ولكل صفحة رابط واحد بس.
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
الـ Canonical Tags
لما نفس المحتوى يظهر على كذا رابط، قول لـ Google أنهي فيهم الأصل.
tag في الـ <head> شكله <link rel="canonical" href="…">. بيقول لمحرّك البحث: "الصفحة دي نسخة، والأصل هو الرابط ده". في متجر Magento النسخ بتيجي من الفلاتر، والترتيب، ورابط المنتج اللي فيه اسم الـ category.
- 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 لنفسها بدل ما تشاور على الصفحة الأولى.
// _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']] ); }
bin/magento config:set catalog/seo/category_canonical_tag 1 bin/magento config:set catalog/seo/product_canonical_tag 1 bin/magento cache:flush
الـ Meta: افتراضي ولكل صفحة
العنوان والوصف اللي بيظهروا في نتيجة البحث.
لكل website أو store view بتحط قيم افتراضية، بتتستخدم لما الصفحة ملهاش قيمة خاصة بيها.
- 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.
بتملا حقول الـ meta تلقائيًا من بيانات المنتج. تقدر تغيّرها لحاجة زي {{name}} | اسم المتجر.
robots.txt و Default Robots
إيه اللي محرّكات البحث تزحف عليه، وإيه اللي يتفهرس.
مفيش ملف حقيقي. موديول Magento_Robots عنده router بيلقط الطلب على /robots.txt ويولّد المحتوى من الإعداد ده (بيتقرا على مستوى الـ website). ولو فعّلت إضافة الـ sitemap (القسم الجاي)، Magento_Sitemap بيضيف سطور Sitemap: تحته.
User-agent: * Disallow: /checkout/ Disallow: /customer/ Disallow: /catalogsearch/ Disallow: /wishlist/
- متقفلش ملفات الـ CSS والـ JS والصور في robots.txt — Google محتاجهم عشان يشوف الصفحة زي المستخدم.
- robots.txt بيمنع الزحف مش الفهرسة. لو عايز تشيل صفحة من النتايج استخدم noindex، ومتقفلهاش في robots.txt، وإلا Google مش هيشوف الـ noindex أصلًا.
- على الـ staging: خلّي Default Robots NOINDEX, NOFOLLOW — وبرضه احميه بباسورد.
الـ XML Sitemap
خريطة بكل روابطك المهمة، بتتحدّث لوحدها بالـ cron.
ضيف sitemap جديد: اسم الملف (زي sitemap.xml)، المسار اللي هيتحفظ فيه، والـ store view. احفظه وولّده. لو عندك كذا store view، اعمل sitemap لكل واحد.
URL Rewrites والـ Redirects
لما رابط يتغيّر أو تنقل من منصة قديمة، من غير ما تخسر ترتيبك.
أول اختيار Create URL Rewrite: Custom، أو For Category، أو For Product، أو For CMS page. بعدها:
- التلقائي: لما تغيّر الـ URL Key لمنتج أو category وخيار الـ permanent redirect متعلّم، Magento بيضيف الـ 301 لوحده.
- بعد migration: اعمل ملف بالروابط القديمة والجديدة، وكل رابط قديم له traffic أو backlinks ياخد 301 مباشر لأقرب صفحة جديدة.
- تجنّب السلاسل: A → B → C بتضيّع وقت الزحف. خلّي A يروح لـ C على طول.
الـ head من Layout XML و PHP
للصفحات اللي إنت عاملها في موديولك، أو لما القيمة محتاجة تتحسب.
صفحة custom في موديولك (زي صفحة البراندات) والعنوان والوصف ثابتين. الـ <head> في الـ layout بيقبل title وmeta وlink وcss وscript وremove وattribute.
<?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>
Magento\Framework\View\Page\Config هو اللي ماسك الـ head. بتوصله من الـ result page في الـ controller، أو من $this->pageConfig جوّه أي block بيورّث من Template (في _prepareLayout()، زي ما الـ core بيعمل الـ canonical بتاع الـ category).
<?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; } }
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 مايفهرسش الصفحة، بس يكمّل ماشي في روابط المنتجات اللي فيها.
<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>
<?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.
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.
template صغير جوّه head.additional. خلّي بالك: head.additional block مش container، فبنستخدم referenceBlock. والبيانات بتتجهّز في ViewModel عشان الـ template يفضل نظيف.
<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>
<?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(), ], ]); } }
<?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>.
لو عندك 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.
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://…'] }
<?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 منفصلة، فمحتاج تربطهم بنفسك.
الأداء جزء من الـ 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 في صفحة المنتج لأ. ضغط الصور وحجمها عليك.
في الـ production mode قسم الـ Developer بيختفي من الأدمن، فبتظبط الإعدادات من الـ CLI:
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
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.
الـ SEO في Magento ✓
دلوقتي عارف إيه اللي بيتظبط من الأدمن (الروابط، الـ canonical، الـ meta، robots.txt، الـ sitemap، والـ redirects)، وإيه اللي بيتكتب في الكود (الـ head، الـ noindex للفلاتر، الـ JSON-LD، والـ hreflang).
الخلاصة: رابط واحد لكل صفحة، ومحتوى واضح لكل رابط، والباقي تفاصيل. ظبّط الأساس في أول المشروع، وراجع الـ checklist قبل كل إطلاق.
خلّصت الدرس؟علّمه عشان تتابع تقدّمك في الكورس.
أسئلة شائعة
إزاي أشيل 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 للصور.