Un formulario de contacto no tiene por qué parecer un formulario de contacto.
Y tampoco necesitas instalar un constructor de formularios de 200 KB, tres librerías JavaScript y media docena de dependencias para preguntar al usuario qué servicio necesita, cuál es su presupuesto y cuándo quiere empezar.
Si trabajas con Joomla, el propio com_contact ya trae buena parte de lo que necesitas.
Con un Override de plantilla, los Campos personalizados nativos y unas pocas líneas de JavaScript vanilla, podemos transformar el formulario de contacto de Joomla en un pequeño cotizador multi-paso.
Sin modificar el núcleo.
Sin depender de un constructor externo.
Y sin convertir una página sencilla en una fiesta de peticiones HTTP.
Qué vamos a conseguir
La idea es sencilla.
En lugar de mostrar al visitante un formulario enorme con todos los campos de golpe, dividimos la experiencia en varios pasos.
Por ejemplo:
- Paso 1: datos de contacto.
- Paso 2: información del proyecto y presupuesto.
- Paso 3: detalles finales y envío.
El visitante ve únicamente la información que necesita en cada momento.
Esto tiene una ventaja psicológica bastante interesante: rellenar tres campos parece mucho más sencillo que rellenar nueve, aunque al final sean exactamente los mismos.
Y ahí empieza la magia de un buen formulario.
No estamos cambiando el funcionamiento interno de com_contact. Estamos cambiando la forma en la que el usuario interactúa con él.
Por qué usar un Override de com_contact
Joomla permite modificar la salida de componentes mediante Overrides de plantilla. La idea consiste en copiar el archivo de presentación a la carpeta html de la plantilla y modificar esa copia, dejando intacto el código original del componente.
La estructura que nos interesa es:
/templates/tu-plantilla/html/com_contact/contact/default_form.php
El archivo original se encuentra actualmente en:
/components/com_contact/tmpl/contact/default_form.php
En Joomla 5.4 y 6.1, ese archivo ya utiliza la API de formularios de Joomla y el WebAssetManager, y recorre los fieldsets mediante $this->form->getFieldsets().
Esto es importante porque no vamos a reconstruir el formulario desde cero.
Vamos a reutilizar el formulario que Joomla ya conoce.
Crear el Override de com_contact
Cuidado si estás en Joomla 6.1.3 que hay un bug en la creación de override desde el panel de administración. Esto se corrige en la versión 6.1.4
La forma más sencilla es entrar en:
Sistema → Plantillas del sitio → tu plantilla → Crear modificaciones
Joomla permite crear un Override desde el propio administrador y coloca automáticamente los archivos en la estructura adecuada.
También puedes crear las carpetas manualmente:
templates/tu-plantilla/html/com_contact/contact/
Y copiar:
components/com_contact/tmpl/contact/default_form.php
como:
templates/tu-plantilla/html/com_contact/contact/default_form.php
A partir de ese momento, trabajaremos sobre nuestro archivo.
El núcleo queda intacto.
Eso significa que una actualización de Joomla no debería machacar directamente nuestros cambios.
Eso sí: un Override también requiere mantenimiento. Si Joomla cambia de forma importante la estructura del archivo original, conviene comparar nuestro Override con la nueva versión del núcleo.
Cómo funciona realmente el formulario nativo de Joomla
Aquí está una de las claves del sistema.
El formulario de com_contact no está formado por un montón de HTML escrito a mano.
Joomla obtiene los campos desde su objeto Form y los renderiza mediante:
$this->form->getFieldsets()
y:
$field->renderField()
La implementación actual del componente sigue precisamente este patrón.
Eso nos permite hacer algo muy interesante.
Podemos decidir en qué paso aparece cada campo sin tener que recrear su HTML.
El propio Joomla seguirá generando etiquetas, inputs, selects, validaciones y estructuras asociadas a cada campo.
Campos personalizados de Joomla para convertir el contacto en una cotizador
Aquí es donde el sistema empieza a ponerse interesante.
Joomla incluye Campos personalizados para diferentes partes del CMS. En el componente de Contactos podemos crear campos asociados a la vista de contacto y también campos destinados al formulario de correo.
Por ejemplo, podríamos crear:
- Servicio solicitad
- Presupuesto aproximado
- Tipo de proyecto
- Fecha prevista de inicio
- Número de páginas
- Necesita mantenimiento
No necesitamos instalar un constructor de formularios para cada pregunta.
Los campos forman parte del ecosistema nativo de Joomla.
Un ejemplo de estructura para nuestro cotizador
Podemos plantearlo así:
Paso 1 Datos de contacto
- Nombre
Paso 2 Sobre el proyecto
- Servicio
- Presupuesto
- Fecha de inicio
Paso 3 Detalles
- Asunto
- Mensaje
- Captcha
- Enviar
El visitante no tiene que enfrentarse a todo el formulario de golpe.
Y nosotros tampoco tenemos que mantener un monstruo de JavaScript.
La parte importante: separar los campos por pasos
Una opción muy cómoda consiste en establecer una pequeña convención para nuestros Campos personalizados.
Por ejemplo:
- step2_servicio
step2_presupuesto
step2_fecha
El prefijo indica en qué paso queremos mostrar el campo.
Los campos nativos de com_contact, que ya conocemos, los asignamos manualmente:
- contact_name → paso 1
contact_email → paso 1
contact_subject → paso 3
contact_message → paso 3
contact_email_copy → paso 3
captcha → paso 3
De esta forma, si mañana añadimos:
- step2_tipo_empresa
el Override puede detectar automáticamente que pertenece al segundo paso.
Este es un ejemplo funcional que mantiene la lógica nativa de Joomla y añade la navegación multi-paso.
Sustituye los nombres de los campos personalizados por los que hayas creado en tu instalación.
<?php
/**
* @package Joomla.Site
* @subpackage com_contact
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
/** @var \Joomla\Component\Contact\Site\View\Contact\HtmlView $this */
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $this->getDocument()->getWebAssetManager();
$wa->useScript('keepalive')
->useScript('form.validate');
/*
* Campos nativos de com_contact.
*/
$coreFields = [
'contact_name' => 1,
'contact_email' => 1,
'contact_subject' => 3,
'contact_message' => 3,
'contact_email_copy' => 3,
'captcha' => 3,
];
/*
* Devuelve el paso de un campo.
*
* Los campos personalizados pueden utilizar el prefijo:
* step1_
* step2_
* step3_
*
* Ejemplo:
* step2_presupuesto
*/
$getStep = static function ($field) use ($coreFields) {
$name = $field->fieldname;
if (isset($coreFields[$name])) {
return $coreFields[$name];
}
if (preg_match('/^step([1-3])_/', $name, $matches)) {
return (int) $matches[1];
}
return 1;
};
$steps = [
1 => [],
2 => [],
3 => [],
];
foreach ($this->form->getFieldsets() as $fieldset) {
if ($fieldset->name === 'captcha' && $this->captchaEnabled) {
$fields = $this->form->getFieldset($fieldset->name);
} else {
$fields = $this->form->getFieldset($fieldset->name);
}
foreach ($fields as $field) {
$step = $getStep($field);
if (!isset($steps[$step])) {
$steps[$step] = [];
}
$steps[$step][] = $field;
}
}
?>
<div class="com-contact__form contact-form cotizador">
<div class="cotizador__progress" aria-label="Progreso del formulario">
<span class="cotizador__progress-item is-active" data-progress="1">
<span>1</span>
Datos
</span>
<span class="cotizador__progress-item" data-progress="2">
<span>2</span>
Proyecto
</span>
<span class="cotizador__progress-item" data-progress="3">
<span>3</span>
Envío
</span>
</div>
<form
id="contact-form"
action="<?php echo Route::_('index.php'); ?>"
method="post"
class="form-validate form-horizontal well"
>
<?php foreach ($steps as $stepNumber => $fields) : ?>
<section
class="cotizador__step"
data-step="<?php echo $stepNumber; ?>"
<?php echo $stepNumber !== 1 ? 'hidden' : ''; ?>
aria-labelledby="cotizador-step-<?php echo $stepNumber; ?>"
>
<h2 id="cotizador-step-<?php echo $stepNumber; ?>">
<?php
echo match ($stepNumber) {
1 => 'Cuéntanos quién eres',
2 => 'Háblanos del proyecto',
3 => 'Últimos detalles',
default => 'Información',
};
?>
</h2>
<?php foreach ($fields as $field) : ?>
<?php if (
$field->fieldname === 'captcha'
&& !$this->captchaEnabled
) : ?>
<?php continue; ?>
<?php endif; ?>
<?php echo $field->renderField(); ?>
<?php endforeach; ?>
<div class="cotizador__actions">
<?php if ($stepNumber > 1) : ?>
<button
type="button"
class="btn btn-secondary cotizador__prev"
data-prev
>
Atrás
</button>
<?php endif; ?>
<?php if ($stepNumber < 3) : ?>
<button
type="button"
class="btn btn-primary cotizador__next"
data-next
>
Continuar
</button>
<?php else : ?>
<button
class="btn btn-primary validate cotizador__submit"
type="submit"
>
<?php echo Text::_('COM_CONTACT_CONTACT_SEND'); ?>
</button>
<?php endif; ?>
</div>
</section>
<?php endforeach; ?>
<input
type="hidden"
name="option"
value="com_contact"
>
<input
type="hidden"
name="task"
value="contact.submit"
>
<input
type="hidden"
name="return"
value="<?php echo $this->return_page; ?>"
>
<input
type="hidden"
name="id"
value="<?php echo $this->item->slug; ?>"
>
<?php echo HTMLHelper::_('form.token'); ?>
</form>
</div>
<style>
.cotizador {
max-width: 760px;
margin-inline: auto;
}
.cotizador__progress {
display: flex;
justify-content: space-between;
gap: 1rem;
margin-bottom: 2rem;
}
.cotizador__progress-item {
flex: 1;
text-align: center;
font-size: .9rem;
opacity: .55;
}
.cotizador__progress-item span {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 50%;
border: 1px solid currentColor;
margin-right: .35rem;
}
.cotizador__progress-item.is-active {
opacity: 1;
font-weight: 700;
}
.cotizador__step[hidden] {
display: none;
}
.cotizador__step h2 {
margin-bottom: 1.5rem;
}
.cotizador__actions {
display: flex;
justify-content: space-between;
gap: 1rem;
margin-top: 2rem;
}
.cotizador__next {
margin-left: auto;
}
@media (max-width: 600px) {
.cotizador__progress {
gap: .35rem;
}
.cotizador__progress-item {
font-size: .75rem;
}
.cotizador__progress-item span {
display: flex;
margin: 0 auto .25rem;
}
}
</style>
<script>
document.addEventListener('DOMContentLoaded', () => {
const form = document.querySelector('#contact-form');
if (!form) {
return;
}
const steps = [
...form.querySelectorAll('.cotizador__step')
];
const progressItems = [
...document.querySelectorAll('.cotizador__progress-item')
];
let currentStep = 1;
const showStep = (step) => {
currentStep = step;
steps.forEach((element) => {
const isCurrent =
Number(element.dataset.step) === step;
element.hidden = !isCurrent;
});
progressItems.forEach((element) => {
const progress =
Number(element.dataset.progress);
const isCurrent = progress === step;
element.classList.toggle(
'is-active',
isCurrent
);
if (isCurrent) {
element.setAttribute(
'aria-current',
'step'
);
} else {
element.removeAttribute(
'aria-current'
);
}
});
const current = steps.find(
(element) =>
Number(element.dataset.step) === step
);
if (current) {
const firstField = current.querySelector(
'input:not([type="hidden"]), select, textarea, button'
);
if (firstField) {
firstField.focus();
}
}
window.scrollTo({
top:
form.getBoundingClientRect().top
+ window.scrollY
- 30,
behavior: 'smooth'
});
};
const validateCurrentStep = () => {
const current = steps.find(
(element) =>
Number(element.dataset.step) === currentStep
);
if (!current) {
return true;
}
const fields = [
...current.querySelectorAll(
'input:not([type="hidden"]), select, textarea'
)
];
for (const field of fields) {
if (!field.checkValidity()) {
field.reportValidity();
return false;
}
}
return true;
};
form.addEventListener('click', (event) => {
const nextButton =
event.target.closest('[data-next]');
if (nextButton) {
if (
validateCurrentStep()
&& currentStep < steps.length
) {
showStep(currentStep + 1);
}
return;
}
const prevButton =
event.target.closest('[data-prev]');
if (prevButton && currentStep > 1) {
showStep(currentStep - 1);
}
});
showStep(1);
});
</script>
La clave está en que el JavaScript no se encarga de validar el formulario completo.
Solo comprueba los campos del paso actual cuando el visitante pulsa Continuar.
La validación nativa de HTML sigue existiendo y el envío final continúa pasando por el formulario de Joomla. La API de validación del navegador permite comprobar campos mediante checkValidity() y mostrar el error correspondiente con reportValidity().
Cómo crear los campos personalizados
En el administrador de Joomla, entra en:
Componentes → Contactos → Campos
Cuando crees un campo para el formulario de correo, selecciona la vista Mail (Correo electrónico). Joomla contempla específicamente esta posibilidad dentro de los Campos personalizados de Contactos.
Por ejemplo:
- Nombre: step2_servicio
Etiqueta: ¿Qué servicio necesitas?
Tipo: Lista
Otro:
- Nombre: step2_presupuesto
Etiqueta: ¿Cuál es tu presupuesto aproximado?
Tipo: Lista
Y otro:
- Nombre: step2_fecha
Etiqueta: ¿Cuándo quieres comenzar?
Tipo: Fecha
El prefijo step2_ hará que nuestro Override los coloque automáticamente en el segundo paso.
Importante: una vez creado cada campo, tienes que ir a la pestaña de permisos para permitir que el grupo de usuario Public pueda "Editar valor de campo personalizado".

¿Y si quiero cuatro pasos?
No hay ningún problema.
El concepto puede ampliarse fácilmente.
Por ejemplo:
$steps = [ 1 => [], 2 => [], 3 => [], 4 => [],];
Y ampliar la expresión que detecta el prefijo:
if (preg_match('/^step([1-4])_/', $name, $matches)) { return (int) $matches[1];}
También tendrás que añadir el cuarto indicador de progreso y el botón correspondiente.
La idea importante es que el número de pasos no está limitado por com_contact.
Una mejora todavía más interesante: mostrar campos según respuestas
Aquí podemos llevar el concepto un poco más lejos.
Imagina que en el segundo paso preguntas:
¿Qué tipo de proyecto necesitas?
Y tienes:
- Web corporativa
Tienda online
Portal Otro
Puedes hacer que aparezcan preguntas diferentes dependiendo de la respuesta.
Por ejemplo, si el usuario selecciona:
- Tienda online
podemos mostrar:
- Número aproximado de productos
¿Necesitas integración con un ERP?
¿Necesitas pasarela de pago?
Para esto podemos apoyarnos en las capacidades de Joomla para mostrar campos condicionalmente y en JavaScript para controlar la experiencia en el frontend.
Joomla 5.2 introdujo mejoras relacionadas con la visualización condicional de campos en función de determinadas configuraciones globales.
No hace falta convertirlo en una aplicación SPA.
Un poco de JavaScript bien planteado suele ser suficiente.
¿Qué pasa con la validación?
Este punto es especialmente importante.
Un formulario multi-paso no debe convertirse en un formulario que permita avanzar alegremente hasta el último paso y descubrir al final que faltan cinco campos obligatorios.
Por eso comprobamos cada paso antes de avanzar.
La lógica es:
- Usuario pulsa Continuar
- Validamos los campos del paso actual
- ¿Todo correcto?
- Sí → Siguiente
- No → Mostrar error
Esto aprovecha la validación nativa del navegador.
Pero hay una segunda capa que no debemos olvidar.
La validación del servidor sigue siendo necesaria.
El navegador no es una barrera de seguridad. Un usuario puede manipular el HTML, desactivar JavaScript o enviar directamente una petición HTTP.
La validación del lado cliente mejora la experiencia.
La validación del servidor protege el procesamiento de los datos.
Y aquí tenemos una ventaja importante: com_contact ya tiene sus propias reglas de validación.
Nuestro Override modifica la presentación.
No estamos sustituyendo el controlador de Joomla.
El CAPTCHA también puede quedarse
Si tienes CAPTCHA configurado en com_contact, no necesitas eliminarlo para crear el cotizador.
El archivo original de Joomla ya contempla el campo CAPTCHA y lo renderiza mediante el formulario cuando está habilitado.
En nuestro ejemplo lo hemos enviado al último paso.
Tiene bastante sentido.
Primero dejamos que el usuario avance por el cuestionario y, cuando ya ha decidido enviar la información, hacemos la comprobación antispam.
Eso sí, prueba siempre el comportamiento concreto del proveedor CAPTCHA que tengas instalado.
Por qué esta solución puede ser más ligera
Aquí conviene evitar una afirmación demasiado alegre.
Quitar un plugin no garantiza automáticamente mejores Core Web Vitals.
Pero si el constructor de formularios estaba cargando JavaScript, CSS, librerías externas, fuentes, peticiones adicionales o código que realmente no necesitabas, eliminar esa carga puede reducir trabajo para el navegador.
Google mide actualmente tres Core Web Vitals principales:
- LCP, relacionado con la carga.
- INP, relacionado con la capacidad de respuesta.
- CLS, relacionado con la estabilidad visual.
Los objetivos considerados buenos son, respectivamente, ≤ 2,5 segundos, ≤ 200 ms y ≤ 0,1 en el percentil 75.
Especialmente interesante es INP.
Un constructor que añada demasiado JavaScript puede aumentar el trabajo que tiene que realizar el navegador durante la carga y las interacciones. Google señala precisamente que una gran cantidad de procesamiento JavaScript puede afectar a la capacidad de respuesta.
Por eso la pregunta correcta no es:
"¿Los plugins son malos?"
La pregunta correcta es:
"¿Qué código estoy cargando y cuánto de él necesito realmente?"
Si para conseguir cinco campos de un formulario cargas una pequeña fábrica de JavaScript, quizá haya una solución más sencilla.
El impacto en la conversión puede ser todavía más interesante
Aquí entra la parte que muchas veces olvidamos.
La velocidad importa.
Pero la fricción también.
Un formulario con diez campos visibles puede generar una sensación de esfuerzo antes de que el usuario haya escrito una sola letra.
Un formulario que dice:
Paso 1 de 3
resulta psicológicamente más manejable.
El visitante sabe cuánto le queda.
Puede centrarse en una pregunta cada vez.
Y, sobre todo, no tiene que enfrentarse a una pared de campos.
Esto no significa que cualquier formulario multi-paso vaya a convertir mejor.
Si divides una pregunta sencilla en siete pantallas, probablemente hayas conseguido justo lo contrario.
El número de pasos debe responder a la complejidad real de la información que necesitas.
Un truco de conversión que merece la pena probar
No empieces pidiendo el presupuesto.
Empieza por lo fácil.
Por ejemplo:
- Paso 1
¿Cómo te llamas?
¿Cuál es tu email?
Después:
- Paso 2
¿Qué necesitas?
¿Cuándo quieres empezar?
¿Cuál es tu presupuesto?
Y finalmente:
- Paso 3
Cuéntanos algo más sobre el proyecto.
Enviar solicitud.
El visitante ya ha invertido tiempo en el formulario.
Pero cuidado con una mala práctica muy habitual.
No conviertas el primer paso en una barrera de entrada.
Si preguntas por teléfono, empresa, dirección, CIF, sector, presupuesto, número de empleados y fecha de nacimiento antes de explicar qué va a recibir el usuario, probablemente tengas un problema de conversión.
El formulario tiene que pedir información porque la necesita.
No porque el campo exista.
Qué ocurre cuando Joomla recibe el formulario
Este punto es fundamental para entender por qué el Override funciona tan bien.
El formulario continúa enviándose a:
option=com_contacttask=contact.submit
El Override no inventa un nuevo endpoint.
Tampoco crea una nueva lógica de envío.
Simplemente cambia la presentación del formulario.
El archivo original de Joomla utiliza precisamente estos campos ocultos para indicar el componente, la tarea, la página de retorno y el contacto, además del token CSRF.
Por eso debemos mantenerlos.
Si los eliminas porque "son campos ocultos que no necesito", Joomla puede dejar de saber qué tiene que hacer con el formulario.
Y entonces sí tendremos un problema.
Qué no deberías hacer
Hay varias tentaciones cuando empiezas con Overrides.
La primera es copiar todo el componente.
No hace falta.
La segunda es modificar directamente:
/components/com_contact/
Tampoco.
La tercera es añadir un framework JavaScript gigantesco para controlar tres botones.
Tampoco hace falta.
La cuarta es confiar exclusivamente en JavaScript para validar los datos.
Mala idea.
Y la quinta es pensar que un Override es inmune a las actualizaciones.
No lo es.
Cómo mantener el Override después de actualizar Joomla
Antes de actualizar Joomla, revisa siempre si el archivo original:
components/com_contact/tmpl/contact/default_form.php
ha cambiado.
Nuestro Override parte de esa estructura.
Si Joomla modifica el formulario, añade nuevos campos o cambia la forma en que carga recursos, conviene comparar ambos archivos.
La ventaja es que el cambio está aislado.
No has tocado el núcleo.
Eso hace que el mantenimiento sea mucho más razonable.
¿Joomla 5 o Joomla 6?
La técnica es válida para las ramas modernas de Joomla que mantienen esta estructura de com_contact.
A fecha de agosto de 2026, la versión estable más reciente es Joomla 6.1.3, publicada el 18 de agosto de 2026. Joomla 5.4.8 también está disponible como versión de mantenimiento.
El archivo default_form.php de las ramas 5.4 y 6.1 mantiene la estructura basada en $this->form, fieldsets y renderField(), por lo que el enfoque mostrado aquí encaja con ambas versiones.
Aun así, prueba siempre el Override en una copia de desarrollo antes de llevarlo a producción.
Una arquitectura sencilla, pero con mucho recorrido
Lo interesante de este enfoque es que podemos empezar con algo muy pequeño:
com_contact + Override + Custom Fields + JavaScript vanilla
Y desde ahí construir algo bastante sofisticado.
Un cotizador.
Un cuestionario.
Un formulario de solicitud de presupuesto.
Un proceso de captación de leads.
Un configurador de servicios.
Todo ello manteniendo Joomla como responsable del formulario y dejando que el Override se encargue de la experiencia.
La idea clave
Antes de instalar un nuevo componente, plugin o constructor, mira lo que ya tienes.
Joomla lleva años incorporando herramientas que muchas veces pasan desapercibidas.
Los Overrides permiten modificar la salida sin tocar el núcleo.
Los Campos personalizados permiten añadir información específica.
El sistema de formularios se encarga de buena parte del trabajo pesado.
Y JavaScript vanilla puede resolver la interacción multi-paso con muy poco código.
No necesitas montar una central nuclear para pedir tres datos.
A veces solo necesitas conocer bien Joomla.
Checklist final
Antes de publicar tu cotizador multi-paso, comprueba:
Checklist de revisión
- ☐ El Override está dentro de
/templates/tu-plantilla/html/ - ☐ No has modificado
/components/com_contact/ - ☐ Los campos personalizados están configurados para Mail
- ☐ Cada campo personalizado tiene asignado su paso
- ☐ Los campos obligatorios se validan correctamente
- ☐ El CAPTCHA funciona
- ☐ El formulario llega correctamente por email
- ☐ El token CSRF sigue presente
- ☐ El formulario funciona sin JavaScript cuando sea razonable
- ☐ Se ha probado en móvil
- ☐ Se ha probado con teclado
- ☐ Se han revisado los mensajes de validación
- ☐ Se han medido Core Web Vitals antes y después
- ☐ Se ha medido la tasa de conversión
Y una última recomendación.
Guarda una copia del default_form.php original de la versión de Joomla que estés utilizando.
No porque Joomla vaya a romperlo mañana.
Sino porque cuando dentro de ocho meses estés mirando el código y pensando "¿por qué demonios hice esto así?", te alegrarás de tener una referencia.
¿Has llegado hasta aquí?
Genial!!! Te hago un pequeño resumen de lo que te acabo de contar y te muestro el resultado de cómo podría quedar el formulario.

Convertir com_contact en un cotizador multi-paso es un buen ejemplo de lo que se puede conseguir cuando dejamos de pensar que cada nueva necesidad requiere un nuevo plugin.
Con un Override bien planteado, Campos personalizados y JavaScript vanilla puedes transformar un formulario nativo de Joomla en una experiencia mucho más cómoda para el usuario.
Y lo mejor es que sigues teniendo Joomla detrás.
Sin modificar el núcleo.
Sin depender de un constructor de formularios para cada pequeño cambio.
Y con una base técnica bastante sencilla de mantener.
Porque la mejor extensión para Joomla a veces es la que no tienes que instalar.
