Pagination with rel=”next” and rel=”prev” in Magento 2

Out of the box, Magento 2 offers fair amount of search engine optimization options but when it comes to category pages, we only have option to add canonical meta tags. In today’s blog post, we will try to spice that up a bit by implementing our own variation of canonical tag and by adding rel=”next” and rel=”prev” tags to help bots with paginated content.

There are several ways to implement said functionality but the first step will most likely involve creating new Magento 2 module. I decided to implement our logic using event observer approach, so the first thing we do is configure our module to watch certain events. We do this by creating events.xml file in our modules etc/frontend folder followed by creating our observer class.

<?xml version="1.0"?>
 
<config xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance" xsi_noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="layout_generate_blocks_after">
        <observer name="categorySeo" instance="InchooCategorySeoObserverCategory" />
    </event>
</config>

As you can see from the XML above, we are observing layout_generate_blocks_after event. Since this event is not exclusive to category pages, first thing we do in our observer execute method is check if we are indeed on category view page. This is trivial since we already have full action name included in event object data.

if ('catalog_category_view' != $observer->getEvent()->getFullActionName()) {
    return $this;
}

 Replacing default canonical tag

We do this because in default implementation, canonical tag always points to root category URL, regardless of the page or filter combination applied.

To remove default canonical tag we will need category model instance. You could use registry (current_category) to retrieve it. In this example I used layout object instead since it’s also a part of the event object data.

/** @var MagentoCatalogBlockProductListProduct $productListBlock */
$productListBlock = $observer->getEvent()->getLayout()->getBlock('category.products.list');
$category = $productListBlock->getLayer()->getCurrentCategory();
 
/**
 * Remove default canonical tag
 */
if ($this->categoryHelper->canUseCanonicalTag()) {
    $this->pageConfig->getAssetCollection()->remove($category->getUrl());
}

We can now add our own canonical tag. To do that we will use pager block instance (MagentoThemeBlockHtmlPager) to generate our canonical URL and page config (MagentoFrameworkViewPageConfig) to insert it.

/** @var MagentoCatalogBlockProductProductListToolbar $toolbarBlock */
$toolbarBlock = $productListBlock->getToolbarBlock();
/** @var MagentoThemeBlockHtmlPager $pagerBlock */
$pagerBlock = $toolbarBlock->getChildBlock('product_list_toolbar_pager');
$pagerBlock->setAvailableLimit($toolbarBlock->getAvailableLimit())
    ->setCollection($productListBlock->getLayer()->getProductCollection());
 
/**
 * Add rel canonical with page var
 */
$this->pageConfig->addRemotePageAsset(
    $this->getPageUrl([
        $pagerBlock->getPageVarName() => $pagerBlock->getCurrentPage()
    ]),
    'canonical',
    ['attributes' => ['rel' => 'canonical']]
);

You might notice we are using our own getPageUrl method. It’s a slightly customized version of the method which can be found in MagentoThemeBlockHtmlPager class.

/**
* Retrieve page URL by defined parameters
*
* @param array $params
* @return string
*/
protected function getPageUrl($params = [])
{
    $urlParams = [];
    $urlParams['_current'] = false;
    $urlParams['_escape'] = true;
    $urlParams['_use_rewrite'] = true;
    $urlParams['_query'] = $params;
 
    return $this->urlBuilder->getUrl('*/*/*', $urlParams);
}

 Adding rel=”prev” and rel=”next”

Final step involves adding rel=”prev” and rel=”next” to indicate paginated content. By doing this we are informing search engine bots to treat these pages as a logical sequence, thus consolidating their linking properties and usually sending searchers to the first page.

/**
 * Add rel prev and rel next
 */
if (1 < $pagerBlock->getCurrentPage()) {
    $this->pageConfig->addRemotePageAsset(
        $this->getPageUrl([
            $pagerBlock->getPageVarName() => $pagerBlock->getCollection()->getCurPage(-1)
        ]),
        'link_rel',
        ['attributes' => ['rel' => 'prev']]
    );
}
if ($pagerBlock->getCurrentPage() < $pagerBlock->getLastPageNum()) {
    $this->pageConfig->addRemotePageAsset(
        $this->getPageUrl([
            $pagerBlock->getPageVarName() => $pagerBlock->getCollection()->getCurPage(+1)
        ]),
        'link_rel',
        ['attributes' => ['rel' => 'next']]
    );
}

Final thoughts

And that’s it! You should now have a working code which adds canonical, rel=”prev” and rel=”next” meta tags. While this is not a complete SEO solution, I like to think it’s a nice improvement of default functionality. Obviously there is a lot of room for improvement, for example we are not even considering product_list_limit and product_list_ordervariables or layered navigation filters, but that’s a story for another time.