301 redirects vs canonical links in Magento

What can you do to prevent duplicated content of products from different stores?
Recently we’ve received one inquiry to optimize an existing Magento website.
Shortly, there are 2 stores with codes: store1 and store2. Store code is included in the URL.
While we were working on the optimization, the client has reported that some of the products show in both: store1 and store2 but they should be visible only in one of those stores (imagine that you’re on store1 and you see there some related products but from store2).
Additional problem in our case was that there were 2 categories, store1 and store2 (same names for categories as for store codes).
Our ICG (Inchoo Consulting Group) team was on top of the all things that we were doing. When they saw our problem, they instantly noticed what that could mean for client’s SEO!
Shortly, when you’re on one product you could manually change URL from /store1/ to /store2/ and you were able to see the same product, but now in different store (with different theme).
Because search engine could see the same product in store1 and store2 (2 same pages but with different URL – with the same content) it might think that you’re “cheating” and it could position you lower in search results. What search engine could do is to index all products on all stores – duplicated content. To avoid that we needed to “find” proper solution for that.
Basically what we could do here is: a) 301 redirects or b) use canonical URLs. So we needed to ensure for some products from store1 not to be visible in store2.
At the end what we did is that we added 301 redirects for products that should be only visible in one store (if you try somehow to change URL), but if product exists in both stores we added canonical URLs to be on store1 (store1 in our case is more important).
What do you think about this approach for this specific issue?
If you have some similar situation here’s an idea how can you solve particular issue (bellow is sample code how can you solve it). Basically we’ve modified 2 files. One controller (for redirects) and one block (for canonical URLs).
Note: we’ve worked on CE ver.: 1.7.0.1. Don’t change the core files! But, because of simplicity I’ll show you the solution by modifying core files.
Open file app/code/core/Mage/Catalog/controllers/ProductController.php and replace viewAction() with the following content:
public function viewAction()
{
// Get initial data from request
$categoryId = (int) $this->getRequest()->getParam('category', false);
$productId = (int) $this->getRequest()->getParam('id');
$specifyOptions = $this->getRequest()->getParam('options');
/////////////////////////////////////// START WITH 301 REDIRECT
$redirectURL = Mage::getUrl('', array(
'_current' => true,
'_use_rewrite' => true,
));
if ($productId) {
$product = Mage::getModel('catalog/product')->load($productId);
}
$tmpStoreCode = false;
$tmpStoreCode = Mage::app()->getStore()->getCode();
$expCatIdStore= false;
switch ($tmpStoreCode) {
case 'store1':
$expCatIdStore = 1;
break;
case 'store2':
$expCatIdStore = 100;
break;
}
if ($expCatIdStore && $productId) {
$catIds = array();
try {
$catIds = $product->getCategoryIds();
} catch (Exception $e) {
//Mage::log or similar
}
$productIsInStore1 = false;
$productIsInStore2 = false;
// imagine that store code store1 represents category_id=1 and
// store with code store2 represents category_id=100
if (in_array(1, $catIds)) {
$productIsInStore1 = true;
}
if (in_array(100, $catIds)) {
$productIsInStore2 = true;
}
// if product should be in both stores don't do any redirect
if ( !($productIsInStore1 && $productIsInStore2) ) {
if ($productIsInStore2 && $tmpStoreCode === 'store1') {
$redirectURL = str_replace('example.com/store1/', 'example.com/store2/', $redirectURL);
header ('HTTP/1.1 301 Moved Permanently');
header ('Location: ' . $redirectURL);
exit;
} elseif ($productIsInStore1 && $tmpStoreCode === 'store2') {
$redirectURL = str_replace('example.com/store2/', 'example.com/store1/', $redirectURL);
header ('HTTP/1.1 301 Moved Permanently');
header ('Location: ' . $redirectURL);
exit;
}
}
}
/////////////////////////////////////// END WITH 301 REDIRECT
// Prepare helper and params
$viewHelper = Mage::helper('catalog/product_view');
$params = new Varien_Object();
$params->setCategoryId($categoryId);
$params->setSpecifyOptions($specifyOptions);
// Render page
try {
$viewHelper->prepareAndRender($productId, $this, $params);
} catch (Exception $e) {
if ($e->getCode() == $viewHelper->ERR_NO_PRODUCT_LOADED) {
if (isset($_GET['store']) && !$this->getResponse()->isRedirect()) {
$this->_redirect('');
} elseif (!$this->getResponse()->isRedirect()) {
$this->_forward('noRoute');
}
} else {
Mage::logException($e);
$this->_forward('noRoute');
}
}
}
Additionally, to use canonical URLs for products that should be in both categories but, perhaps, show to search engine that it’s in only one (for example store1) category change _prepareLayout() method with the following content
Open file app/code/core/Mage/Catalog/Block/Product/View.php
protected function _prepareLayout()
{
$this->getLayout()->createBlock('catalog/breadcrumbs');
$headBlock = $this->getLayout()->getBlock('head');
if ($headBlock) {
$product = $this->getProduct();
$title = $product->getMetaTitle();
if ($title) {
$headBlock->setTitle($title);
}
$keyword = $product->getMetaKeyword();
$currentCategory = Mage::registry('current_category');
if ($keyword) {
$headBlock->setKeywords($keyword);
} elseif($currentCategory) {
$headBlock->setKeywords($product->getName());
}
$description = $product->getMetaDescription();
if ($description) {
$headBlock->setDescription( ($description) );
} else {
$headBlock->setDescription(Mage::helper('core/string')->substr($product->getDescription(), 0, 255));
}
if ($this->helper('catalog/product')->canUseCanonicalTag()) {
$params = array('_ignore_category'=>true);
/////////////////////////////////////// START WITH CANONICAL
$cannURL = $product->getUrlModel()->getUrl($product, $params);
$productId = (int) $this->getRequest()->getParam('id');
$tmpStoreCode = false;
$tmpStoreCode = Mage::app()->getStore()->getCode();
$expCatIdStore = false;
switch ($tmpStoreCode) {
case 'store1':
$expCatIdStore = 1;
break;
case 'store2':
$expCatIdStore = 100;
break;
}
if ($expCatIdStore && $productId) {
$catIds = array();
try {
$catIds = $product->getCategoryIds();
} catch (Exception $e) {
//die silently
}
$productIsInBoth = false;
if ( in_array(1, $catIds) && in_array(100, $catIds) ) {
$productIsInBoth = true;
}
if ($productIsInBoth && $tmpStoreCode === 'store2') {
$cannURL = str_replace('example.com/store2/', 'example.com/store1/', $cannURL);
}
}
$headBlock->addLinkRel('canonical', $cannURL);
//////////////////////////////////// END WITH CANONICAL
//$headBlock->addLinkRel('canonical', $product->getUrlModel()->getUrl($product, $params));
}
}
return parent::_prepareLayout();
}
And that’s about it.
5 comments
Hello !
in fact, to know if a product has been “translated” in English, I check if the short_description of the current store view is the same than the short_description of the admin store view. If there are equal, the short description (so, all the product) has not been translated.
If the product has not been translated the canonical url will content the FR main-store-view.
All seems to work fine, thanks.
Very interesting post !
I was looking for solutions about duplicate content for a multi-store-view and multi-language web site and you put me on the track.
For each language I have a main store-view and some specialized other store-views. Adding categories to url, some products could be in several categories in each store view but this limited problem of duplicate content between categories is solved by the canonical native Magento functionality.
But when a product is in a specialized store-view it’s also in the main store-view and your solution could be a part of the answer… if there was no language issue !
A complementary approach could be testing the store-view and if it’s a specialized FR store-view canonical must content FR main-store-view and for English the EN main-store-view (nothing to do for main-store view).
But some time, the store’s admin adds some products without time to translate it. So you could find some duplicate contents (not translated yet) between main-store-views FR and EN.
I think this could be treated using the back-office information “Use Default Value” for “Short Description” for example. If I find this information to “Yes”, the canonical URL will have to content the FR main-store-view (because the default language of the shop is French), if the information is “No” the canonical URL will have to content the main-store-view EN.
I’ve just to find how to test “Use Default Value”…
For some purposes, John’s solution could be softer and more elegant.
In some cases it may be best to actual have different product name, description etc for the product in the different stores, then both pages are accessible but you remove the duplication issue as the content is different and you have a chance at both pages appearing in the SERPs.
We have gone down the route of canonicalising to a single store though in cases where different descriptions are not viable. In this case though we have also added a product attribute to determine the “primary” store for a product and then the canonical for the product page points to the website that is set as the primary store for that product (your example seems to always canonicalise to store1 but in some cases store2 may be better)
Hi Edwin.
I’m talking here about Stores, not Store Views. I wrote this post before several months and I was looking for a way how to modify it so it can be easier for read…
I believe that other developers have the same/similar issue and I wanted to provide you our solution for particular issue.
Thank you on your tip!
First off: I’ve encountered this problem before. I’m not sure if this is the same thing, but it had to do with storeviews. Every product in every store view could be reached via /product. We solved this by adding a cross-domain canonical. Mainly because some products had to be reached in more then one store.
I’m interest if you are talking about stores or about store views in this case.