El post anterior de esta serie cubrió el problema: PrestaShop 8 muestra un contador en los meta basado en el maxlength de la base de datos (128/512), no en el objetivo SEO real (60/160). En este post se construye la solución desde cero. Tutorial técnico, código listo, sin dependencias raras. Al final se comenta el atajo: módulo Zeyvro Meta Counter ya empaquetado para los que prefieran no escribirlo.
Lo que hay que conseguir
- Detectar todos los campos
meta_titleymeta_descriptionque aparezcan en formularios del backoffice (Productos, Categorías, Marcas, Proveedores, CMS, etc.). - Inyectar bajo cada uno un contador con formato
SEO: X / Y. - Cambiar el color del contador según distancia al objetivo: verde si quedan más de 10 caracteres, ámbar si quedan 10 o menos, rojo si se ha pasado.
- Esconder el contador nativo de la página V2 de producto, que muestra el maxlength y resta espacio sin aportar.
- Que funcione en pestañas SEO que se cargan en lazy (la V2 de producto las carga vía Symfony tras click).
- Sin overrides agresivos al theme. Sin alterar la BD. Limpio para desinstalar.
Estructura mínima del módulo
Crear un módulo PS8 mínimo que registra un hook de medios para inyectar CSS+JS en cada página de admin. Estructura del directorio:
zeyvrometacounter/
├── zeyvrometacounter.php
├── config.xml
├── logo.png
├── views/
│ ├── css/meta-counter.css
│ └── js/meta-counter.js
└── index.php (anti-listing)
El archivo principal:
<?php
if (!defined('_PS_VERSION_')) exit;
class Zeyvrometacounter extends Module
{
public function __construct()
{
$this->name = 'zeyvrometacounter';
$this->tab = 'seo';
$this->version = '1.0.0';
$this->author = 'Zeyvro';
$this->ps_versions_compliancy = ['min' => '8.0.0', 'max' => '8.99.99'];
$this->bootstrap = true;
parent::__construct();
$this->displayName = $this->l('Zeyvro Meta Counter');
$this->description = $this->l('SEO-oriented character counter for meta fields.');
}
public function install()
{
return parent::install() && $this->registerHook('actionAdminControllerSetMedia');
}
public function hookActionAdminControllerSetMedia($params)
{
$controller = $this->context->controller;
if (!is_object($controller)) return;
$controller->addCSS($this->_path . 'views/css/meta-counter.css');
$controller->addJS($this->_path . 'views/js/meta-counter.js');
}
}
El hook actionAdminControllerSetMedia se dispara en cada controlador de admin antes de renderizar. Ahí inyectas el CSS y el JS que hagan el trabajo.
El JavaScript que detecta y cuenta
El JS necesita: (a) encontrar los campos meta en el DOM, (b) acoplarles un counter, (c) actualizar el counter al teclear, (d) volver a escanear cuando aparezcan campos nuevos por carga lazy.
(function () {
'use strict';
var LIMIT_TITLE = 60;
var LIMIT_DESC = 160;
function isMetaTitle(el) {
var k = ((el.name || '') + ' ' + (el.id || '')).toLowerCase();
return k.indexOf('meta_title') !== -1 || k.indexOf('metatitle') !== -1;
}
function isMetaDesc(el) {
var k = ((el.name || '') + ' ' + (el.id || '')).toLowerCase();
return k.indexOf('meta_description') !== -1 || k.indexOf('metadescription') !== -1;
}
function classify(el) {
if (isMetaTitle(el)) return { max: LIMIT_TITLE };
if (isMetaDesc(el)) return { max: LIMIT_DESC };
return null;
}
function findFields() {
return document.querySelectorAll(
'input[name*="meta_title"], input[id*="meta_title"], '
+ 'textarea[name*="meta_description"], textarea[id*="meta_description"], '
+ 'input[name*="meta_description"], input[id*="meta_description"]'
);
}
function attach(el) {
if (el.dataset.zvCounter === '1') return;
var info = classify(el);
if (!info) return;
el.dataset.zvCounter = '1';
var counter = document.createElement('small');
counter.className = 'zv-meta-counter';
if (el.nextSibling) {
el.parentNode.insertBefore(counter, el.nextSibling);
} else {
el.parentNode.appendChild(counter);
}
function update() {
var len = (el.value || '').length;
var left = info.max - len;
counter.textContent = 'SEO: ' + len + ' / ' + info.max;
counter.classList.remove('zv-meta-counter--ok','zv-meta-counter--warn','zv-meta-counter--over');
if (left < 0) counter.classList.add('zv-meta-counter--over');
else if (left <= 10) counter.classList.add('zv-meta-counter--warn');
else counter.classList.add('zv-meta-counter--ok');
}
el.addEventListener('input', update);
el.addEventListener('change', update);
update();
}
function scan() {
var fields = findFields();
for (var i = 0; i < fields.length; i++) attach(fields[i]);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', scan);
} else {
scan();
}
// Lazy-loaded SEO tab in V2 product form
var observer = new MutationObserver(function () { scan(); });
observer.observe(document.documentElement, { childList: true, subtree: true });
})();
Detalles a notar:
- Detección flexible: buscamos en
namey enidconindexOf. PS varía el sufijo del campo según el contexto (Symfony del V2 producto añade[1]al final, formularios legacy usanmeta_title_1, el editor Symfony de categorías otra variante). Buscar por contains cubre todos los casos. - Marcado para no duplicar:
data-zv-counter="1"evita acoplar dos counters al mismo input cuando el observer dispara más de una vez. - MutationObserver: necesario porque la V2 de producto en PS8 carga la pestaña SEO en lazy (el HTML del input no existe al cargar la página). Sin observer, el contador no aparecería en esa pestaña hasta refrescar.
- Sin jQuery: aunque PS8 BO carga jQuery, lo evitamos para que el módulo siga siendo válido cuando PS migre completamente a Symfony y elimine jQuery del admin.
El CSS que pinta el counter y oculta el nativo
.zv-meta-counter {
display: block;
margin-top: 4px;
font-size: 12px;
font-weight: 500;
line-height: 1.2;
font-variant-numeric: tabular-nums;
}
.zv-meta-counter--ok { color: #00b88a; }
.zv-meta-counter--warn { color: #F59E0B; }
.zv-meta-counter--over { color: #EF4444; }
/* Hide PS native char counter in V2 product form */
.js-text-with-length-counter .input-group-append {
display: none !important;
}
El selector .js-text-with-length-counter .input-group-append apunta al span que PS8 V2 inyecta debajo del campo cuando tiene maxlength. Ocultarlo evita que el lector vea dos contadores contradictorios — el de PS marcando 0/128 y el nuestro marcando SEO: 0/60. Si tu tienda usa el formulario legacy (no V2), ese selector no afecta a nada.
La regla font-variant-numeric: tabular-nums evita el «brincar» del counter al cambiar entre dígitos de distinto ancho. Detalle pequeño, mejora notable.
Casos que no se cubren por defecto
- Campos meta de módulos terceros que NO usen el patrón
meta_title/meta_descriptionen sunameoid. Si un módulo X mete un campo SEO con nameseo_titulo, no lo detectamos. Ampliar la regex defindFieldssegún los módulos que tengas instalados. - Multi-idioma con tabs: cada idioma renderiza su propio input. El observer pilla los nuevos cuando se cargan, pero el counter de un idioma inactivo se queda con el último valor. Solución avanzada: re-evaluar al cambiar de tab. Para v1.0.0 no es necesario porque la mayoría de SEOs revisan el meta del idioma activo y poco más.
- Frontend: este módulo NO toca el frontend. Los meta del frontend se renderizan via theme + Rank Math o equivalente — eso es otra capa.
Empaquetado y publicación
Para distribuir el módulo en una tienda PS8: zip -r zeyvrometacounter-1.0.0.zip zeyvrometacounter/ -x "zeyvrometacounter/.git/*". Subir desde Backoffice → Módulos → Subir un módulo → arrastra el zip → Instalar. Sin configuración. Sin tablas. Sin overrides.
Atajo: módulo Zeyvro Meta Counter
Si prefieres no escribir el módulo desde cero, hemos publicado Zeyvro Meta Counter v1.0.0 con la implementación descrita en este post: módulo PS8 free, MIT, descargable desde nuestra tienda. Repo en GitHub zeyvro/zeyvro-prestashop-metacounter (MIT, descarga directa en Releases).
Diferencias respecto a escribirlo tú: ahorras la curva de detección de campos en V2 producto + casos edge del MutationObserver + estructura de empaquetado limpia. El código fuente es el mismo que has visto arriba.
Si tu experiencia con campos meta de PS8 contradice algo de este tutorial o has descubierto un caso edge no listado, escríbenos a hola@zeyvro.com.
Si prefieres no montar esto a mano, el módulo ya lo hace instalado.
Zeyvro Meta Counter para PrestaShop 8 →Zeyvro desarrolla módulos para PrestaShop 8 desde una tienda real en producción. Código sin ofuscar.