Spring Boot

SpringBoot

Tutorial SpringBoot: Layouts y fragmentos en Thymeleaf

Aprende a crear y reutilizar fragmentos, usar th:replace e implementar herencia de plantillas en Thymeleaf para aplicaciones web profesionales.

Aprende SpringBoot y certifícate

Creación de fragmentos reutilizables

Los fragmentos en Thymeleaf son piezas de código HTML que podemos definir una vez y reutilizar en múltiples plantillas. Esta funcionalidad nos permite mantener nuestro código más organizado y evitar la duplicación, especialmente útil para elementos comunes como cabeceras, menús de navegación o pies de página.

Un fragmento se define utilizando el atributo th:fragment en cualquier elemento HTML. Este atributo actúa como un identificador que nos permitirá referenciar ese fragmento desde otras plantillas.

Definición básica de fragmentos

Para crear un fragmento, simplemente añadimos el atributo th:fragment a cualquier elemento HTML y le asignamos un nombre único:

<div th:fragment="mensaje-bienvenida">
    <h2>¡Bienvenido a nuestra aplicación!</h2>
    <p>Esperamos que disfrutes de tu experiencia.</p>
</div>

Este fragmento puede estar ubicado en cualquier plantilla, pero es una buena práctica crear archivos específicos para almacenar fragmentos que se van a reutilizar frecuentemente. Normalmente estos archivos se colocan en una carpeta llamada fragments dentro del directorio templates.

Estructura recomendada para fragmentos

Creemos un archivo dedicado para nuestros fragmentos comunes. En src/main/resources/templates/fragments/common.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
</head>
<body>

<!-- Fragmento para mostrar alertas -->
<div th:fragment="alerta(tipo, mensaje)" class="alert" th:classappend="'alert-' + ${tipo}">
    <span th:text="${mensaje}">Mensaje de alerta</span>
</div>

<!-- Fragmento para información de usuario -->
<div th:fragment="info-usuario">
    <div class="card">
        <div class="card-body">
            <h5 class="card-title">Información del Usuario</h5>
            <p class="card-text">Usuario conectado desde: <span th:text="${#dates.format(#dates.createNow(), 'dd/MM/yyyy HH:mm')}"></span></p>
        </div>
    </div>
</div>

</body>
</html>

Fragmentos con parámetros

Los fragmentos pueden aceptar parámetros, lo que los hace mucho más flexibles y reutilizables. En el ejemplo anterior, el fragmento alerta recibe dos parámetros: tipo y mensaje.

Veamos un ejemplo más complejo de un fragmento parametrizado para mostrar tarjetas de productos:

<div th:fragment="tarjeta-producto(nombre, precio, descripcion)" class="card mb-3">
    <div class="card-body">
        <h5 class="card-title" th:text="${nombre}">Nombre del producto</h5>
        <p class="card-text" th:text="${descripcion}">Descripción del producto</p>
        <p class="card-text">
            <strong>Precio: </strong>
            <span th:text="${#numbers.formatDecimal(precio, 1, 2)} + ' €'">0,00 €</span>
        </p>
        <button class="btn btn-primary">Añadir al carrito</button>
    </div>
</div>

Fragmentos con contenido dinámico

También podemos crear fragmentos que contengan lógica condicional o bucles, haciéndolos aún más versátiles:

<div th:fragment="lista-elementos(elementos, titulo)">
    <div class="card">
        <div class="card-header">
            <h4 th:text="${titulo}">Título de la lista</h4>
        </div>
        <div class="card-body">
            <div th:if="${#lists.isEmpty(elementos)}">
                <p class="text-muted">No hay elementos para mostrar</p>
            </div>
            <ul th:unless="${#lists.isEmpty(elementos)}" class="list-group list-group-flush">
                <li th:each="elemento : ${elementos}" 
                    th:text="${elemento}" 
                    class="list-group-item">
                    Elemento de la lista
                </li>
            </ul>
        </div>
    </div>
</div>

Organización de fragmentos por funcionalidad

Para proyectos más grandes, es recomendable organizar los fragmentos en diferentes archivos según su funcionalidad. Por ejemplo:

fragments/forms.html para fragmentos relacionados con formularios:

<div th:fragment="campo-texto(nombre, etiqueta, valor)" class="mb-3">
    <label th:for="${nombre}" class="form-label" th:text="${etiqueta}">Etiqueta</label>
    <input type="text" 
           class="form-control" 
           th:id="${nombre}" 
           th:name="${nombre}" 
           th:value="${valor}">
</div>

<div th:fragment="boton-enviar(texto)" class="mb-3">
    <button type="submit" class="btn btn-primary" th:text="${texto}">Enviar</button>
</div>

fragments/layout.html para elementos de estructura:

<head th:fragment="head-comun(titulo)">
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title th:text="${titulo}">Título de la página</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>

Esta organización modular nos permite mantener el código más limpio y facilita el mantenimiento de nuestras plantillas, especialmente cuando el proyecto crece en complejidad.

Inclusión con th:replace

Una vez que hemos definido nuestros fragmentos reutilizables, necesitamos una forma de incluirlos en nuestras plantillas. Thymeleaf nos proporciona varios atributos para esta tarea, siendo th:replace uno de los más utilizados y versátiles.

El atributo th:replace sustituye completamente el elemento que lo contiene por el fragmento especificado. Esto significa que el elemento HTML donde colocamos th:replace desaparece y es reemplazado por el contenido del fragmento.

Sintaxis básica de th:replace

La sintaxis para usar th:replace sigue el patrón archivo :: fragmento:

<div th:replace="fragments/common :: mensaje-bienvenida"></div>

En este ejemplo, el <div> será completamente reemplazado por el contenido del fragmento mensaje-bienvenida que se encuentra en el archivo fragments/common.html.

Inclusión de fragmentos sin parámetros

Supongamos que tenemos el fragmento info-usuario que creamos anteriormente. Para incluirlo en cualquier plantilla, simplemente usamos:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Mi página principal</title>
</head>
<body>
    <div class="container">
        <h1>Página Principal</h1>
        
        <!-- El div será reemplazado por el fragmento completo -->
        <div th:replace="fragments/common :: info-usuario"></div>
        
        <p>Resto del contenido de la página...</p>
    </div>
</body>
</html>

Inclusión de fragmentos con parámetros

Para fragmentos que aceptan parámetros, pasamos los valores entre paréntesis después del nombre del fragmento:

<div class="container">
    <h1>Gestión de Productos</h1>
    
    <!-- Incluir alerta de éxito -->
    <div th:replace="fragments/common :: alerta('success', 'Producto guardado correctamente')"></div>
    
    <!-- Incluir tarjeta de producto con datos específicos -->
    <div th:replace="fragments/common :: tarjeta-producto('Laptop Gaming', 1299.99, 'Potente laptop para gaming y trabajo profesional')"></div>
</div>

Pasar variables del controlador como parámetros

Los parámetros también pueden ser variables que provienen del controlador de Spring Boot:

@Controller
public class ProductoController {
    
    @GetMapping("/productos")
    public String mostrarProductos(Model model) {
        model.addAttribute("nombreProducto", "Smartphone Pro");
        model.addAttribute("precioProducto", 899.99);
        model.addAttribute("descripcionProducto", "Último modelo con cámara avanzada");
        return "productos";
    }
}

En la plantilla productos.html:

<div class="row">
    <div class="col-md-6">
        <div th:replace="fragments/common :: tarjeta-producto(${nombreProducto}, ${precioProducto}, ${descripcionProducto})"></div>
    </div>
</div>

Diferencia entre th:replace y th:insert

Es importante entender la diferencia entre th:replace y th:insert:

Con th:replace:

<!-- Plantilla original -->
<div class="mi-contenedor" th:replace="fragments/common :: mensaje-bienvenida">
    Contenido temporal
</div>

<!-- Resultado después del procesamiento -->
<div th:fragment="mensaje-bienvenida">
    <h2>¡Bienvenido a nuestra aplicación!</h2>
    <p>Esperamos que disfrutes de tu experiencia.</p>
</div>

Con th:insert:

<!-- Plantilla original -->
<div class="mi-contenedor" th:insert="fragments/common :: mensaje-bienvenida">
    Contenido temporal
</div>

<!-- Resultado después del procesamiento -->
<div class="mi-contenedor">
    <div th:fragment="mensaje-bienvenida">
        <h2>¡Bienvenido a nuestra aplicación!</h2>
        <p>Esperamos que disfrutes de tu experiencia.</p>
    </div>
</div>

Inclusión condicional de fragmentos

Podemos combinar th:replace con expresiones condicionales para incluir fragmentos solo cuando se cumplan ciertas condiciones:

<div class="alerts-container">
    <!-- Mostrar alerta solo si hay un mensaje de error -->
    <div th:if="${error}" th:replace="fragments/common :: alerta('danger', ${error})"></div>
    
    <!-- Mostrar alerta solo si hay un mensaje de éxito -->
    <div th:if="${success}" th:replace="fragments/common :: alerta('success', ${success})"></div>
</div>

Inclusión de fragmentos en bucles

También es posible usar th:replace dentro de bucles para generar contenido repetitivo:

<div class="productos-grid">
    <div th:each="producto : ${productos}" 
         th:replace="fragments/common :: tarjeta-producto(${producto.nombre}, ${producto.precio}, ${producto.descripcion})">
    </div>
</div>

Manejo de errores en inclusiones

Si intentamos incluir un fragmento que no existe, Thymeleaf lanzará una excepción. Para evitar esto en casos donde el fragmento puede no estar disponible, podemos usar el operador de navegación segura:

<!-- Si el fragmento no existe, no se incluye nada -->
<div th:replace="fragments/optional :: fragmento-opcional" th:if="${condition}"></div>

Esta funcionalidad de inclusión de fragmentos nos permite crear interfaces de usuario más modulares y mantenibles, donde cada componente puede ser desarrollado y probado de forma independiente antes de ser integrado en las páginas principales.

Herencia de plantillas

La herencia de plantillas en Thymeleaf nos permite crear una estructura base común que puede ser extendida por otras plantillas específicas. Este enfoque es especialmente útil cuando queremos mantener una estructura HTML consistente en toda nuestra aplicación, como el <head>, la navegación principal y el pie de página, mientras permitimos que cada página tenga su contenido único.

A diferencia de los fragmentos que vimos anteriormente, la herencia trabaja con plantillas completas que actúan como esqueletos o marcos base. Estas plantillas padre definen la estructura general y proporcionan puntos específicos donde las plantillas hijas pueden insertar su contenido personalizado.

Creación de una plantilla base

Comenzamos creando una plantilla base que contendrá la estructura común de nuestras páginas. Creamos el archivo src/main/resources/templates/layout/base.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:fragment="layout (titulo, contenido)">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title th:text="${titulo}">Título por defecto</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
        <div class="container">
            <a class="navbar-brand" href="/">Mi Aplicación</a>
            <div class="navbar-nav">
                <a class="nav-link" href="/">Inicio</a>
                <a class="nav-link" href="/productos">Productos</a>
                <a class="nav-link" href="/contacto">Contacto</a>
            </div>
        </div>
    </nav>

    <main class="container mt-4">
        <div th:replace="${contenido}">
            <p>Contenido por defecto</p>
        </div>
    </main>

    <footer class="bg-light text-center py-3 mt-5">
        <div class="container">
            <p>&copy; 2024 Mi Aplicación. Todos los derechos reservados.</p>
        </div>
    </footer>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

La clave de esta plantilla base está en el fragmento principal layout que acepta dos parámetros: titulo para el título de la página y contenido que será reemplazado por el contenido específico de cada página hija.

Uso de la plantilla base en páginas específicas

Ahora podemos crear páginas que hereden de esta plantilla base. Por ejemplo, creamos productos.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" 
      th:replace="layout/base :: layout('Gestión de Productos', ~{::contenido})">
<body>
    <div th:fragment="contenido">
        <div class="row">
            <div class="col-12">
                <h1>Catálogo de Productos</h1>
                <p class="lead">Explora nuestra amplia gama de productos disponibles.</p>
            </div>
        </div>
        
        <div class="row">
            <div class="col-md-4" th:each="producto : ${productos}">
                <div class="card mb-4">
                    <div class="card-body">
                        <h5 class="card-title" th:text="${producto.nombre}">Nombre del producto</h5>
                        <p class="card-text" th:text="${producto.descripcion}">Descripción</p>
                        <p class="text-primary fw-bold" th:text="${producto.precio} + ' €'">Precio</p>
                        <button class="btn btn-primary">Ver detalles</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

Sintaxis de herencia con expresiones de fragmento

La expresión clave en la herencia es ~{::contenido}, que le dice a Thymeleaf que tome el fragmento llamado contenido de la plantilla actual y lo pase como parámetro a la plantilla base. Esta sintaxis especial permite que el contenido específico de cada página se inserte en el lugar apropiado de la plantilla padre.

Veamos otro ejemplo con una página de contacto más simple:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" 
      th:replace="layout/base :: layout('Contacto', ~{::contenido})">
<body>
    <div th:fragment="contenido">
        <h1>Contáctanos</h1>
        <div class="row">
            <div class="col-md-8">
                <form>
                    <div class="mb-3">
                        <label for="nombre" class="form-label">Nombre</label>
                        <input type="text" class="form-control" id="nombre" name="nombre">
                    </div>
                    <div class="mb-3">
                        <label for="email" class="form-label">Email</label>
                        <input type="email" class="form-control" id="email" name="email">
                    </div>
                    <div class="mb-3">
                        <label for="mensaje" class="form-label">Mensaje</label>
                        <textarea class="form-control" id="mensaje" name="mensaje" rows="5"></textarea>
                    </div>
                    <button type="submit" class="btn btn-primary">Enviar mensaje</button>
                </form>
            </div>
            <div class="col-md-4">
                <h4>Información de contacto</h4>
                <p><strong>Teléfono:</strong> +34 123 456 789</p>
                <p><strong>Email:</strong> info@miapp.com</p>
                <p><strong>Dirección:</strong> Calle Principal, 123</p>
            </div>
        </div>
    </div>
</body>
</html>

Plantillas base con múltiples secciones

Podemos crear plantillas base más sofisticadas que permitan personalizar múltiples secciones. Por ejemplo, una plantilla que permita personalizar tanto el contenido principal como una barra lateral:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:fragment="layout-con-sidebar (titulo, contenido, sidebar)">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title th:text="${titulo}">Mi Aplicación</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
        <div class="container-fluid">
            <a class="navbar-brand" href="/">Mi Aplicación</a>
        </div>
    </nav>

    <div class="container-fluid mt-4">
        <div class="row">
            <div class="col-md-9">
                <main th:replace="${contenido}">
                    <p>Contenido principal por defecto</p>
                </main>
            </div>
            <div class="col-md-3">
                <aside th:replace="${sidebar}">
                    <p>Barra lateral por defecto</p>
                </aside>
            </div>
        </div>
    </div>
</body>
</html>

Para usar esta plantilla con sidebar:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" 
      th:replace="layout/base-sidebar :: layout-con-sidebar('Dashboard', ~{::main-content}, ~{::sidebar-content})">
<body>
    <div th:fragment="main-content">
        <h1>Panel de Control</h1>
        <div class="row">
            <div class="col-md-6">
                <div class="card">
                    <div class="card-body">
                        <h5 class="card-title">Estadísticas</h5>
                        <p class="card-text">Resumen de actividad reciente</p>
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    <div th:fragment="sidebar-content">
        <div class="card">
            <div class="card-header">
                <h6>Acciones rápidas</h6>
            </div>
            <div class="card-body">
                <a href="/nuevo-producto" class="btn btn-sm btn-primary d-block mb-2">Nuevo Producto</a>
                <a href="/reportes" class="btn btn-sm btn-secondary d-block">Ver Reportes</a>
            </div>
        </div>
    </div>
</body>
</html>

Ventajas de la herencia de plantillas

La herencia de plantillas nos proporciona beneficios significativos:

  • Consistencia visual: Todas las páginas mantienen la misma estructura base, navegación y estilos
  • Mantenimiento centralizado: Los cambios en la estructura común se aplican automáticamente a todas las páginas
  • Desarrollo más rápido: Las nuevas páginas se crean más rápidamente al heredar la estructura base
  • Separación de responsabilidades: El diseño general se separa del contenido específico de cada página

Esta aproximación de herencia complementa perfectamente el uso de fragmentos, permitiendo crear aplicaciones web con una arquitectura de plantillas robusta y mantenible.

Ahora que conocemos los conceptos de fragmentos y herencia de plantillas, vamos a aplicarlos de forma práctica creando una barra de navegación y un footer con Bootstrap que sean comunes a todas las páginas de nuestra aplicación. Esta implementación nos permitirá mantener una interfaz consistente y profesional en todo el sitio web.

Creación del fragmento de navegación

Comenzamos creando un archivo específico para los componentes de navegación en src/main/resources/templates/fragments/navigation.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
</head>
<body>

<nav th:fragment="navbar" class="navbar navbar-expand-lg navbar-dark bg-dark shadow">
    <div class="container">
        <a class="navbar-brand fw-bold" href="/" th:href="@{/}">
            <i class="bi bi-house-door me-2"></i>MiApp
        </a>
        
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" 
                data-bs-target="#navbarNav" aria-controls="navbarNav" 
                aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        
        <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav me-auto">
                <li class="nav-item">
                    <a class="nav-link" href="/" th:href="@{/}" 
                       th:classappend="${#request.requestURI == '/' ? 'active' : ''}">
                        Inicio
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/productos" th:href="@{/productos}"
                       th:classappend="${#strings.startsWith(#request.requestURI, '/productos') ? 'active' : ''}">
                        Productos
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/servicios" th:href="@{/servicios}"
                       th:classappend="${#strings.startsWith(#request.requestURI, '/servicios') ? 'active' : ''}">
                        Servicios
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/contacto" th:href="@{/contacto}"
                       th:classappend="${#request.requestURI == '/contacto' ? 'active' : ''}">
                        Contacto
                    </a>
                </li>
            </ul>
            
            <ul class="navbar-nav">
                <li class="nav-item dropdown">
                    <a class="nav-link dropdown-toggle" href="#" role="button" 
                       data-bs-toggle="dropdown" aria-expanded="false">
                        <i class="bi bi-person-circle me-1"></i>Usuario
                    </a>
                    <ul class="dropdown-menu">
                        <li><a class="dropdown-item" href="/perfil" th:href="@{/perfil}">Mi Perfil</a></li>
                        <li><a class="dropdown-item" href="/configuracion" th:href="@{/configuracion}">Configuración</a></li>
                        <li><hr class="dropdown-divider"></li>
                        <li><a class="dropdown-item" href="/logout" th:href="@{/logout}">Cerrar Sesión</a></li>
                    </ul>
                </li>
            </ul>
        </div>
    </div>
</nav>

</body>
</html>

Creación del fragmento de footer

En el mismo archivo navigation.html, añadimos el fragmento del footer:

<footer th:fragment="footer" class="bg-dark text-light mt-5">
    <div class="container py-4">
        <div class="row">
            <div class="col-md-4 mb-3">
                <h5 class="fw-bold">MiApp</h5>
                <p class="text-muted">
                    Soluciones innovadoras para tu negocio. 
                    Comprometidos con la excelencia y la satisfacción del cliente.
                </p>
                <div class="d-flex gap-3">
                    <a href="#" class="text-light"><i class="bi bi-facebook"></i></a>
                    <a href="#" class="text-light"><i class="bi bi-twitter"></i></a>
                    <a href="#" class="text-light"><i class="bi bi-linkedin"></i></a>
                    <a href="#" class="text-light"><i class="bi bi-instagram"></i></a>
                </div>
            </div>
            
            <div class="col-md-2 mb-3">
                <h6 class="fw-bold">Navegación</h6>
                <ul class="list-unstyled">
                    <li><a href="/" th:href="@{/}" class="text-muted text-decoration-none">Inicio</a></li>
                    <li><a href="/productos" th:href="@{/productos}" class="text-muted text-decoration-none">Productos</a></li>
                    <li><a href="/servicios" th:href="@{/servicios}" class="text-muted text-decoration-none">Servicios</a></li>
                    <li><a href="/contacto" th:href="@{/contacto}" class="text-muted text-decoration-none">Contacto</a></li>
                </ul>
            </div>
            
            <div class="col-md-3 mb-3">
                <h6 class="fw-bold">Información</h6>
                <ul class="list-unstyled">
                    <li><a href="/sobre-nosotros" th:href="@{/sobre-nosotros}" class="text-muted text-decoration-none">Sobre Nosotros</a></li>
                    <li><a href="/politica-privacidad" th:href="@{/politica-privacidad}" class="text-muted text-decoration-none">Política de Privacidad</a></li>
                    <li><a href="/terminos" th:href="@{/terminos}" class="text-muted text-decoration-none">Términos de Uso</a></li>
                </ul>
            </div>
            
            <div class="col-md-3 mb-3">
                <h6 class="fw-bold">Contacto</h6>
                <div class="text-muted">
                    <p class="mb-1"><i class="bi bi-geo-alt me-2"></i>Calle Principal, 123</p>
                    <p class="mb-1"><i class="bi bi-telephone me-2"></i>+34 123 456 789</p>
                    <p class="mb-1"><i class="bi bi-envelope me-2"></i>info@miapp.com</p>
                </div>
            </div>
        </div>
        
        <hr class="my-4">
        
        <div class="row align-items-center">
            <div class="col-md-6">
                <p class="text-muted mb-0">
                    &copy; <span th:text="${#dates.year(#dates.createNow())}">2024</span> MiApp. 
                    Todos los derechos reservados.
                </p>
            </div>
            <div class="col-md-6 text-md-end">
                <p class="text-muted mb-0">
                    Desarrollado con <i class="bi bi-heart-fill text-danger"></i> 
                    usando Spring Boot y Thymeleaf
                </p>
            </div>
        </div>
    </div>
</footer>

Plantilla base completa con navegación y footer

Ahora creamos una plantilla base que integre estos fragmentos en src/main/resources/templates/layout/main.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" th:fragment="layout (titulo, contenido)">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title th:text="${titulo} + ' - MiApp'">MiApp</title>
    
    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <!-- Bootstrap Icons -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
    
    <!-- Estilos personalizados -->
    <style>
        body {
            min-height: 100vh;
            display: flex;
            flex-direction: column;
        }
        
        main {
            flex: 1;
        }
        
        .navbar-brand {
            font-size: 1.5rem;
        }
        
        .nav-link.active {
            font-weight: bold;
            background-color: rgba(255, 255, 255, 0.1);
            border-radius: 0.375rem;
        }
    </style>
</head>
<body>
    <!-- Barra de navegación -->
    <div th:replace="fragments/navigation :: navbar"></div>
    
    <!-- Contenido principal -->
    <main class="container-fluid">
        <div th:replace="${contenido}">
            <div class="container py-5">
                <h1>Contenido por defecto</h1>
                <p>Esta página está en construcción.</p>
            </div>
        </div>
    </main>
    
    <!-- Footer -->
    <div th:replace="fragments/navigation :: footer"></div>
    
    <!-- Bootstrap JavaScript -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Implementación en páginas específicas

Ahora podemos usar esta plantilla base en cualquier página de nuestra aplicación. Por ejemplo, en home.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" 
      th:replace="layout/main :: layout('Inicio', ~{::contenido})">
<body>
    <div th:fragment="contenido">
        <div class="container py-5">
            <!-- Hero Section -->
            <div class="row align-items-center mb-5">
                <div class="col-lg-6">
                    <h1 class="display-4 fw-bold text-primary">Bienvenido a MiApp</h1>
                    <p class="lead">La solución completa para gestionar tu negocio de manera eficiente y profesional.</p>
                    <div class="d-flex gap-3">
                        <a href="/productos" th:href="@{/productos}" class="btn btn-primary btn-lg">
                            Ver Productos
                        </a>
                        <a href="/contacto" th:href="@{/contacto}" class="btn btn-outline-primary btn-lg">
                            Contactar
                        </a>
                    </div>
                </div>
                <div class="col-lg-6 text-center">
                    <img src="https://via.placeholder.com/500x300" class="img-fluid rounded shadow" alt="Hero Image">
                </div>
            </div>
            
            <!-- Features Section -->
            <div class="row g-4">
                <div class="col-md-4">
                    <div class="card h-100 border-0 shadow-sm">
                        <div class="card-body text-center">
                            <i class="bi bi-speedometer2 text-primary" style="font-size: 3rem;"></i>
                            <h5 class="card-title mt-3">Rápido y Eficiente</h5>
                            <p class="card-text">Optimiza tus procesos con nuestra tecnología avanzada.</p>
                        </div>
                    </div>
                </div>
                <div class="col-md-4">
                    <div class="card h-100 border-0 shadow-sm">
                        <div class="card-body text-center">
                            <i class="bi bi-shield-check text-success" style="font-size: 3rem;"></i>
                            <h5 class="card-title mt-3">Seguro y Confiable</h5>
                            <p class="card-text">Tus datos están protegidos con los más altos estándares de seguridad.</p>
                        </div>
                    </div>
                </div>
                <div class="col-md-4">
                    <div class="card h-100 border-0 shadow-sm">
                        <div class="card-body text-center">
                            <i class="bi bi-people text-info" style="font-size: 3rem;"></i>
                            <h5 class="card-title mt-3">Soporte 24/7</h5>
                            <p class="card-text">Nuestro equipo está disponible para ayudarte cuando lo necesites.</p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

Características destacadas de la implementación

Esta implementación incluye varias características profesionales:

  • Navegación responsiva: La barra de navegación se adapta automáticamente a dispositivos móviles usando las clases de Bootstrap
  • Enlaces activos: Los enlaces de navegación se resaltan automáticamente según la página actual usando expresiones Thymeleaf
  • Footer informativo: El footer incluye información de contacto, enlaces útiles y redes sociales
  • Iconos Bootstrap: Se utilizan iconos para mejorar la experiencia visual
  • Diseño flexible: La estructura permite que el contenido principal ocupe todo el espacio disponible
  • Año dinámico: El copyright en el footer se actualiza automáticamente con el año actual

Ventajas de esta aproximación

Al implementar la navegación y footer como fragmentos reutilizables, obtenemos:

  • Mantenimiento centralizado: Cualquier cambio en la navegación o footer se aplica automáticamente a todas las páginas
  • Consistencia visual: Todas las páginas mantienen la misma apariencia y comportamiento
  • Desarrollo ágil: Las nuevas páginas heredan automáticamente la estructura común
  • Código limpio: Cada página se enfoca únicamente en su contenido específico

Esta estructura proporciona una base sólida para cualquier aplicación web, manteniendo un diseño profesional y una experiencia de usuario consistente en toda la aplicación.

Aprende SpringBoot online

Otras lecciones de SpringBoot

Accede a todas las lecciones de SpringBoot y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A Spring Boot

Spring Boot

Introducción Y Entorno

Spring Boot Starters

Spring Boot

Introducción Y Entorno

Inyección De Dependencias

Spring Boot

Introducción Y Entorno

Crear Proyecto Con Spring Initializr

Spring Boot

Introducción Y Entorno

Crear Proyecto Desde Visual Studio Code

Spring Boot

Introducción Y Entorno

Controladores Spring Mvc

Spring Boot

Spring Mvc Con Thymeleaf

Vista En Spring Mvc Con Thymeleaf

Spring Boot

Spring Mvc Con Thymeleaf

Controladores Spring Rest

Spring Boot

Spring Mvc Con Thymeleaf

Open Api Y Cómo Agregarlo En Spring Boot

Spring Boot

Spring Mvc Con Thymeleaf

Servicios En Spring

Spring Boot

Spring Mvc Con Thymeleaf

Clientes Resttemplate Y Restclient

Spring Boot

Spring Mvc Con Thymeleaf

Rxjava En Spring Web

Spring Boot

Spring Mvc Con Thymeleaf

Métodos Post En Controladores Mvc

Spring Boot

Spring Mvc Con Thymeleaf

Métodos Get En Controladores Mvc

Spring Boot

Spring Mvc Con Thymeleaf

Formularios En Spring Mvc

Spring Boot

Spring Mvc Con Thymeleaf

Crear Proyecto Con Intellij Idea

Spring Boot

Spring Mvc Con Thymeleaf

Introducción A Los Modelos Mvc

Spring Boot

Spring Mvc Con Thymeleaf

Layouts Y Fragmentos En Thymeleaf

Spring Boot

Spring Mvc Con Thymeleaf

Estilización Con Bootstrap Css

Spring Boot

Spring Mvc Con Thymeleaf

Gestión De Errores Controlleradvice

Spring Boot

Spring Mvc Con Thymeleaf

Estilización Con Tailwind Css

Spring Boot

Spring Mvc Con Thymeleaf

Introducción A Controladores Rest

Spring Boot

Spring Rest

Métodos Get En Controladores Rest

Spring Boot

Spring Rest

Métodos Post En Controladores Rest

Spring Boot

Spring Rest

Métodos Delete En Controladores Rest

Spring Boot

Spring Rest

Métodos Put Y Patch En Controladores Rest

Spring Boot

Spring Rest

Gestión De Errores Restcontrolleradvice

Spring Boot

Spring Rest

Creación De Entidades Jpa

Spring Boot

Spring Data Jpa

Asociaciones De Entidades Jpa

Spring Boot

Spring Data Jpa

Repositorios Spring Data

Spring Boot

Spring Data Jpa

Métodos Find En Repositorios

Spring Boot

Spring Data Jpa

Inserción De Datos

Spring Boot

Spring Data Jpa

Actualizar Datos De Base De Datos

Spring Boot

Spring Data Jpa

Borrar Datos De Base De Datos

Spring Boot

Spring Data Jpa

Consultas Jpql Con @Query En Spring Data Jpa

Spring Boot

Spring Data Jpa

Api Query By Example (Qbe)

Spring Boot

Spring Data Jpa

Api Specification

Spring Boot

Spring Data Jpa

Repositorios Reactivos

Spring Boot

Spring Data Jpa

Configuración Base De Datos Postgresql

Spring Boot

Spring Data Jpa

Configuración Base De Datos Mysql

Spring Boot

Spring Data Jpa

Introducción A Jpa Y Spring Data Jpa

Spring Boot

Spring Data Jpa

Configuración Base De Datos H2

Spring Boot

Spring Data Jpa

Testing Unitario De Componentes Y Servicios

Spring Boot

Testing Con Spring Test

Testing De Repositorios Spring Data Jpa

Spring Boot

Testing Con Spring Test

Testing Controladores Spring Mvc Con Thymeleaf

Spring Boot

Testing Con Spring Test

Testing Controladores Rest Con Json

Spring Boot

Testing Con Spring Test

Testing De Aplicaciones Reactivas Webflux

Spring Boot

Testing Con Spring Test

Testing De Seguridad Spring Security

Spring Boot

Testing Con Spring Test

Testing Con Apache Kafka

Spring Boot

Testing Con Spring Test

Introducción Al Testing

Spring Boot

Testing Con Spring Test

Introducción A Spring Security

Spring Boot

Seguridad Con Spring Security

Seguridad Basada En Formulario

Spring Boot

Seguridad Con Spring Security

Registro De Usuarios En Api Rest

Spring Boot

Seguridad Con Spring Security

Login De Usuarios En Api Rest

Spring Boot

Seguridad Con Spring Security

Validación Jwt En Api Rest

Spring Boot

Seguridad Con Spring Security

Autenticación Jwt Completa En Api Rest

Spring Boot

Seguridad Con Spring Security

Seguridad Jwt En Api Rest Reactiva Spring Webflux

Spring Boot

Seguridad Con Spring Security

Autenticación Y Autorización Con Anotaciones

Spring Boot

Seguridad Con Spring Security

Fundamentos De Autenticación Oauth

Spring Boot

Seguridad Con Spring Security

Autenticación Oauth Con Github

Spring Boot

Seguridad Con Spring Security

Testing Con Spring Security Test

Spring Boot

Seguridad Con Spring Security

Autenticación Oauth En Api Rest

Spring Boot

Seguridad Con Spring Security

Introducción A Spring Webflux

Spring Boot

Reactividad Webflux

Spring Data R2dbc

Spring Boot

Reactividad Webflux

Controlador Reactivo Basado En Anotaciones

Spring Boot

Reactividad Webflux

Controlador Reactivo Basado En Funciones

Spring Boot

Reactividad Webflux

Operadores Reactivos Básicos

Spring Boot

Reactividad Webflux

Operadores Reactivos Avanzados

Spring Boot

Reactividad Webflux

Cliente Reactivo Webclient

Spring Boot

Reactividad Webflux

Introducción E Instalación De Apache Kafka

Spring Boot

Mensajería Asíncrona

Crear Proyecto Con Apache Kafka

Spring Boot

Mensajería Asíncrona

Creación De Producers

Spring Boot

Mensajería Asíncrona

Creación De Consumers

Spring Boot

Mensajería Asíncrona

Kafka Streams En Spring Boot

Spring Boot

Mensajería Asíncrona

Integración Con Angular

Spring Boot

Integración Frontend

Integración Con React

Spring Boot

Integración Frontend

Integración Con Vue

Spring Boot

Integración Frontend

Accede GRATIS a SpringBoot y certifícate

Ejercicios de programación de SpringBoot

Evalúa tus conocimientos de esta lección Layouts y fragmentos en Thymeleaf con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender qué son los fragmentos en Thymeleaf y cómo definirlos.
  • Aprender a parametrizar fragmentos para aumentar su reutilización.
  • Saber incluir fragmentos en plantillas usando th:replace y manejar inclusiones condicionales y en bucles.
  • Entender la herencia de plantillas para crear estructuras base y extenderlas en páginas específicas.
  • Implementar una barra de navegación y un footer comunes usando fragmentos y herencia para mantener consistencia y facilitar el mantenimiento.