Skip to content

Migrate PrestaShop to WooCommerce on cPanel

This guide shows you how to migrate a PrestaShop 1.6, 1.7 or 8.x store to WooCommerce on a cPanel server, with both shops running side by side until cutover. You will finish with products, combinations, customers and images in WooCommerce and every old PrestaShop URL redirecting to its new product page.

This guide walks you through moving a PrestaShop store to WooCommerce when both run on a cPanel/WHM hosting account. Many merchants reach this point with a PrestaShop 1.6 or 1.7 shop that is hard to upgrade, and they fear losing their catalogue, customer accounts and search rankings. Those fears are manageable, because PrestaShop keeps everything in a MySQL database on your own server. The practical answer is a data conversion. You build WooCommerce next to the live shop, an importer plugin reads the PrestaShop database with read-only access, and a 301 redirect map sends every old product URL to its new page. By the end, WooCommerce takes orders on your domain, and the PrestaShop files and database sit archived beside it, ready for a rollback.

What you’ll accomplish

  • Read your PrestaShop version, table prefix, languages and catalogue size, so you know what the import has to carry.
  • Build WordPress and WooCommerce on a subdomain of the same cPanel account while PrestaShop keeps selling.
  • Import products, categories and images with the FG PrestaShop to WooCommerce plugin, using a SELECT-only database grant.
  • Write a 301 redirect map in .htaccess for PrestaShop’s friendly URLs, language prefixes and query-string links.
  • Swap the new store into public_html in a short window, test a real checkout, and keep a rollback ready.

Prerequisites

  • A cPanel account holding the PrestaShop store, with access to the Domains and MySQL Databases screens (or WHM root access).
  • SSH access with WP-CLI, the command-line tool for WordPress. Check with wp --info.
  • Admin access to the PrestaShop back office.
  • Enough disk space for a second copy of your product images. Check with du -sh ~/public_html/img/p.
  • A full cPanel backup. Nothing here writes to the PrestaShop database, but the cutover moves files.
  • A decision on the importer edition. The free edition imports products, categories, tags, images and content pages. Combinations, customers and orders need Premium, according to the plugin page.

The steps assume PrestaShop sits in public_html, the cPanel username is exmpl, and the table prefix is ps_. Swap in your own values wherever you see those.

How it works

PrestaShop and WooCommerce both run on PHP and MySQL. The importer only reads PrestaShop’s tables, so you do not need to upgrade PrestaShop first. cPanel’s MultiPHP Manager lets the old shop keep its PHP version while the new subdomain uses a current one.

Should you move at all? Stay on PrestaShop if you rely on multistore (several shops run from one back office). Also stay if you depend on business-to-business (B2B) or marketplace modules with no WooCommerce equivalent. Move if your content already lives in WordPress, or if a stuck 1.6 or 1.7 upgrade would be a rebuild anyway. A healthy PrestaShop 8 store with a happy team is a fine place to stay.

How the data maps. PrestaShop splits each product across several tables. ps_product holds the price, reference and status. ps_product_lang holds the name, description and link_rewrite (the URL slug), with one row per language, marked by id_lang. Combinations, which PrestaShop calls the size-and-colour variants of a product, live in ps_product_attribute. Categories are in ps_category, customers in ps_customer, orders in ps_orders, and image records in ps_image.

Mapping diagram of seven PrestaShop table groups to WooCommerce: ps_product and ps_product_lang become products, ps_attribute_group and ps_attribute become global pa_ attributes, ps_product_attribute combinations become variations, ps_category becomes product categories, ps_image files in the nested img/p folders become Media Library images, ps_customer becomes users with the customer role, and ps_orders becomes order history, with attributes, variations, customers and orders marked as Premium edition features and theme, modules, carriers and tax rules rebuilt by hand

In WooCommerce, a product with combinations becomes a variable product. Each combination becomes a variation with its own SKU (stock-keeping unit), price and stock. Option names become global attributes, stored as pa_ taxonomies such as pa_size.

Images live in nested folders. Since PrestaShop 1.5, image number 123 is stored as img/p/1/2/3/123.jpg, one folder per digit, with resized copies such as 123-home_default.jpg beside it. The importer downloads images over HTTP from the shop URL you give it, so the shop must stay online and out of maintenance mode during the import.

Multilingual shops need a WordPress decision first. WooCommerce on its own is single-language. A bilingual English and French shop needs a multilingual plugin, usually WPML or Polylang, each with its WooCommerce companion. The Premium importer has an add-on that imports translations into WPML. Make that choice before you import.

Passwords depend on the PrestaShop version. PrestaShop 1.6 stored md5(_COOKIE_KEY_ . password), a hash that only works with that shop’s secret key. PrestaShop 1.7 and later use bcrypt through PHP’s password_hash(). The Premium importer’s feature list says customers can log in to WordPress with their PrestaShop passwords. Treat that as something to prove on one test account, not something to assume.

Step-by-step

  1. Record the database details and version. On PrestaShop 1.7 and 8.x, read app/config/parameters.php:

    grep -E "'database_(host|name|user|prefix)'" ~/public_html/app/config/parameters.php

    Expected output:

        'database_host' => '127.0.0.1',
        'database_name' => 'exmpl_presta',
        'database_user' => 'exmpl_ps',
        'database_prefix' => 'ps_',

    On PrestaShop 1.6, run grep -E "_DB_(SERVER|NAME|USER|PREFIX)_|_PS_VERSION_" ~/public_html/config/settings.inc.php instead. It prints define(...) lines with the same values. Then confirm the version from the database:

    mysql -u exmpl_ps -p exmpl_presta -N -e "SELECT value FROM ps_configuration WHERE name='PS_VERSION_DB';"

    Expected: one line such as 8.1.7. If MySQL says the table does not exist, use the prefix from the file instead of ps_.

  2. Size the store and list the languages.

    mysql -u exmpl_ps -p exmpl_presta -e "SELECT (SELECT COUNT(*) FROM ps_product WHERE active=1) AS products, (SELECT COUNT(*) FROM ps_product_attribute) AS combinations, (SELECT COUNT(*) FROM ps_customer WHERE deleted=0) AS customers, (SELECT COUNT(*) FROM ps_orders) AS orders;"
    mysql -u exmpl_ps -p exmpl_presta -e "SELECT l.id_lang, l.iso_code, l.active, c.value IS NOT NULL AS is_default FROM ps_lang l LEFT JOIN ps_configuration c ON c.name='PS_LANG_DEFAULT' AND c.value=l.id_lang;"

    Expected: one row of counts such as 412 | 1830 | 2917 | 6204, then one row per language. A non-zero combinations count means you need Premium for variations. More than one active language means the WPML or Polylang decision from “How it works” comes first. Note the id_lang where is_default is 1; the redirect map uses it.

  3. Check how customer passwords are stored.

    mysql -u exmpl_ps -p exmpl_presta -e "SELECT LENGTH(passwd) AS hash_length, COUNT(*) FROM ps_customer WHERE deleted=0 AND is_guest=0 GROUP BY hash_length;"

    Expected: rows with 60 (bcrypt) and possibly 32 (the old md5 format). PrestaShop 1.7 and later upgrade an old hash when that customer next logs in, so a store upgraded from 1.6 often shows both. Customers in the 32 group are the ones most likely to need a password reset.

  4. Build WordPress and WooCommerce on a subdomain. In cPanel, open Domains and create new.example.com, then note its Document Root. The steps expect the recent cPanel default, ~/new.example.com, outside public_html. If cPanel placed it inside public_html, change it before you install, or the folder swap in step 10 will move the new store along with the old one. Create an empty database and user for WordPress in MySQL Databases, such as exmpl_wp and exmpl_wpuser. Then install:

    cd ~/new.example.com
    wp core download
    wp config create --dbname=exmpl_wp --dbuser=exmpl_wpuser --dbpass='REPLACE_ME'
    wp core install --url=https://new.example.com --title='Example Shop' \
      --admin_user=admin --admin_email=you@example.com --skip-email
    wp rewrite structure '/%postname%/' --hard
    wp plugin install woocommerce --activate

    Expected: Success: WordPress installed successfully., Success: Rewrite structure set., then Plugin 'woocommerce' activated. WooCommerce must be active before the importer runs. Its product pages use /product/<slug>/ by default, which is what the redirect map targets.

  5. Give the WordPress database user read-only access to PrestaShop. Both databases sit on the same MySQL server under the same account, so one grant does it.

    uapi Mysql set_privileges_on_database user=exmpl_wpuser database=exmpl_presta privileges=SELECT

    Run it as the cPanel user; as root, add --user=exmpl after uapi. Expected: a YAML block containing status: 1. With SELECT only, a wrong importer setting cannot change a single PrestaShop row.

  6. Run the importer.

    wp plugin install fg-prestashop-to-woocommerce --activate

    Expected: Plugin 'fg-prestashop-to-woocommerce' activated. For Premium, install its zip instead. In the WordPress admin, open Tools > Import > PrestaShop. Enter the shop’s home page URL (https://example.com), the host from step 1, database exmpl_presta, user exmpl_wpuser, its password and prefix ps_. Test the connection, start the import, and keep the tab open until it finishes. Then count what landed:

    wp post list --post_type=product --post_status=publish --format=count
    wp post list --post_type=product_variation --format=count
    wp term list product_cat --format=count

    Expected: a product count close to step 2’s active products, a variation count close to the combinations (zero on the free edition), and a category count close to PrestaShop’s. Check that slugs match link_rewrite, since the redirects depend on it:

    wp post list --post_type=product --fields=ID,post_name --posts_per_page=3

    Expected: slugs such as mug-best-day that appear in your old product URLs.

  7. Confirm the images came across. Count the original image files, not the resized copies, and compare with the attachments in WordPress:

    mysql -u exmpl_ps -p exmpl_presta -N -e "SELECT COUNT(*) FROM ps_image;"
    find ~/public_html/img/p -type f -regex '.*/[0-9]+\.jpg' | wc -l
    wp post list --post_type=attachment --post_status=inherit --format=count

    Expected: three numbers in the same range. A much lower WordPress count means the importer could not fetch images; see Troubleshooting.

  8. Generate the redirect map. PrestaShop’s default product URL is /{category}/{id}-{rewrite}.html. Version 1.7 and later can add a combination ID, as in /home/12-45-mug-best-day.html. Multilingual shops add /en/ or /fr/ in front. With friendly URLs off, links look like /index.php?id_product=12&controller=product.

    Diagram of PrestaShop URL shapes mapped to redirect rules: friendly URLs with a language prefix and URLs with a combination ID are caught by one RewriteRule per product, query-string URLs with id_product are caught by RewriteCond on QUERY_STRING, all ending at the WooCommerce product page with a 301, the block placed above BEGIN WordPress, followed by a four-step cutover of pausing the shop, swapping folders, rewriting the hostname and testing, with a rollback that moves the folders back

    Export IDs and slugs for the default language (replace id_lang=1 with your value from step 2), then turn each row into rules:

    mysql -u exmpl_ps -p exmpl_presta -N -e "SELECT p.id_product, pl.link_rewrite FROM ps_product p JOIN ps_product_lang pl ON pl.id_product=p.id_product AND pl.id_shop=1 AND pl.id_lang=1 WHERE p.active=1 ORDER BY p.id_product;" > ~/ps-products.tsv
    awk -F'\t' '{
      printf "RewriteRule ^([a-z]{2}/)?(.+/)?%s(-[0-9]+)?-%s(-[0-9]+)?\\.html$ https://example.com/product/%s/ [R=301,L]\n", $1, $2, $2
      printf "RewriteCond %%{QUERY_STRING} (^|&)id_product=%s(&|$)\n", $1
      printf "RewriteCond %%{QUERY_STRING} (^|&)controller=product(&|$)\n"
      printf "RewriteRule ^(index\\.php)?$ https://example.com/product/%s/? [R=301,L]\n", $2
    }' ~/ps-products.tsv > ~/ps-redirects.conf
    head -4 ~/ps-redirects.conf

    Expected:

    RewriteRule ^([a-z]{2}/)?(.+/)?12(-[0-9]+)?-mug-best-day(-[0-9]+)?\.html$ https://example.com/product/mug-best-day/ [R=301,L]
    RewriteCond %{QUERY_STRING} (^|&)id_product=12(&|$)
    RewriteCond %{QUERY_STRING} (^|&)controller=product(&|$)
    RewriteRule ^(index\.php)?$ https://example.com/product/mug-best-day/? [R=301,L]

    The first rule catches the product with or without a language prefix, category path, combination ID or trailing EAN13 barcode number. The next three handle query-string links, which Apache’s plain Redirect directive cannot see. The trailing ? in the target drops the old query string.

    On a bilingual shop, this map sends French URLs to the default-language product. Once each translation has its own address, repeat the export with the French id_lang and a ^fr/ rule. The Premium edition can also redirect old URLs by itself; if you use that, skip this step but still run the tests below.

    Paste the rules into ~/new.example.com/.htaccess above # BEGIN WordPress, with hand-written lines for categories and CMS pages:

    # --- PrestaShop legacy URLs: keep above the WordPress block ---
    RewriteEngine On
    RewriteRule ^([a-z]{2}/)?3-mugs/?$ https://example.com/product-category/mugs/ [R=301,L]
    RewriteRule ^([a-z]{2}/)?content/4-about-us/?$ https://example.com/about-us/ [R=301,L]
    # ...rest of ps-redirects.conf...
    # --- end PrestaShop legacy URLs ---
    
    # BEGIN WordPress

    Placement matters. The WordPress block starts with RewriteRule ^index\.php$ - [L], which ends processing for index.php requests, so query-string rules below it never run.

  9. Rebuild taxes, shipping and payments. The importer’s feature list does not include tax rules, carriers or payment modules. Set these up in WooCommerce by hand before cutover. For a Canadian store, follow our guide to WooCommerce tax rates for GST, HST and QST. Our pre-flight audit for a WooCommerce migration lists the gateway and webhook checks to clear first. Place one test order on new.example.com with each payment method before moving on.

  10. Cut over. Run a final import so recent products and orders are current. Then record the last order, so you can spot any placed during the swap:

    mysql -u exmpl_ps -p exmpl_presta -e "SELECT MAX(id_order), MAX(date_add) FROM ps_orders;"

    Expected: one row with the newest order ID and timestamp. In the PrestaShop back office, set Shop Parameters > General > Maintenance > Enable store to No (on 1.6, Preferences > Maintenance). Then swap the folders:

    cd ~
    mkdir prestashop-archive
    mv public_html/* public_html/.htaccess prestashop-archive/
    mv new.example.com/* new.example.com/.htaccess public_html/
    cd ~/public_html
    wp search-replace 'https://new.example.com' 'https://example.com' --all-tables
    wp option get siteurl

    Expected: a replacement table ending in a line such as Success: Made 3412 replacements., then https://example.com. WP-CLI keeps WordPress’s serialized settings intact, which a SQL REPLACE would break. Run the MAX(id_order) query again; a higher number is an order to re-enter by hand.

    Both stores live on one account, so DNS does not change. If WooCommerce runs on another server, lower the TTL (how long resolvers cache the record) to 300 seconds a day ahead. Change the A record in the window and check it with dig +short example.com, which should print the new IP.

    Rollback. If checkout fails and you cannot fix it quickly, reverse the swap. Move public_html contents into a woo-failed folder, move prestashop-archive back, and set Enable store to Yes. The PrestaShop database was never written to, so the shop returns as it was. Orders taken on WooCommerce in between need re-entering.

  11. Test customer logins. Log in on /my-account/ as one real customer from the 60 group and one from the 32 group in step 3. If either test fails, send every customer account a reset link. Many cPanel servers cap how many emails a domain may send per hour, so the first line splits the customer IDs into files of 200 and the second sends the first file:

    wp user list --role=customer --field=ID | split -l 200 - ~/reset-batch-
    xargs wp user reset-password < ~/reset-batch-aa

    Expected: one Reset password for ... line per user in the batch, then a Success: line. An hour later, run the second line again with reset-batch-ab, then reset-batch-ac, until you have sent every file that ls ~/reset-batch-* lists. These commands cover all customers, not only the failing group. Warn customers before the emails go out, so they do not look like phishing.

Validate it works

Test each URL shape against a known product:

for u in '/home/12-mug-best-day.html' '/en/home/12-45-mug-best-day.html' '/index.php?id_product=12&controller=product'; do
  printf '%s  %s\n' "$(curl -s -o /dev/null -w '%{http_code} %{redirect_url}' "https://example.com$u")" "$u"
done

Expected: three lines, each starting 301 https://example.com/product/mug-best-day/. Then check that no content still points at PrestaShop image paths:

wp db query "SELECT COUNT(*) FROM wp_posts WHERE post_status='publish' AND post_content LIKE '%/img/p/%';" --skip-column-names

Expected: 0. Finish with one real order on the live domain, paid and refunded, and confirm the order email arrives.

Troubleshooting

  • The importer cannot connect. The grant from step 5 is missing or the prefix is wrong. Test with mysql -u exmpl_wpuser -p exmpl_presta -e "SHOW TABLES LIKE 'ps_product';", which should print the table name. If 127.0.0.1 fails, try localhost.
  • Products imported but images are missing. The plugin’s FAQ says the URL field must hold the shop’s home page and maintenance mode must be off. An http to https redirect, a Cloudflare challenge or cPanel hotlink protection can also block downloads. Fix those, then re-run the import.
  • Products have no size or colour options. Combinations and attributes are Premium features. The free edition imports each product without its variants.
  • The import stops partway. Add define('WP_MEMORY_LIMIT', '2G'); to wp-config.php and run it again. The plugin’s FAQ says a new run continues where the last one stopped.
  • Old URLs return 404 after cutover. The redirect block is below # BEGIN WordPress. Check with grep -n 'BEGIN WordPress\|PrestaShop legacy' ~/public_html/.htaccess; the legacy line number must be smaller.

When this is the wrong approach

If the store’s value lives in modules, the catalogue import is the easy part. A marketplace module, a B2B price-list system or a custom checkout needs its own WooCommerce plan, settled before you schedule a cutover.

Multistore setups and shops in four or more languages are a bigger project than this guide covers. For those, upgrading to a supported PrestaShop release may cost less than a platform change. Apache’s mod_rewrite reference documents the RewriteCond and RewriteRule directives used above.

Running WordPress that earns real money deserves an operator — Stack Harbor runs that stack under Enterprise WordPress; book an infrastructure consult.