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:
- Nuevos controladores: Identificación y Registro son controladores independientes.
- Cambio en el marcado de los bloques asíncronos.
- Nuevas política de contraseñas
- Nuevos bloques y subtpls necesarios.
- Compatibilidad con nuevos formatos de imagen, ahora por defecto se pueden generar imágenes WEBP o AVIF
- 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:
- Guarda el theme.js viejo por si acaso
- Copia el de Classic de la versión 8.2
- 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.X | Selector 8.2 | Contexto de uso |
---|---|---|
#quantity_wanted | #quantity_wanted | Input de cantidad en página de producto |
.images-container | .images-container, .js-images-container | Contenedor de galería de imágenes |
.product-container | .product-container, .js-product-container | Contenedor principal del producto |
#product-availability | #product-availability, .js-product-availability | Disponibilidad/stock del producto |
.product-actions | .product-actions, .js-product-actions | Acciones del producto (añadir, cantidad) |
.product-variants | .product-variants, .js-product-variants | Selector de variantes/combinaciones |
.product-refresh | .product-refresh, .js-product-refresh | Botón de actualizar producto |
.product-miniature | .js-product-miniature | Miniatura en listados de productos |
.product-minimal-quantity | .product-minimal-quantity, .js-product-minimal-quantity | Cantidad mínima de compra |
.product-add-to-cart | .product-add-to-cart, .js-product-add-to-cart | Botón añadir al carrito |
.product-prices | .product-prices, .js-product-prices | Bloque de precios |
input[name="id_customization"] | .js-product-actions .js-product-customization-id | ID de personalización |
.product-customization | .product-customization, .js-product-customization | Bloque de personalización |
.product-variants | .product-variants, .js-product-variants | Actualización de variantes AJAX |
.product-discounts | .product-discounts, .js-product-discounts | Descuentos del producto |
.product-additional-info | .product-additional-info, .js-product-additional-info | Información adicional |
#product-details | #product-details, .js-product-details | Detalles/ficha técnica |
.product-flags | .product-flags, .js-product-flags | Etiquetas (nuevo, oferta, etc.) |
Selectores de Listado/Catálogo
Selector 1.7.X | Selector 8.2 | Contexto de uso |
---|---|---|
.quick-view | .quick-view, .js-quick-view | Botón de vista rápida |
Selectores de Checkout
Selector 1.7.X | Selector 8.2 | Contexto de uso |
---|---|---|
.checkout-step form | .checkout-step form | Formularios del checkout |
.js-current-step | .js-current-step | Paso actual del checkout |
.checkout-step | .checkout-step | Contenedor de pasos |
.step-title | .step-title, .js-step-title | Título del paso |
#payment-confirmation button | #payment-confirmation button, .js-payment-confirmation | Botón confirmar pago |
#conditions-to-approve input[type="checkbox"] | #conditions-to-approve input[type="checkbox"], .js-conditions-to-approve | Checkbox de condiciones |
— | .js-alert-payment-conditions | Alerta de condiciones de pago |
— | .js-additional-information | Información adicional |
— | .js-payment-option-form | Formulario 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-binary | Opciones de pago binarias |
Selectores de Envío (Checkout)
Selector 1.7.X | Selector 8.2 | Contexto de uso |
---|---|---|
#js-delivery | #js-delivery | Formulario de envío |
#js-checkout-summary | #js-checkout-summary | Resumen del checkout |
#checkout-delivery-step | #checkout-delivery-step | Paso de envío |
— | .js-edit-delivery | Botón editar envío |
.delivery-option | .delivery-option, .js-delivery-option | Opciones de envío |
— | .js-cart-payment-step-refresh | Actualizar paso de pago |
Selectores de direcciones (Checkout)
Selector 1.7.X | Selector 8.2 | Contexto de uso |
---|---|---|
— | .js-edit-addresses | Editar 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-item | Item de dirección |
#checkout-addresses-step | #checkout-addresses-step | Paso 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-error | Error en dirección |
#not-valid-addresses | #not-valid-addresses, .js-not-valid-addresses | Direcciones no válidas |
#invoice-addresses | #invoice-addresses, .js-address-selector | Direcciones de facturación |
— | .js-address-form | Formulario de dirección |
Selectores de Carrito
Selector 1.7.X | Selector 8.2 | Contexto de uso |
---|---|---|
.cart-detailed-totals | .cart-detailed-totals, .js-cart-detailed-totals | Totales detallados |
.cart-summary-items-subtotal | .cart-summary-items-subtotal, .js-cart-summary-items-subtotal | Subtotal de items |
.cart-summary-subtotals-container | .cart-summary-subtotals-container, .js-cart-summary-subtotals-container | Contenedor de subtotales |
.cart-summary-totals | .cart-summary-totals, .js-cart-summary-totals | Totales del resumen |
.cart-summary-products | .cart-summary-products, .js-cart-summary-products | Productos del resumen |
.cart-detailed-actions | .cart-detailed-actions, .js-cart-detailed-actions | Acciones detalladas |
.cart-voucher | .cart-voucher, .js-cart-voucher | Cupones/vales descuento |
.cart-overview | .cart-overview | Vista general del carrito |
.cart-summary-top | .cart-summary-top, .js-cart-summary-top | Parte superior del resumen |
#product_customization_id | #product_customization_id, .js-product-customization-id | ID personalización en carrito |
— | .js-cart-line-product-quantity | Cantidad 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.
- Tienes que actualizar la configuración de la búsqueda facetada
- Comprobar que tienes el Hook colum left activos en ambos controladores.
- Ir a la configuración de páginas y marcar a dos columnas manufacturer y search
Problemas que SEGURO encontrarás
- «Undefined index» everywhere: Wrap con
{if !empty()}
- El carrito no se actualiza: Revisa los selectores JS
- Las imágenes no cargan en Safari: Orden de sources incorrecto
- Búsqueda facetada muestra todo del mismo color: Usaste
$product.cover
- 500 Internal Server Error al hacer login: No creaste registration.tpl
- Los formularios no validan: No implementaste la validación de contraseñas
- «No template found»: Crea los templates que faltan
- 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.