Guía práctica para Migrar theme PrestaShop 1.7.X a 8.2 sin romper el checkout

El otro día creo que hice cuentas y ya llevaba más de 20 años en esto. Y este año además de llevar proyectos, hacer alguna que otra consultoría UX (qué es realmente lo que me gusta), también he migrado Themes desde PrestaShop 1.7.X a 8.2.

Así que me he animado a escribir una pequeña guía con todo lo que he aprendido a base de debugging hasta las 3AM, y mucho, mucho café por la mañana.

Los principales cambios a resolver son:

  1. Nuevos controladores: Identificación y Registro son controladores independientes.
  2. Cambio en el marcado de los bloques asíncronos.
  3. Nuevas política de contraseñas
  4. Nuevos bloques y subtpls necesarios.
  5. Compatibilidad con nuevos formatos de imagen, ahora por defecto se pueden generar imágenes WEBP  o AVIF
  6. Compatibilidad de búsqueda facetada con Manufacturers y Search  🙂

¿Quieres hacerlo tú solo? Adelante. Pero antes de que te lances, lee esto. O contrata a alguien que sepa (ejem). Tu elección.

Vale, te atreves y no sabes como empezar.

1. Revisa theme.yml

Es posible que si se trata de una versión bastante antigua como la 1.7.2 tengamos que realizar algunos ajustes en el theme, ya que en las versiones previas de 1.7. no había un yml de configuración por lo que habrá que crearlo porque sin él, PrestaShop 8 no sabe qué hacer.

yaml

# config/theme.yml
name: mi_theme
display_name: Mi Theme Migrado
version: 2.0.0
author:
  name: "Tu Nombre"
  email: "tu@email.com"
  url: "https://tu-web.com"

meta:
  compatibility:
    from: 8.0.0
    to: ~
  available_layouts:
    layout-full-width:
      name: Full Width
      description: "Sin columnas laterales"
    layout-both-columns:
      name: Three Columns
      description: "Columnas izquierda y derecha"
    layout-left-column:
      name: Two Columns, left sidebar
      description: "Columna izquierda solamente"
    layout-right-column:
      name: Two Columns, right sidebar
      description: "Columna derecha solamente"

assets:
  use_parent_assets: false
  css:
    all:
      - id: theme-main
        path: assets/css/theme.css
        priority: 50
        media: all
      - id: custom
        path: assets/css/custom.css
        priority: 100
        media: all
  js:
    all:
      - id: theme-main
        path: assets/js/theme.js
        priority: 50
        position: bottom
      - id: custom
        path: assets/js/custom.js
        priority: 100
        position: bottom

global_settings:
  configuration:
    PS_QUICK_VIEW: true
    PS_DISABLE_OVERRIDES: false
  
  modules:
    to_enable:
      - ps_linklist
      - ps_mainmenu
      - ps_searchbar
      - ps_shoppingcart
      - ps_customersignin
    to_disable:
      - welcome
      - ps_advertising
  
  hooks:
    modules_to_hook:
      displayNav:
        - ps_customersignin
        - ps_shoppingcart
      displayTop:
        - ps_mainmenu
        - ps_searchbar
      displayHome:
        - ps_imageslider
        - ps_featuredproducts
      displayFooter:
        - ps_linklist
        - ps_customeraccountlinks
  
  image_formats:
    cart_default:
      width: 125
      height: 125
      scope: [products]
    small_default:
      width: 98
      height: 98
      scope: [products, categories, manufacturers, suppliers]
    medium_default:
      width: 452
      height: 452
      scope: [products, manufacturers, suppliers]
    home_default:
      width: 250
      height: 250
      scope: [products]
    large_default:
      width: 800
      height: 800
      scope: [products, manufacturers, suppliers]
    category_default:
      width: 141
      height: 180
      scope: [categories]
    stores_default:
      width: 170
      height: 115
      scope: [stores]

2. Theme.js: Tíralo y empieza de cero

No pierdas tiempo. En serio. Haz esto:

  1. Guarda el theme.js viejo por si acaso
  2. Copia el de Classic de la versión 8.2
  3. Añade el posible código custom de theme.js a custom.js

No intentes adaptar un bundle de webpack minificado de 2018. No vale la pena.

3. Tabla completa de mapeo de selectores PrestaShop 1.7.X → 8.2

Otro problema con el que te encontrarás serán los bloques AJAX y te ocurrirán cosas como:

  • No funciona el cambio de precio con el cambio de combinación.
  • No se cambia la imagen al cambiar la combinación.
  • No funciona la vista rápida (deberías quitarla siempre).
  • Problemas diversos en el checkout direcciones, unidades, etc.

Esto principalmente es debido a cambios en los selectores JS que hacen la recarga de dichos bloques AJAX, que están definidos en core.js

Este es el cambio más tedioso. Los selectores han cambiado. Para aplicar estos cambios, tienes que buscar en tu template cada uso de estos selectores y actualizarlos y comprobar que existe el id o clase nueva definida. Sí, es un coñazo y Sí, PrestaShop podría haberlo hecho retrocompatible…

Selectores de Producto

Selector 1.7.XSelector 8.2Contexto de uso
#quantity_wanted#quantity_wantedInput de cantidad en página de producto
.images-container.images-container, .js-images-containerContenedor de galería de imágenes
.product-container.product-container, .js-product-containerContenedor principal del producto
#product-availability#product-availability, .js-product-availabilityDisponibilidad/stock del producto
.product-actions.product-actions, .js-product-actionsAcciones del producto (añadir, cantidad)
.product-variants.product-variants, .js-product-variantsSelector de variantes/combinaciones
.product-refresh.product-refresh, .js-product-refreshBotón de actualizar producto
.product-miniature.js-product-miniatureMiniatura en listados de productos
.product-minimal-quantity.product-minimal-quantity, .js-product-minimal-quantityCantidad mínima de compra
.product-add-to-cart.product-add-to-cart, .js-product-add-to-cartBotón añadir al carrito
.product-prices.product-prices, .js-product-pricesBloque de precios
input[name="id_customization"].js-product-actions .js-product-customization-idID de personalización
.product-customization.product-customization, .js-product-customizationBloque de personalización
.product-variants.product-variants, .js-product-variantsActualización de variantes AJAX
.product-discounts.product-discounts, .js-product-discountsDescuentos del producto
.product-additional-info.product-additional-info, .js-product-additional-infoInformación adicional
#product-details#product-details, .js-product-detailsDetalles/ficha técnica
.product-flags.product-flags, .js-product-flagsEtiquetas (nuevo, oferta, etc.)

Selectores de Listado/Catálogo

Selector 1.7.XSelector 8.2Contexto de uso
.quick-view.quick-view, .js-quick-viewBotón de vista rápida

Selectores de Checkout

Selector 1.7.XSelector 8.2Contexto de uso
.checkout-step form.checkout-step formFormularios del checkout
.js-current-step.js-current-stepPaso actual del checkout
.checkout-step.checkout-stepContenedor de pasos
.step-title.step-title, .js-step-titleTítulo del paso
#payment-confirmation button#payment-confirmation button, .js-payment-confirmationBotón confirmar pago
#conditions-to-approve input[type="checkbox"]#conditions-to-approve input[type="checkbox"], .js-conditions-to-approveCheckbox de condiciones
.js-alert-payment-conditionsAlerta de condiciones de pago
.js-additional-informationInformación adicional
.js-payment-option-formFormulario opciones de pago
#conditions-to-approve input[name="conditions_to_approve[terms-and-conditions]"].js-conditions-to-approve input[name="conditions_to_approve[terms-and-conditions]"]Checkbox términos y condiciones
.payment-binary.payment-binary, .js-payment-binaryOpciones de pago binarias

Selectores de Envío (Checkout)

Selector 1.7.XSelector 8.2Contexto de uso
#js-delivery#js-deliveryFormulario de envío
#js-checkout-summary#js-checkout-summaryResumen del checkout
#checkout-delivery-step#checkout-delivery-stepPaso de envío
.js-edit-deliveryBotón editar envío
.delivery-option.delivery-option, .js-delivery-optionOpciones de envío
.js-cart-payment-step-refreshActualizar paso de pago

Selectores de direcciones (Checkout)

Selector 1.7.XSelector 8.2Contexto de uso
.js-edit-addressesEditar direcciones
#delivery-addresses input[type=radio]#delivery-addresses input[type=radio], #invoice-addresses input[type=radio], .js-address-selector input[type=radio]Radio buttons de direcciones
.address-item.address-item, .js-address-itemItem de dirección
#checkout-addresses-step#checkout-addresses-stepPaso de direcciones
.address-item:has(input[type=radio]:checked).address-item:has(input[type=radio]:checked), .js-address-item:has(input[type=radio]:checked)Dirección seleccionada
.js-address-errorError en dirección
#not-valid-addresses#not-valid-addresses, .js-not-valid-addressesDirecciones no válidas
#invoice-addresses#invoice-addresses, .js-address-selectorDirecciones de facturación
.js-address-formFormulario de dirección

Selectores de Carrito

Selector 1.7.XSelector 8.2Contexto de uso
.cart-detailed-totals.cart-detailed-totals, .js-cart-detailed-totalsTotales detallados
.cart-summary-items-subtotal.cart-summary-items-subtotal, .js-cart-summary-items-subtotalSubtotal de items
.cart-summary-subtotals-container.cart-summary-subtotals-container, .js-cart-summary-subtotals-containerContenedor de subtotales
.cart-summary-totals.cart-summary-totals, .js-cart-summary-totalsTotales del resumen
.cart-summary-products.cart-summary-products, .js-cart-summary-productsProductos del resumen
.cart-detailed-actions.cart-detailed-actions, .js-cart-detailed-actionsAcciones detalladas
.cart-voucher.cart-voucher, .js-cart-voucherCupones/vales descuento
.cart-overview.cart-overviewVista general del carrito
.cart-summary-top.cart-summary-top, .js-cart-summary-topParte superior del resumen
#product_customization_id#product_customization_id, .js-product-customization-idID personalización en carrito
.js-cart-line-product-quantityCantidad de línea de producto

4. Nuevo controlador de registro y login

En las versiones de PrestaShop encontrábamos estos enlaces para Registro/Login como:

{$urls.pages.authentication}
{$urls.pages.register}

También es posible que os los encontréis con el parámetro create_account=1, a saber.

La recomendación es cambiarlas por sus nuevas urls.

{url entity='registration' params=$registerParams}
{url entity='authentication' params=$authParams}

Además habrá que generar o revisar que existe el tpl registration.tpl (en versiones iniciales no existía).

Cambiar los enlaces de registro y login y revisar que existe templates/customer/registration.tpl

5. Nuevas políticas de contraseñas

PrestaShop 8 tiene políticas de contraseña configurables. Genial. ¿Validación client-side? definida a través de un nuevo componente #password-feedback, pero en mi caso he preferido añadir una barrita debajo del campo contraseña para mostrar el feedback dando más información sobre lo que falta.

Así, que si no quieres usar el componente nativo y quieres tener más control puedes usar este JS en línea dentro de form-fields-tpl:

{* templates/_partials/form-fields.tpl *}
{block name='form_field_item_password'}
  {assign var="passwordMaxLength" value=Configuration::get('PS_SECURITY_PASSWORD_POLICY_MAXIMUM_LENGTH')}
  {assign var="passwordMinLength" value=Configuration::get('PS_SECURITY_PASSWORD_POLICY_MINIMUM_LENGTH')}
  {assign var="passwordMinScore" value=Configuration::get('PS_SECURITY_PASSWORD_POLICY_MINIMUM_SCORE')}
  {assign var="fieldId" value="field_{$field.name}"}
  {assign var="skipValidation" value=false}
  
  {* Verificar si estamos en contexto donde debemos saltar la validación *}
  {if $field.autocomplete == 'current-password'}
    {assign var="skipValidation" value=true}
  {/if}
  
  {* Definir traducciones para JavaScript solo si no saltamos la validación *}
  {if !$skipValidation && !isset($password_js_defined)}
    {assign var="password_js_defined" value=true scope="global"}
    <script type="text/javascript">
      var passwordTranslations = {
        needChars: "{l s='You need to add' d='Shop.Theme.Global' js=1}",
        moreChars: "{l s='You need' d='Shop.Theme.Global' js=1}",
        lessChars: "{l s='Too many' d='Shop.Theme.Global' js=1}",
        charactersSingular: "{l s='character' d='Shop.Theme.Global' js=1}",
        charactersPlural: "{l s='characters' d='Shop.Theme.Global' js=1}",
        lowercase: "{l s='lowercase letters' d='Shop.Theme.Global' js=1}",
        uppercase: "{l s='uppercase letters' d='Shop.Theme.Global' js=1}",
        numbers: "{l s='numbers' d='Shop.Theme.Global' js=1}",
        special: "{l s='special characters' d='Shop.Theme.Global' js=1}",
        and: "{l s='and' d='Shop.Theme.Global' js=1}",
        addComplexity: "{l s='Your password needs to be more complex.' d='Shop.Theme.Global' js=1}",
        invalid: "{l s='Password does not meet minimum requirements' d='Shop.Theme.Global' js=1}"
      };
    </script>
  {/if}
  
  <div class="input-group js-parent-focus">
    <input
      class="form-control js-child-focus js-visible-password {if !$skipValidation}js-password-input-{$field.name}{/if}"
      name="{$field.name}"
      title="{l s='Password requirements' d='Shop.Theme.Global'}"
      type="password"
      id="{$fieldId}"
      value=""
      {if !$skipValidation}
        minlength="{$passwordMinLength}"
        maxlength="{$passwordMaxLength}"
        data-min-length="{$passwordMinLength}"
        data-max-length="{$passwordMaxLength}"
        data-min-score="{$passwordMinScore}"
        data-field-name="{$field.name}"
      {/if}
      {if $field.required}required{/if}
    >
    <span class="input-group-btn">
      <button
        class="btn"
        type="button"
        data-action="show-password"
        data-text-show="{l s='Show' d='Shop.Theme.Actions'}"
        data-text-hide="{l s='Hide' d='Shop.Theme.Actions'}"
        aria-label="{l s='Show/hide password' d='Shop.Theme.Global'}"
      >
        {l s='Show' d='Shop.Theme.Actions'}
      </button>
    </span>
  </div>
  
  {* Solo mostrar requisitos y validación si no saltamos la validación *}
  {if !$skipValidation}
    <div class="password-requirements mt-2">
      <small class="form-text password-suggestions-{$field.name} text-danger mt-1" style="display: none;"></small>
      <div class="password-strength-meter mt-2 mb-2">
        <label class="sr-only">{l s='Password strength:' d='Shop.Theme.Global'}</label>
        <div class="progress" style="height: 10px; border-radius: 5px; background-color: #f0f0f0;">
          <div class="password-progress-bar-{$field.name}" 
               role="progressbar" 
               style="width: 0%; height: 10px;" 
               aria-valuenow="0" 
               aria-valuemin="0" 
               aria-valuemax="4"></div>
        </div>
      </div>
    </div>
    
    <script type="text/javascript">
      document.addEventListener('DOMContentLoaded', function() {
        if (typeof initPasswordField === 'function') {
          initPasswordField('{$field.name}');
        }
      });
    </script>
  {/if}
  
  {* Incluir el código JavaScript principal solo una vez *}
  {if !$skipValidation && !isset($password_script_defined)}
    {assign var="password_script_defined" value=true scope="global"}
    {literal}
    <script type="text/javascript">
      // Función principal para inicializar cada campo de contraseña
      function initPasswordField(fieldName) {
        var passwordInput = document.querySelector('.js-password-input-' + fieldName);
        
        if (passwordInput) {
          var minLength = parseInt(passwordInput.getAttribute('data-min-length')) || 8;
          var maxLength = parseInt(passwordInput.getAttribute('data-max-length')) || 72;
          var minScore = parseInt(passwordInput.getAttribute('data-min-score')) || 0;
          
          // Inicializar el estado
          checkPasswordStrength(passwordInput, fieldName);
          
          // Validar la contraseña cuando cambia
          passwordInput.addEventListener('input', function() {
            checkPasswordStrength(this, fieldName);
          });
          
          // Establecer validez del formulario
          passwordInput.addEventListener('invalid', function(event) {
            if (isPasswordValid(this.value, minLength, maxLength, minScore)) {
              event.preventDefault();
            }
          });
        }
      }
      
      // Función principal para verificar la fuerza de la contraseña
      function checkPasswordStrength(inputElement, fieldName) {
        var password = inputElement.value;
        var minLength = parseInt(inputElement.getAttribute('data-min-length')) || 8;
        var maxLength = parseInt(inputElement.getAttribute('data-max-length')) || 72;
        var minScore = parseInt(inputElement.getAttribute('data-min-score')) || 0;
        
        // Verificar la longitud
        var lengthValid = password.length >= minLength && 
                         (maxLength === 0 || password.length <= maxLength);
        
        // Análisis de componentes
        var analysis = analyzePassword(password);
        var score = analysis.score;
        var scoreValid = score >= minScore;
        
        // Actualizar la barra de progreso
        updateProgressBar(score, scoreValid, fieldName);
        
        // Actualizar los mensajes de requisitos
        updateRequirements(lengthValid, scoreValid, password.length > 0, fieldName);
        
        // Mostrar sugerencias si es necesario
        updateDetailedSuggestions(password, lengthValid, scoreValid, analysis, 
                                 fieldName, minLength, maxLength, minScore);
        
        // Establecer la validez del campo
        setFieldValidity(inputElement, lengthValid && (scoreValid || password.length === 0));
      }
      
      // Analizar la contraseña y obtener puntuación y componentes faltantes
      function analyzePassword(password) {
        if (!password) return { score: 0, missing: [] };
        
        var score = 0;
        var missing = [];
        var hasLowercase = /[a-z]/.test(password);
        var hasUppercase = /[A-Z]/.test(password);
        var hasNumbers = /\d/.test(password);
        var hasSpecial = /[^A-Za-z0-9]/.test(password);
        
        // Longitud
        if (password.length >= 8) {
          score += 1;
        }
        
        // Letras minúsculas y mayúsculas
        if (hasLowercase && hasUppercase) {
          score += 1;
        } else {
          if (!hasLowercase) missing.push('lowercase');
          if (!hasUppercase) missing.push('uppercase');
        }
        
        // Números
        if (hasNumbers) {
          score += 1;
        } else {
          missing.push('numbers');
        }
        
        // Caracteres especiales
        if (hasSpecial) {
          score += 1;
        } else {
          missing.push('special');
        }
        
        return {
          score: score,
          hasLowercase: hasLowercase,
          hasUppercase: hasUppercase,
          hasNumbers: hasNumbers,
          hasSpecial: hasSpecial,
          missing: missing
        };
      }
      
      // Función para actualizar la barra de progreso
      function updateProgressBar(score, isValid, fieldName) {
        var progressBar = document.querySelector('.password-progress-bar-' + fieldName);
        if (progressBar) {
          // Ancho según la puntuación
          var widthPercentage = (score * 25) + '%';
          progressBar.style.width = widthPercentage;
          progressBar.setAttribute('aria-valuenow', score);
          
          // Color según validez
          if (score === 0) {
            progressBar.style.backgroundColor = '#dc3545'; // danger
          } else if (!isValid) {
            progressBar.style.backgroundColor = '#ffc107'; // warning
          } else {
            progressBar.style.backgroundColor = '#28a745'; // success
          }
        }
      }
      
      // Función para actualizar los requisitos visuales
      function updateRequirements(lengthValid, scoreValid, hasInput, fieldName) {
        var strengthReq = document.querySelector('.password-strength-req-' + fieldName);
        
        if (strengthReq && hasInput) {
          strengthReq.classList.toggle('text-success', scoreValid);
          strengthReq.classList.toggle('text-danger', !scoreValid);
        } else if (strengthReq) {
          strengthReq.classList.remove('text-success', 'text-danger');
        }
      }
      
      // Función para mostrar sugerencias detalladas de mejora
      function updateDetailedSuggestions(password, lengthValid, scoreValid, analysis, 
                                        fieldName, minLength, maxLength, minScore) {
        var suggestionElement = document.querySelector('.password-suggestions-' + fieldName);
        if (!suggestionElement) return;
        
        if (password.length > 0 && (!lengthValid || !scoreValid)) {
          var message = '';
          
          // Sugerencias basadas en longitud
          if (!lengthValid) {
            if (password.length < minLength) {
              var charsNeeded = minLength - password.length;
              message = passwordTranslations.moreChars + ' ' + charsNeeded + ' ' +
                (charsNeeded === 1 ? passwordTranslations.charactersSingular : 
                                     passwordTranslations.charactersPlural);
            } else if (maxLength > 0 && password.length > maxLength) {
              var charsExcess = password.length - maxLength;
              message = passwordTranslations.lessChars + ' ' + charsExcess + ' ' +
                (charsExcess === 1 ? passwordTranslations.charactersSingular : 
                                     passwordTranslations.charactersPlural);
            }
          }
          
          // Sugerencias basadas en complejidad
          if (!scoreValid && analysis.missing.length > 0) {
            var neededComponents = [];
            
            for (var i = 0; i < analysis.missing.length; i++) {
              var type = analysis.missing[i];
              switch (type) {
                case 'lowercase':
                  neededComponents.push(passwordTranslations.lowercase);
                  break;
                case 'uppercase':
                  neededComponents.push(passwordTranslations.uppercase);
                  break;
                case 'numbers':
                  neededComponents.push(passwordTranslations.numbers);
                  break;
                case 'special':
                  neededComponents.push(passwordTranslations.special);
                  break;
              }
            }
            
            if (neededComponents.length > 0) {
              var complexityMessage = passwordTranslations.needChars + ': ';
              
              if (neededComponents.length === 1) {
                complexityMessage += neededComponents[0];
              } else {
                for (var j = 0; j < neededComponents.length - 1; j++) {
                  complexityMessage += neededComponents[j] + ", ";
                }
                complexityMessage += passwordTranslations.and + " " + 
                                   neededComponents[neededComponents.length - 1];
              }
              
              message = message ? message + '. ' + complexityMessage : complexityMessage;
            }
          }
          
          // Si aún necesita más complejidad pero ya tiene todos los componentes
          if (!scoreValid && analysis.missing.length === 0 && analysis.score < minScore) {
            var additionalMessage = passwordTranslations.addComplexity;
            message = message ? message + '. ' + additionalMessage : additionalMessage;
          }
          
          if (message) {
            suggestionElement.style.display = 'block';
            suggestionElement.innerHTML = message;
          } else {
            suggestionElement.style.display = 'none';
          }
        } else {
          suggestionElement.style.display = 'none';
        }
      }
      
      // Establecer la validez del campo
      function setFieldValidity(inputField, isValid) {
        if (!isValid && inputField.value.length > 0) {
          inputField.setCustomValidity(passwordTranslations.invalid);
        } else {
          inputField.setCustomValidity('');
        }
      }
      
      // Comprobar si la contraseña es válida
      function isPasswordValid(password, minLength, maxLength, minScore) {
        if (password.length === 0) return true; // Campo vacío es válido (se manejará por required)
        
        var lengthValid = password.length >= minLength && 
                         (maxLength === 0 || password.length <= maxLength);
        var scoreValid = analyzePassword(password).score >= minScore;
        
        return lengthValid && scoreValid;
      }
    </script>
    {/literal}
  {/if}
{/block}

Funciona y tus usuarios no se volverán locos intentando adivinar qué contraseña quiere PrestaShop según los requisitos que definas en tu plantilla heredada.

6. Imágenes WEBP/AVIF: por fin algo bueno.

PrestaShop 8 genera WEBP y AVIF nativamente. La buena noticia: adiós plugins de conversión. La mala: tienes que reescribir TODOS tus templates de imágenes.

smarty

{* Así es como debes mostrar las imágenes ahora *}
<picture>
  {if !empty($product.default_image.bySize.large_default.sources.webp)}
    <source
      srcset="{$product.default_image.bySize.large_default.sources.webp} {$product.default_image.bySize.large_default.width}w, {$product.default_image.bySize.medium_default.sources.webp} {$product.default_image.bySize.medium_default.width}w"
      sizes="auto" type="image/webp">
  {/if}
  {if !empty($product.default_image.bySize.large_default.sources.avif)}
    <source
      srcset="{$product.default_image.bySize.large_default.sources.avif} {$product.default_image.bySize.large_default.width}w, {$product.default_image.bySize.medium_default.sources.avif} {$product.default_image.bySize.medium_default.width}w"
      sizes="auto" type="image/avif">
  {/if}
  <source
    srcset="{$product.default_image.bySize.large_default.url} {$product.default_image.bySize.large_default.width}w, {$product.default_image.bySize.medium_default.url} {$product.default_image.bySize.medium_default.width}w"
    sizes="auto" type="image/jpg">
  <img sizes="auto" width="xxx" height="xxx" alt="xx" class="xxx" />
</picture>

IMPORTANTE: USA $product.default_image, NO $product.cover.

¿Por qué? Porque cuando filtras por color en la búsqueda facetada, PrestaShop cambia la imagen cover al color seleccionado y así mejoras la experiencia de uso cuando la usuaria busca camisetas rojas.

7. Templates y subtemplates que faltan

Activa el debug (_PS_MODE_DEV_ en defines.inc.php) y mira los logs. Verás cosas como:

No template found for checkout/_partials/cart-summary-top.tpl
No template found for checkout/_partials/cart-summary-totals.tpl
No template found for checkout/_partials/cart-summary-products.tpl
No template found for catalog/_partials/category-footer.tpl

Cada uno de estos es un partial nuevo que PrestaShop 8 espera. Tienes que crearlos todos:

8. Resolver TODOS los notices de Smarty

Con el debug activado, verás algún que otro:

Notice: Undefined index: some_variable

La solución es envolver TODO con checks de existencia:

Recuerda, que la mayoría de notices que encontrarás serán por usar variables en Smarty sin definir. Basta con un:
{if !empty($var)}
{$var}
{/if}

Sí, es tedioso. Sí, deberías hacerlo. Los notices ralentizan la carga y llenan los logs.

9. Compatibilidad con búsqueda facetada mejorada

La búsqueda facetada ahora funciona con manufacturers y search. Pero tu PrestaShop no lo sabe.

  1. Tienes que actualizar la configuración de la búsqueda facetada
  2. Comprobar que tienes el Hook colum left activos en ambos controladores.
  3. Ir a la configuración de páginas y marcar a dos columnas manufacturer y search

Problemas que SEGURO encontrarás

  1. «Undefined index» everywhere: Wrap con {if !empty()}
  2. El carrito no se actualiza: Revisa los selectores JS
  3. Las imágenes no cargan en Safari: Orden de sources incorrecto
  4. Búsqueda facetada muestra todo del mismo color: Usaste $product.cover
  5. 500 Internal Server Error al hacer login: No creaste registration.tpl
  6. Los formularios no validan: No implementaste la validación de contraseñas
  7. «No template found»: Crea los templates que faltan
  8. JavaScript errors en consola: theme.js incompatible

¿Funciona al final? Sí. ¿Vale la pena? Si tu cliente paga, sí. ¿Es frustrante? Absolutamente.

Pero bueno, aquí tienes toda la información que PrestaShop no te da. Gratis. De nada.

Si te sirve y te ahorra tiempo, invítame a algo. Si no te funciona, probablemente es porque tu theme tiene alguna particularidad que no he visto. En ese caso, debugging y paciencia.

P.D.: Todo este código está testeado en producción con themes reales. No es teoría, es práctica.