Plantillas base y bloques reutilizables
La herencia de plantillas en Jinja2 representa uno de los mecanismos más importantes para crear interfaces web escalables y mantenibles. Este sistema permite definir una estructura común que múltiples páginas pueden reutilizar, evitando la duplicación de código HTML y facilitando cambios globales en el diseño.
Concepto de plantillas base
Una plantilla base actúa como el esqueleto principal de nuestro sitio web. Define la estructura HTML común que compartirán todas las páginas: el doctype, las etiquetas meta, la navegación, el footer y las referencias a archivos CSS y JavaScript.
La plantilla base utiliza bloques (block
) para marcar las secciones que las plantillas hijas podrán personalizar. Estos bloques funcionan como espacios reservados que pueden ser sobrescritos por plantillas que hereden de la base.
Estructura básica de una plantilla base:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Mi Sitio Web{% endblock %}</title>
<link href="{{ url_for('static', path='/css/style.css') }}" rel="stylesheet">
{% block head %}{% endblock %}
</head>
<body>
<header>
<nav>
<a href="/">Inicio</a>
<a href="/productos">Productos</a>
<a href="/contacto">Contacto</a>
</nav>
</header>
<main>
{% block content %}
<p>Contenido por defecto</p>
{% endblock %}
</main>
<footer>
<p>© 2024 Mi Empresa. Todos los derechos reservados.</p>
{% block footer %}{% endblock %}
</footer>
{% block scripts %}{% endblock %}
</body>
</html>
Definición y tipos de bloques
Los bloques se definen usando la sintaxis {% block nombre_bloque %}
y se cierran con {% endblock %}
. Cada bloque puede contener contenido por defecto que se mostrará si la plantilla hija no lo sobrescribe.
Tipos de bloques comunes:
- 1. Bloque de título: Permite personalizar el título de cada página
<title>{% block title %}Título por defecto{% endblock %}</title>
- 2. Bloque de head: Para agregar CSS, meta tags específicos o scripts adicionales
{% block head %}
<!-- CSS o meta tags adicionales -->
{% endblock %}
- 3. Bloque de contenido principal: La sección más importante donde va el contenido único de cada página
<main>
{% block content %}
<h1>Página en construcción</h1>
{% endblock %}
</main>
- 4. Bloque de scripts: Para JavaScript específico de cada página
{% block scripts %}
<!-- Scripts específicos de la página -->
{% endblock %}
Implementación en FastAPI
Para usar plantillas base en FastAPI, organizamos nuestros archivos de template en una estructura jerárquica clara. La plantilla base se coloca normalmente en la raíz del directorio de templates o en una subcarpeta llamada layouts
.
Estructura de directorios recomendada:
templates/
├── base.html
├── layouts/
│ └── admin_base.html
├── pages/
│ ├── home.html
│ ├── products.html
│ └── contact.html
└── components/
├── navbar.html
└── footer.html
Ejemplo de plantilla base completa para FastAPI:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %} - Mi Aplicación FastAPI</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- CSS personalizado -->
<link href="{{ url_for('static', path='/css/style.css') }}" rel="stylesheet">
{% block head %}{% endblock %}
</head>
<body>
<!-- Navegación -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="/">{% block brand %}Mi App{% endblock %}</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>
<!-- Mensajes flash -->
{% block messages %}
<div class="container mt-3">
<div id="flash-messages"></div>
</div>
{% endblock %}
<!-- Contenido principal -->
<div class="container mt-4">
{% block content %}
<div class="row">
<div class="col-12">
<h1>Página por defecto</h1>
<p>Este contenido se muestra si no se define el bloque content.</p>
</div>
</div>
{% endblock %}
</div>
<!-- Footer -->
<footer class="bg-dark text-white mt-5 py-4">
<div class="container">
{% block footer %}
<div class="row">
<div class="col-12 text-center">
<p>© 2024 Mi Aplicación. Desarrollada con FastAPI y Jinja2.</p>
</div>
</div>
{% endblock %}
</div>
</footer>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Scripts personalizados -->
{% block scripts %}{% endblock %}
</body>
</html>
Bloques anidados y jerarquías complejas
Jinja2 permite crear jerarquías de herencia más complejas donde una plantilla puede heredar de otra que ya hereda de una base. Este patrón es útil para sitios con diferentes secciones que comparten elementos comunes pero tienen estructuras específicas.
Ejemplo de plantilla intermedia para área administrativa:
{% extends "base.html" %}
{% block brand %}Panel Admin{% endblock %}
{% block content %}
<div class="row">
<!-- Sidebar de administración -->
<div class="col-md-3">
<div class="list-group">
{% block admin_nav %}
<a href="/admin/usuarios" class="list-group-item">Usuarios</a>
<a href="/admin/productos" class="list-group-item">Productos</a>
<a href="/admin/configuracion" class="list-group-item">Configuración</a>
{% endblock %}
</div>
</div>
<!-- Contenido del panel -->
<div class="col-md-9">
{% block admin_content %}
<h2>Panel de Administración</h2>
<p>Selecciona una opción del menú lateral.</p>
{% endblock %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', path='/js/admin.js') }}"></script>
{% endblock %}
Ventajas de las plantillas base
El uso de plantillas base aporta múltiples beneficios al desarrollo web:
- Consistencia visual: Garantiza que todas las páginas mantengan el mismo diseño y estructura
- Mantenibilidad: Los cambios en la navegación, footer o estructura general se aplican automáticamente a todas las páginas
- Reutilización: Evita duplicar código HTML común entre diferentes páginas
- Flexibilidad: Permite personalizar secciones específicas manteniendo la estructura base
- Escalabilidad: Facilita agregar nuevas páginas que hereden automáticamente el diseño establecido
La separación de responsabilidades que ofrecen las plantillas base permite que los desarrolladores se concentren en el contenido específico de cada página, mientras que el diseño global se mantiene centralizado y consistente en toda la aplicación.
Extensión y personalización de plantillas
La extensión de plantillas permite que las plantillas hijas hereden y personalicen el contenido de plantillas base mediante la directiva {% extends %}
. Este mecanismo proporciona flexibilidad total para adaptar la estructura base a las necesidades específicas de cada página.
Sintaxis de extensión básica
Para crear una plantilla que herede de otra, utilizamos la directiva {% extends %}
como primera línea del archivo. Esta debe ser la primera declaración en la plantilla, antes de cualquier otro contenido HTML.
{% extends "base.html" %}
{% block title %}Mi Página Personalizada{% endblock %}
{% block content %}
<div class="hero-section">
<h1>Bienvenido a mi página</h1>
<p>Este contenido reemplaza completamente el bloque content de la plantilla base.</p>
</div>
{% endblock %}
La ruta especificada en extends
es relativa al directorio de templates configurado en FastAPI. Si organizamos nuestras plantillas en subdirectorios, debemos incluir la ruta completa:
{% extends "layouts/admin_base.html" %}
Sobrescritura de bloques
Cuando una plantilla hija define un bloque que existe en la plantilla padre, sobrescribe completamente su contenido. Esto permite personalizar secciones específicas manteniendo el resto de la estructura intacta.
Ejemplo de página de productos que extiende la plantilla base:
{% extends "base.html" %}
{% block title %}Catálogo de Productos{% endblock %}
{% block head %}
<style>
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.product-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>
{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-12">
<h1>Nuestros Productos</h1>
<p class="lead">Descubre nuestra selección de productos de alta calidad.</p>
</div>
</div>
<div class="product-grid mt-4">
{% for producto in productos %}
<div class="product-card">
<h3>{{ producto.nombre }}</h3>
<p class="text-muted">${{ producto.precio }}</p>
<p>{{ producto.descripcion }}</p>
<a href="/productos/{{ producto.id }}" class="btn btn-primary">Ver detalles</a>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// JavaScript específico para la página de productos
const productCards = document.querySelectorAll('.product-card');
productCards.forEach(card => {
card.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-5px)';
});
card.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
});
});
});
</script>
{% endblock %}
Uso de super() para contenido incremental
La función {{ super() }}
permite conservar el contenido original del bloque padre y agregarle contenido adicional. Esto es especialmente útil cuando queremos extender funcionalidad sin perder la implementación base.
Ejemplo de plantilla que agrega contenido al footer:
{% extends "base.html" %}
{% block title %}Contacto{% endblock %}
{% block content %}
<div class="contact-section">
<h1>Contáctanos</h1>
<div class="row mt-4">
<div class="col-md-6">
<form action="/contacto" method="post">
<div class="mb-3">
<label for="nombre" class="form-label">Nombre</label>
<input type="text" class="form-control" id="nombre" name="nombre" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="mensaje" class="form-label">Mensaje</label>
<textarea class="form-control" id="mensaje" name="mensaje" rows="5" required></textarea>
</div>
<button type="submit" class="btn btn-success">Enviar mensaje</button>
</form>
</div>
<div class="col-md-6">
<h3>Información de contacto</h3>
<p><strong>Teléfono:</strong> +34 123 456 789</p>
<p><strong>Email:</strong> info@miempresa.com</p>
<p><strong>Dirección:</strong> Calle Principal 123, Madrid</p>
</div>
</div>
</div>
{% endblock %}
{% block footer %}
{{ super() }}
<div class="row mt-3">
<div class="col-12 text-center">
<div class="social-links">
<a href="#" class="text-white me-3">Facebook</a>
<a href="#" class="text-white me-3">Twitter</a>
<a href="#" class="text-white">LinkedIn</a>
</div>
</div>
</div>
{% endblock %}
Personalización condicional de bloques
Jinja2 permite usar lógica condicional dentro de los bloques para personalizar el contenido según diferentes criterios. Esto es útil para crear páginas adaptativas basadas en datos del contexto.
Ejemplo con lógica condicional en el endpoint FastAPI:
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
@app.get("/dashboard/{usuario_tipo}")
async def dashboard(request: Request, usuario_tipo: str):
# Datos diferentes según el tipo de usuario
contexto = {
"request": request,
"usuario_tipo": usuario_tipo,
"es_admin": usuario_tipo == "admin",
"estadisticas": {
"usuarios": 150,
"productos": 45,
"ventas": 1200
} if usuario_tipo == "admin" else None
}
return templates.TemplateResponse("dashboard.html", contexto)
Plantilla con personalización condicional:
{% extends "base.html" %}
{% block title %}
{% if es_admin %}
Panel de Administración
{% else %}
Mi Dashboard
{% endif %}
{% endblock %}
{% block content %}
<div class="dashboard">
<div class="row">
<div class="col-12">
<h1>
{% if es_admin %}
Bienvenido, Administrador
{% else %}
Bienvenido a tu dashboard
{% endif %}
</h1>
</div>
</div>
{% if es_admin and estadisticas %}
<div class="row mt-4">
<div class="col-md-4">
<div class="card bg-primary text-white">
<div class="card-body">
<h5 class="card-title">Usuarios Totales</h5>
<h2>{{ estadisticas.usuarios }}</h2>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-success text-white">
<div class="card-body">
<h5 class="card-title">Productos</h5>
<h2>{{ estadisticas.productos }}</h2>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-info text-white">
<div class="card-body">
<h5 class="card-title">Ventas Este Mes</h5>
<h2>${{ estadisticas.ventas }}</h2>
</div>
</div>
</div>
</div>
{% else %}
<div class="row mt-4">
<div class="col-12">
<div class="alert alert-info">
<h4>Tu área personal</h4>
<p>Desde aquí puedes gestionar tu perfil y ver tu actividad.</p>
<a href="/perfil" class="btn btn-primary">Ver perfil</a>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
Bloques múltiples y organización modular
Las plantillas pueden definir múltiples bloques que se personalizan de forma independiente. Esta aproximación modular facilita el mantenimiento y permite reutilizar componentes específicos.
Plantilla de artículo con múltiples secciones personalizables:
{% extends "base.html" %}
{% block title %}{{ articulo.titulo }}{% endblock %}
{% block head %}
<meta name="description" content="{{ articulo.resumen }}">
<meta name="author" content="{{ articulo.autor }}">
<link rel="canonical" href="{{ request.url }}">
{% endblock %}
{% block content %}
<article class="blog-post">
{% block article_header %}
<header class="mb-4">
<h1>{{ articulo.titulo }}</h1>
<div class="article-meta text-muted">
<span>Por {{ articulo.autor }}</span>
<span class="mx-2">•</span>
<span>{{ articulo.fecha.strftime('%d de %B de %Y') }}</span>
<span class="mx-2">•</span>
<span>{{ articulo.tiempo_lectura }} min de lectura</span>
</div>
</header>
{% endblock %}
{% block article_content %}
<div class="article-body">
{% if articulo.imagen %}
<img src="{{ url_for('static', path=articulo.imagen) }}"
class="img-fluid rounded mb-4"
alt="{{ articulo.titulo }}">
{% endif %}
<div class="content">
{{ articulo.contenido|safe }}
</div>
</div>
{% endblock %}
{% block article_footer %}
<footer class="mt-5">
<div class="row">
<div class="col-md-6">
<div class="tags">
<strong>Tags:</strong>
{% for tag in articulo.tags %}
<span class="badge bg-secondary me-1">{{ tag }}</span>
{% endfor %}
</div>
</div>
<div class="col-md-6 text-end">
<div class="share-buttons">
<strong>Compartir:</strong>
<a href="#" class="btn btn-sm btn-outline-primary">Facebook</a>
<a href="#" class="btn btn-sm btn-outline-info">Twitter</a>
</div>
</div>
</div>
</footer>
{% endblock %}
</article>
{% endblock %}
{% block scripts %}
<script>
// JavaScript para funcionalidad de compartir
document.addEventListener('DOMContentLoaded', function() {
const shareButtons = document.querySelectorAll('.share-buttons a');
shareButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const url = encodeURIComponent(window.location.href);
const title = encodeURIComponent(document.title);
let shareUrl;
if (this.textContent.includes('Facebook')) {
shareUrl = `https://www.facebook.com/sharer/sharer.php?u=${url}`;
} else if (this.textContent.includes('Twitter')) {
shareUrl = `https://twitter.com/intent/tweet?url=${url}&text=${title}`;
}
window.open(shareUrl, '_blank', 'width=600,height=400');
});
});
});
</script>
{% endblock %}
Herencia múltiple y composición avanzada
Jinja2 permite crear estructuras de herencia complejas donde las plantillas pueden extender de diferentes bases según el contexto. Esto se logra usando variables en la directiva extends
.
Ejemplo de herencia dinámica:
@app.get("/pagina/{tipo_layout}")
async def pagina_dinamica(request: Request, tipo_layout: str):
# Seleccionar plantilla base según el parámetro
layout_map = {
"admin": "layouts/admin_base.html",
"publico": "base.html",
"mobile": "layouts/mobile_base.html"
}
contexto = {
"request": request,
"layout_template": layout_map.get(tipo_layout, "base.html"),
"contenido": "Contenido específico para este tipo de layout"
}
return templates.TemplateResponse("pagina_dinamica.html", contexto)
{% extends layout_template %}
{% block title %}Página con Layout Dinámico{% endblock %}
{% block content %}
<div class="dynamic-content">
<h1>Contenido adaptativo</h1>
<p>{{ contenido }}</p>
<p>Esta página usa el layout: <strong>{{ layout_template }}</strong></p>
</div>
{% endblock %}
Esta flexibilidad en la extensión permite crear aplicaciones web altamente modulares donde el diseño se adapta dinámicamente a diferentes contextos, dispositivos o tipos de usuario, manteniendo siempre una base sólida y reutilizable.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en FastAPI
Documentación oficial de FastAPI
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, FastAPI es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de FastAPI
Explora más contenido relacionado con FastAPI y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Comprender el concepto y estructura de plantillas base en Jinja2.
- Aprender a definir y sobrescribir bloques para personalizar contenido.
- Implementar herencia de plantillas en proyectos FastAPI.
- Utilizar la función super() para extender contenido de bloques.
- Aplicar lógica condicional y herencia múltiple para personalización avanzada.