Magento 2 Product list aggregate rating fix

During our work with Magento 2 Rich Snippets module we’ve noticed that the product list page has numerous errors if switched to the list mode. The errors were generated by the standard Magento 2 Luma theme, when attempting to aggregate rating markup rendering for each product in the product list. This blog post describes a configurable fix for the issue. We will have an opportunity to disable this fix for any custom theme which doesn’t have this error or in case the next Magento 2 version has a fix for this included.

Aggregate Rating Errors in Magento2 product list mode

Let’s start from the reason of the issue. Aggregate rating markup should be located inside the product scope tag of the Schema.org markup:

<div itemscope="" itemtype="http://schema.org/Product">
	...................
</div>

It is so on the product view page, but the product list page is missing it.

If we try to debug, where the ratings come from, we will find that it is generated by Magento\Review\Block\Product\ReviewRenderer::getReviewsSummaryHtml() method. So, we need to plug in this method and wrap the html generated as the method output with a tag, that is specified above. We will also need to do so for product list page only and make sure that we will be able to easily disable our fix for the custom theme or after Magento update.

Let’s start from the extension declaration. You may skip this part and add the system configuration and Magento 2 plugin to an existing extension. First of all we will need a module.xml :

<?xml version="1.0"?>
<!-- 
	Location: magento2_root/app/code/Vendorname/Extensionname/etc/module.xml
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Vendorname_Extensionname" setup_version="0.0.1">
    </module>
</config>

We will also need a registration.php:

<?php
// Location: magento2_root/app/code/Vendorname/Extensionname/registration.php

\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Vendorname_Extensionname',
    __DIR__
);

The last but not least is magento2_root/app/code/Vendorname/Extensionname/composer.json:

{
  "name": "vendorname/extensionname",
  "description": "N/A",
  "require": {
    "php": "~5.5.0|~5.6.0|~7.0.0"
  },
  "type": "magento2-module",
  "version": "0.0.1-beta",
  "license": [
    "OSL-3.0",
    "AFL-3.0"
  ],
  "autoload": {
    "files": [ "registration.php" ],
    "psr-4": {
      "Vendorname\\Extensionname\\": ""
    }
  }
}

We’re set up. Now, let’s add a configuration, which allows us to easily enable or disable our fix:

<?xml version="1.0"?>
<!--
/**
 * Location: magento2_root/app/code/Vendorname/Extensionname/etc/adminhtml/system.xml
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <tab id="example_tab" translate="label" sortOrder="1000">
            <label>Example tab config</label>
        </tab>
        <section id="example_section" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1">
            <label>Example config section</label>
            <tab>example_tab</tab>
            <resource>Vendorname_Extensionname::config</resource>
            <group id="category" translate="label" type="text" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Category Page Product Reviews Fix</label>
                <field id="review_fix_enabled" translate="label" type="select" sortOrder="120" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Enable Category Page Product Reviews Fix</label>
                    <source_model>Magento\Config\Model\Config\Source\Enabledisable</source_model>
                    <comment><![CDATA[Wrap native product review summary with target product markup, which is missing in native functionality.]]></comment>
                </field>
            </group>
        </section>
    </system>
</config>

It is useful to have a default value for our configuration:

<?xml version="1.0"?>
<!--
/**
 * Location: magento2_root/app/code/Vendorname/Extensionname/etc/config.xml
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <example_section>
            <category>
                <review_fix_enabled>1</review_fix_enabled>
            </category>
        </example_section>
    </default>
</config>

Let’s add our Magento 2 plugin to di.xml :

<?xml version="1.0"?>
<!--
/**
 * Location: magento2_root/app/code/Vendorname/Extensionname/etc/di.xml
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Review\Block\Product\ReviewRenderer">
        <plugin name="vendornma-extensionname-product-list-rating-wrapper"
                type="Vendorname\Extensionname\Plugin\Block\Product\ReviewRenderer" sortOrder="1000"/>
    </type>
</config>

We will also need a helper for the config value extraction:

<?php
// Location: magento2_root/app/code/Vendorname/Extensionname/Helper/Data.php

namespace Vendorname\Extensionname\Helper;
use Magento\Framework\App\Helper\Context;

class Data extends \Magento\Framework\App\Helper\AbstractHelper
{
    /**
     * Returns system configuration value
     *
     * @param $key
     * @param null $store
     * @return mixed
     */
    public function getConfigurationValue($key, $store = null)
    {
        return $this->scopeConfig->getValue(
            'example_section/' . $key,
            \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
            $store
        );
    }
}

Now, we are ready to add a plugin file. We will plugin after the main method run and will wrap its output with a proper markup if the route is correct and our fix is enabled in the config:

<?php
// Location: magento2_root/app/code/Vendorname/Extensionname/Plugin/Block/Produc/ReviewRenderer.php

namespace Vendorname\Extensionname\Plugin\Block\Product;

use Vendorname\Extensionname\Helper\Data as Helper;
use Magento\Review\Block\Product\ReviewRenderer as SubjectBlock;

class ReviewRenderer
{
    /**
     * @var \Vendorname\Extensionname\Helper\Data
     */
    protected $helper;

    public function __construct(
        Helper $helper
    )
    {
        $this->helper = $helper;
    }

    public function afterGetReviewsSummaryHtml(SubjectBlock $subject, $result = '')
    {
        if ($result != ''
            && !is_null($subject->getRequest())
            && $subject->getRequest()->getFullActionName() == 'catalog_category_view'
            && $this->helper->getConfigurationValue('category/review_fix_enabled') == 1
            && $product = $subject->getProduct()) {
            $result = '<div itemscope itemtype="http://schema.org/Product"><div itemprop="name" content="'
                . htmlspecialchars($product->getName()) . '"></div>' . $result . '</div>';
        }

        return $result;
    }
}

Note, that we are also adding a product name to our markup.

That’s all what we need. We hope, that this blog post will spare you some time and help you improve your Magento 2 SEO rates. We will include this fix in our next release of the Rich Snippets extension. Thanks for reading!