2

How would you override the product canonical url in Magento 2?

Scenario: We have a group products which cannot be configurable's (client request) so we've created 4 simples and connected them via product relations. Each of the 4 products are related to each other. Our problem is each of the 4 products have the same product information except for pricing & url which is bad for SEO.

Our proposed solution would be to change the canonical URL for 3 of the products to point to the 4th product which could be, for example, the cheapest product.

2

So I've gone in a different direction to solve my problem and this is what I come up with.

Create a custom product attribute called 'custom_canonical_url' and Input Validation for Store Owner set to 'URL'.

Add the following to Vendor/Module/etc/di.xml to extend the Magento\Catalog\Helper\Product\View class.

<preference for="Magento\Catalog\Helper\Product\View" 
type="Vendor\Module\Helper\Product\View" />

In your module.xml add a sequence node for Magento_Catalog

<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Vendor_Module" setup_version="1.0.0">
        <sequence>
            <module name="Magento_Catalog"/>
        </sequence>
    </module>
</config>

Create View.php in Vendor/Module/Helper/Product e.g. Vendor/Module/Helper/Product/View.php

<?php
namespace Vendor\Module\Helper\Product;

use Magento\Catalog\Helper\Product\View as ProductView;
use Magento\Framework\View\Result\Page as ResultPage;

class View extends ProductView
{

    // Custom private function to amend the canonical URL.
    // You cannot override a private function so you need to create a custom private function and change the reference where it is declared.

    private function preparePageMetadataCustom(ResultPage $resultPage, $product){
        $pageConfig = $resultPage->getConfig();

        $metaTitle = $product->getMetaTitle();
        $pageConfig->setMetaTitle($metaTitle);
        $pageConfig->getTitle()->set($metaTitle ?: $product->getName());

        $keyword = $product->getMetaKeyword();
        $currentCategory = $this->_coreRegistry->registry('current_category');
        if ($keyword) {
            $pageConfig->setKeywords($keyword);
        } elseif ($currentCategory) {
            $pageConfig->setKeywords($product->getName());
        }

        $description = $product->getMetaDescription();
        if ($description) {
            $pageConfig->setDescription($description);
        } else {
            $pageConfig->setDescription($this->string->substr(strip_tags($product->getDescription()), 0, 255));
        }

        if ($this->_catalogProduct->canUseCanonicalTag()) {

        /* Custom Rewrite */
            $custom_canonical_url       = $product->getResource()->getAttribute('custom_canonical_url');
            $custom_canonical_url_value = $custom_canonical_url->getFrontend()->getValue($product);

            if ($custom_canonical_url) {
                $canonicalUrl = $custom_canonical_url;
            }else{
                $canonicalUrl = $product->getUrlModel()->getUrl($product, ['_ignore_category' => true]);
            }
            $pageConfig->addRemotePageAsset(
                $canonicalUrl,
                'canonical',
                ['attributes' => ['rel' => 'canonical']]
            );
            // $pageConfig->addRemotePageAsset(
            //     $product->getUrlModel()->getUrl($product, ['_ignore_category' => true]),
            //     'canonical',
            //     ['attributes' => ['rel' => 'canonical']]
            // );
        /* Custom Rewrite */

        }

        $pageMainTitle = $resultPage->getLayout()->getBlock('page.main.title');
        if ($pageMainTitle) {
            $pageMainTitle->setPageTitle($product->getName());
        }

        return $this;
    }


    public function prepareAndRender(ResultPage $resultPage, $productId, $controller, $params = null)
    {
        /**
         * Remove default action handle from layout update to avoid its usage during processing of another action,
         * It is possible that forwarding to another action occurs, e.g. to 'noroute'.
         * Default action handle is restored just before the end of current method.
         */
        $defaultActionHandle = $resultPage->getDefaultLayoutHandle();
        $handles = $resultPage->getLayout()->getUpdate()->getHandles();
        if (in_array($defaultActionHandle, $handles)) {
            $resultPage->getLayout()->getUpdate()->removeHandle($resultPage->getDefaultLayoutHandle());
        }

        if (!$controller instanceof \Magento\Catalog\Controller\Product\View\ViewInterface) {
            throw new \Magento\Framework\Exception\LocalizedException(
                __('Bad controller interface for showing product')
            );
        }
        // Prepare data
        $productHelper = $this->_catalogProduct;
        if (!$params) {
            $params = new \Magento\Framework\DataObject();
        }

        // Standard algorithm to prepare and render product view page
        $product = $productHelper->initProduct($productId, $controller, $params);
        if (!$product) {
            throw new \Magento\Framework\Exception\NoSuchEntityException(__('Product is not loaded'));
        }

        $buyRequest = $params->getBuyRequest();
        if ($buyRequest) {
            $productHelper->prepareProductOptions($product, $buyRequest);
        }

        if ($params->hasConfigureMode()) {
            $product->setConfigureMode($params->getConfigureMode());
        }

        $this->_eventManager->dispatch('catalog_controller_product_view', ['product' => $product]);

        $this->_catalogSession->setLastViewedProductId($product->getId());

        if (in_array($defaultActionHandle, $handles)) {
            $resultPage->addDefaultHandle();
        }

        $this->initProductLayout($resultPage, $product, $params);
    /* Custom Rewrite */
        //$this->preparePageMetadata($resultPage, $product);
        $this->preparePageMetadataCustom($resultPage, $product);
    /* Custom Rewrite */
        return $this;
    }


}
?>

As you can see there is a new private function called preparePageMetadataCustom, this is a duplicate of preparePageMetadata as you cannot override a private function.

View.php Edits explained

To use the custom_canonical_url I modified the the following

$pageConfig->addRemotePageAsset(
    $product->getUrlModel()->getUrl($product, ['_ignore_category' => true]),
    'canonical',
    ['attributes' => ['rel' => 'canonical']]
);

to

/* Custom Rewrite */
$custom_canonical_url       = $product->getResource()->getAttribute('custom_canonical_url');
$custom_canonical_url_value = $custom_canonical_url->getFrontend()->getValue($product);

if ($custom_canonical_url) {
    $canonicalUrl = $custom_canonical_url;
}else{
    $canonicalUrl = $product->getUrlModel()->getUrl($product, ['_ignore_category' => true]);
}
$pageConfig->addRemotePageAsset(
    $canonicalUrl,
    'canonical',
    ['attributes' => ['rel' => 'canonical']]
);
/* Custom Rewrite */

When a product is loaded it will check if there is a custom URL added to custom_canonical_url. If there is, it will use it. If not, it will fall back to the original canonical URL.

To use the new private function, I changed the following line in the prepareAndRender function

$this->preparePageMetadata($resultPage, $product);

to our new function

$this->preparePageMetadataCustom($resultPage, $product);

I hope this is helpful to anyone else looking to modify the canonical URL.

Also, stackexchanger's if I havent posted correctly I apologise.. it's my first post of this kind :)

0

I was trying to do the exactly same thing but I did it with another approach with Plugin.

as you can see prepareAndRender method is public and it's calling the private method preparePageMetadata

There is no need to override the private method while we can create an after plugin for prepareAndRender and just modify the $resultPage config assets.

In your module add di.xml file with the following content:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Catalog\Helper\Product\View">
        <plugin name="vendor_module_product_view_meta"
                type="Vendor\Modulename\Plugin\Catalog\Helper\Product\ViewPlugin" />
    </type>
</config>

Then create class Vendor\Modulename\Plugin\Catalog\Helper\Product\ViewPlugin

<?php

use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\View\Result\Page as ResultPage;

class ViewPlugin
{
    public function __construct(
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        \Magento\Catalog\Api\ProductRepositoryInterface $productRepository
    ) {
        $this->productRepository = $productRepository;
        $this->storeManager = $storeManager;
    }

    public function afterPrepareAndRender($subject, $result, ResultPage $resultPage, $productId, $controller, $params = null)
    {
        try {
            $product = $this->productRepository->getById($productId, false, $this->storeManager->getStore()->getId());
            /** @var \Magento\Framework\View\Page\Config $pageConfig */
            $pageConfig = $resultPage->getConfig();
            $assets = $pageConfig->getAssetCollection()->getGroups();

            foreach ($assets as $asset) {
                if($asset->getProperty('content_type') == 'canonical') {
                    $url = $product->getUrlModel()->getUrl($product, ['_ignore_category' => true]);
                    $pageConfig->getAssetCollection()->remove($url);
                    $newUrl = $product->getData('your_custom_attribute');
                    $pageConfig->addRemotePageAsset(
                        $newUrl,
                        'canonical',
                        ['attributes' => ['rel' => 'canonical']]
                    );
                }
            }
        } catch (NoSuchEntityException $e) {
            return false;
        }
    }
}

Finally you just need to replace the $newUrl value and the custom attribute code to work.

This is much cleaner way of doing this and it also provide security that your code will work even if Magento is upgraded to newer versions.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.