How to Solve the Duplicate Content Issue in OpenCart? (Updated)
Imagine this - you are the administrator of the imaginary OpenCart store http://myshinystore.com and you have just set up your store to use SEO URLs.
As a result your category links look something like this:
http://myshinystore.com/parent_category/child_category (instead of http://myshinystore.com/index.php?route=product/category&path=11_3)
Similarly, the product links look something like this:
http://myshinystore.com/product_link (instead of http://myshinystore.com/index.php?route=product/product&product_id=7)
Looks great, right? No more ugly long URLs - only meaningful paths from now on.
So what?
Well, there is one small thing which could impact your website ranking in search engines like Google or Bing. Take for example the category link above. The same page can be accessed from:
http://myshinystore.com/child_category (instead of http://myshinystore.com/parent_category/child_category)
This is a problem because even though both links lead to absolutely the same content, the search engine will regard them as different pages on your website - the so-called “Duplicate content” issue. More information about it here:
https://support.google.com/webmasters/answer/66359?hl=en
So, how do we solve this?
Since there is no setting in OpenCart to resolve this, we will need to get our hands dirty and modify a bit of code in your store. The modification will be made as an OCMOD extension to avoid changes to your core files.
Note: Please keep in mind that the changes we are about to make might cause conflicts with other third-party extensions on your store. If this happens, feel free to disable the modifications in order to return to the previous behavior.
Note: Also keep in mind that these modifications are developed for OpenCart 2.0.0.0 - 2.3.0.2. They may not be compatible with newer versions until we update this blog post.
Step 1 - Prepare the file.
Without further ado, let’s begin! Using your favorite text editor, create a new file called duplicate_url_fix.ocmod.xml.
There are several ways in which the duplicate issue can be resolved. Use only one of the approaches below, depending on your preference.
Step 2, Approach 1 - Modify the store to use only the short versions of the links.
This will make OpenCart convert all of the SEO links to only a single word (without any paths). So as a result all links to child categories and products will look like this:
http://myshinystore.com/child_category (instead of http://myshinystore.com/parent_category/child_category)
Add the following contents to the newly created file duplicate_url_fix.ocmod.xml:
<?xml version="1.0" encoding="UTF-8"?>
<modification>
<name><![CDATA[Duplicate Content Fix]]></name>
<code><![CDATA[duplicate_content_fix]]></code>
<version><![CDATA[1.0]]></version>
<author><![CDATA[iSenseLabs]]></author>
<link><![CDATA[https://isenselabs.com]]></link>
<file path="catalog/controller/*/seo_url.php">
<operation>
<search><![CDATA[parse_str($url_info['query'], $data);]]></search>
<add position="after"><![CDATA[
$has_product_id = false;
$has_path = false;
foreach ($data as $query_key => $query_value) {
if ($query_key == 'product_id' && !empty($data['route']) && $data['route'] == 'product/product') {
$has_product_id = true;
}
if ($query_key == 'path') {
$has_path = true;
}
}
if ($has_product_id && $has_path) {
unset($data['path']);
} else if (!$has_product_id && $has_path) {
$path_parts = explode('_', $data['path']);
$data['path'] = $path_parts[count($path_parts) - 1];
}
]]></add>
</operation>
</file>
</modification>
Step 2, Approach 2 - Modify the store to always use the long versions of the links for categories and products.
This will make OpenCart convert all of the SEO links to the longest path possible. So as a result all links to child categories and products will look like this:
http://myshinystore.com/parent_category_1/parent_category_2/child_category (instead of http://myshinystore.com/child_category)
Add the following contents to the newly created file duplicate_url_fix.ocmod.xml:
<?xml version="1.0" encoding="UTF-8"?>
<modification>
<name><![CDATA[Duplicate Content Fix]]></name>
<code><![CDATA[duplicate_content_fix]]></code>
<version><![CDATA[1.0]]></version>
<author><![CDATA[iSenseLabs]]></author>
<link><![CDATA[https://isenselabs.com]]></link>
<file path="catalog/controller/product/category.php">
<operation>
<search><![CDATA[$category_info = $this->model_catalog_category->getCategory($category_id);]]></search>
<add position="before"><![CDATA[
$this->session->data['last.entered.category'] = $category_id;
]]></add>
</operation>
</file>
<file path="catalog/controller/*/seo_url.php">
<operation>
<search><![CDATA[public function index() {]]></search>
<add position="before"><![CDATA[
public function findParentPath($category_id) {
$found_path = array($category_id);
do {
$category_result = $this->db->query("SELECT * FROM " . DB_PREFIX . "category WHERE category_id = '" . $category_id . "'");
$category_id = (int)$category_result->row['parent_id'];
if ($category_id > 0 && !in_array($category_id, $found_path)) {
array_unshift($found_path, $category_id);
}
} while ($category_id != 0);
return $found_path;
}
]]></add>
</operation>
<operation>
<search><![CDATA[parse_str($url_info['query'], $data);]]></search>
<add position="after"><![CDATA[
$has_product_id = false;
$has_path = false;
foreach ($data as $query_key => $query_value) {
if ($query_key == 'product_id' && !empty($data['route']) && $data['route'] == 'product/product') {
$has_product_id = $query_value;
unset($data['product_id']); // We will set it later, because we need product_id to be after path.
}
if ($query_key == 'path') {
$has_path = true;
}
}
// Calculate full path
$parent_categories_paths = array();
if ($has_product_id !== false) {
// Find the true path based on the product_id
$parent_categories_result = $this->db->query("SELECT * FROM " . DB_PREFIX . "product_to_category WHERE product_id='" . (int)$has_product_id . "'");
foreach ($parent_categories_result->rows as $parent_category) {
$parent_categories_paths[] = $this->findParentPath($parent_category['category_id']);
}
} else if ($has_path) {
// Find the true path based on the last category_id
$path_parts = explode('_', $data['path']);
$category_id = $path_parts[count($path_parts) - 1];
$parent_categories_paths[] = $this->findParentPath($category_id);
}
if (!empty($parent_categories_paths)) {
$last_entered_category = !empty($this->session->data['last.entered.category']) ? (int)$this->session->data['last.entered.category'] : 0;
$data['path'] = implode('_', $parent_categories_paths[0]);
$has_path = true;
foreach ($parent_categories_paths as $parent_categories_path_candidate) {
if (in_array($last_entered_category, $parent_categories_path_candidate)) {
$data['path'] = implode('_', $parent_categories_path_candidate);
break;
}
}
}
if ($has_product_id !== false) {
$data['product_id'] = $has_product_id;
}
]]></add>
</operation>
</file>
</modification>
Step 2, Approach 3 - Modify the store to always use the long versions of the links for categories and short links for products.
This will make OpenCart convert all of the SEO links for categories to the longest path possible. The SEO links for products will be converted to the shortest path possible.
So as a result all links to child categories like this:
http://myshinystore.com/parent_category_1/parent_category_2/child_category (instead of http://myshinystore.com/child_category)
... and all links to products will look like this:
http://myshinystore.com/product_seo_url
Add the following contents to the newly created file duplicate_url_fix.ocmod.xml:
<?xml version="1.0" encoding="UTF-8"?>
<modification>
<name><![CDATA[Duplicate Content Fix]]></name>
<code><![CDATA[duplicate_content_fix]]></code>
<version><![CDATA[1.0]]></version>
<author><![CDATA[iSenseLabs]]></author>
<link><![CDATA[https://isenselabs.com]]></link>
<file path="catalog/controller/product/category.php">
<operation>
<search><![CDATA[$category_info = $this->model_catalog_category->getCategory($category_id);]]></search>
<add position="before"><![CDATA[
$this->session->data['last.entered.category'] = $category_id;
]]></add>
</operation>
</file>
<file path="catalog/controller/*/seo_url.php">
<operation>
<search><![CDATA[public function index() {]]></search>
<add position="before"><![CDATA[
public function findParentPath($category_id) {
$found_path = array($category_id);
do {
$category_result = $this->db->query("SELECT * FROM " . DB_PREFIX . "category WHERE category_id = '" . $category_id . "'");
$category_id = (int)$category_result->row['parent_id'];
if ($category_id > 0 && !in_array($category_id, $found_path)) {
array_unshift($found_path, $category_id);
}
} while ($category_id != 0);
return $found_path;
}
]]></add>
</operation>
<operation>
<search><![CDATA[parse_str($url_info['query'], $data);]]></search>
<add position="after"><![CDATA[
$has_product_id = false;
$has_path = false;
foreach ($data as $query_key => $query_value) {
if ($query_key == 'product_id' && !empty($data['route']) && $data['route'] == 'product/product') {
$has_product_id = $query_value;
unset($data['product_id']); // We will set it later, because we need product_id to be after path.
}
if ($query_key == 'path') {
$has_path = true;
}
}
// Calculate full path
$parent_categories_paths = array();
if ($has_product_id !== false) {
// Unset path and manufacturer_id because there is a product_id
unset($data['path']);
unset($data['manufacturer_id']);
} else if ($has_path) {
// Find the true path based on the last category_id
$path_parts = explode('_', $data['path']);
$category_id = $path_parts[count($path_parts) - 1];
$parent_categories_paths[] = $this->findParentPath($category_id);
}
if (!empty($parent_categories_paths)) {
$last_entered_category = !empty($this->session->data['last.entered.category']) ? (int)$this->session->data['last.entered.category'] : 0;
$data['path'] = implode('_', $parent_categories_paths[0]);
$has_path = true;
foreach ($parent_categories_paths as $parent_categories_path_candidate) {
if (in_array($last_entered_category, $parent_categories_path_candidate)) {
$data['path'] = implode('_', $parent_categories_path_candidate);
break;
}
}
}
if ($has_product_id !== false) {
$data['product_id'] = $has_product_id;
}
]]></add>
</operation>
</file>
</modification>
Step 3 - Uploading the file
Almost there. Now save your file and install it with the OpenCart Extension Installer. Make sure after you upload the file to click Refresh in Admin > Extensions > Modifications in order for the changes to get applied.
That’s it!
Congratulations! The changes you made will help avoid the duplicate content issue. Note that this is not the only way to resolve this issue - another totally different approach would be to use canonical URLs in your pages. More information about canonical URLs can be found here:
https://support.google.com/webmasters/answer/139066
If you want to use canonical URLs in your website, there are a few ready modules in the OpenCart Extension store:
http://www.opencart.com/index.php?route=marketplace/extension&filter_search=canonical
I hope you found the information above useful. Let us know if you have any questions in the comments below.
Note: This post was last updated on Oct. 25th, 2016, and on July 17th, 2017.