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

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="Inchoo\CategorySeo\Observer\Category" />
    </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 \Magento\Catalog\Block\Product\ListProduct $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 (\Magento\Theme\Block\Html\Pager) to generate our canonical URL and page config (\Magento\Framework\View\Page\Config) to insert it.

/** @var \Magento\Catalog\Block\Product\ProductList\Toolbar $toolbarBlock */
$toolbarBlock = $productListBlock->getToolbarBlock();
/** @var \Magento\Theme\Block\Html\Pager $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 \Magento\Theme\Block\Html\Pager 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.

Related Inchoo Services

You made it all the way down here so you must have enjoyed this post! You may also like:

Editing robots.txt in Magento 2 Admin Toni Anicic
Toni Anicic, | 2

Editing robots.txt in Magento 2 Admin

Build your 2017 Magento SEO strategy with these tips Ivona Namjesnik
Ivona Namjesnik, | 10

Build your 2017 Magento SEO strategy with these tips

Common Magento 2 SEO mistakes Toni Anicic
Toni Anicic, | 6

Common Magento 2 SEO mistakes

7 comments

  1. Replacing line
    $pagerBlock->setAvailableLimit($toolbarBlock->getAvailableLimit())
    ->setCollection($productListBlock->getLayer()->getProductCollection());

    by
    $pagerBlock->setAvailableLimit($toolbarBlock->getAvailableLimit())
    ->setCollection($category->getProductCollection());

    in magento 2.4.3 to keep ordering category working.

  2. In magento 2.4.3, this line is not working while using ordering by select toolbar

    ->setCollection($productListBlock->getLayer()->getProductCollection());

    categories url with ?product_list_order=price are same content as default ordering (position) and products are not sorted.
    How to fix this ?

  3. Hi guys,

    I noticed a possible bug, when you change the default limit it doesn’t work as expected. It still counts the last page number with the default limit, this can be solved by adding

    ->setLimit($toolbarBlock -> getLimit())

    before setting the collection to the pagerblock, like this:

    $pagerBlock -> setAvailableLimit($toolbarBlock -> getAvailableLimit())->setLimit($toolbarBlock -> getLimit()) -> setCollection($productListBlock -> getLayer() -> getProductCollection());

    .

  4. I tried your solution but it displays no products except on the first page. And on the first page, a list of products displayed but they’re the last page products. The $pagerBlock shows the correct current page number, but the products displayed were not accurate.

  5. Hi

    i try to implement your solution, but i have some problem with the construct. i have error with params
    $this->categoryHelper and $this->pageConfig which are not defined in your exemple.

    Could you help me please ?

  6. Is there any way to simply inject a rel canonical into the head, or override the canonical tag? Magento 1 allowed you to do this very simply with a xml layout update – but I haven’t been able to find an equivalent solution for Magento 2.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <blockquote cite=""> <code> <del datetime=""> <em> <s> <strike> <strong>. You may use following syntax for source code: <pre><code>$current = "Inchoo";</code></pre>.

Tell us about your project

Drop us a line. We'd love to know more about your project.