Definición y estructura
Las macros en Twig son como funciones reutilizables que te permiten encapsular fragmentos de código HTML/Twig para usarlos múltiples veces. Son especialmente útiles en Grav para crear componentes repetitivos.
Estructura básica:
{% verbatim %}{% macro nombreMacro(parametro1, parametro2) %}
{# Código de la macro #}
<div>{{ parametro1 }} - {{ parametro2 }}</div>
{% endmacro %}
Para usar una macro, primero debes importarla:
{% verbatim %}{% import _self as macros %}
{{ macros.nombreMacro('valor1', 'valor2') }}
O importar desde otro archivo:
{% verbatim %}{% import 'partials/macros.html.twig' as macros %}
Ejemplos básicos
Ejemplo 1: Botón reutilizable
{% verbatim %}{# En tu plantilla o en partials/macros.html.twig #}
{% macro boton(texto, url, clase) %}
<a href="{{ url }}" class="btn {{ clase|default('btn-primary') }}">
{{ texto }}
</a>
{% endmacro %}
{# Uso #}
{% import _self as ui %}
{{ ui.boton('Leer más', page.url, 'btn-success') }}
{{ ui.boton('Inicio', base_url_absolute, 'btn-secondary') }}
Ejemplo 2: Breadcrumbs en Grav
{% verbatim %}{% macro breadcrumbs(page) %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{% for crumb in page.parent().children() %}
<li class="breadcrumb-item">
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
</li>
{% endfor %}
<li class="breadcrumb-item active">{{ page.title }}</li>
</ol>
</nav>
{% endmacro %}
{# Uso #}
{% import _self as nav %}
{{ nav.breadcrumbs(page) }}
Ejemplo 3: Imagen responsive
{% verbatim %}{% macro imagen_responsive(media, alt, clases) %}
{% if media %}
<img src="{{ media.url }}"
alt="{{ alt }}"
class="{{ clases|default('img-fluid') }}"
loading="lazy">
{% endif %}
{% endmacro %}
{# Uso con imágenes de Grav #}
{% import _self as img %}
{{ img.imagen_responsive(page.media['foto.jpg'], 'Descripción', 'rounded') }}
Ejemplos complejos
Ejemplo 1: Sistema de tarjetas (cards) con múltiples opciones
{% verbatim %}{% macro card(titulo, contenido, imagen, opciones) %}
{% set defaults = {
'clase_card': 'card mb-4',
'clase_img': 'card-img-top',
'mostrar_fecha': true,
'mostrar_autor': false,
'boton_texto': 'Leer más',
'boton_url': '#'
} %}
{% set config = defaults|merge(opciones|default({})) %}
<div class="{{ config.clase_card }}">
{% if imagen %}
<img src="{{ imagen }}" class="{{ config.clase_img }}" alt="{{ titulo }}">
{% endif %}
<div class="card-body">
<h5 class="card-title">{{ titulo }}</h5>
<p class="card-text">{{ contenido }}</p>
{% if config.mostrar_fecha or config.mostrar_autor %}
<div class="card-meta">
{% if config.mostrar_fecha %}
<small class="text-muted">{{ config.fecha|date('d/m/Y') }}</small>
{% endif %}
{% if config.mostrar_autor %}
<small class="text-muted">Por {{ config.autor }}</small>
{% endif %}
</div>
{% endif %}
<a href="{{ config.boton_url }}" class="btn btn-primary">
{{ config.boton_texto }}
</a>
</div>
</div>
{% endmacro %}
{# Uso en Grav #}
{% import _self as componentes %}
{% for child in page.children.published %}
{{ componentes.card(
child.title,
child.summary(200),
child.media.images|first.url,
{
'mostrar_fecha': true,
'mostrar_autor': true,
'fecha': child.date,
'autor': child.header.author,
'boton_url': child.url,
'boton_texto': 'Ver artículo'
}
) }}
{% endfor %}
Ejemplo 2: Menú multinivel recursivo
{% verbatim %}{% macro menu_recursivo(paginas, nivel) %}
{% set nivel = nivel|default(0) %}
<ul class="menu-nivel-{{ nivel }}">
{% for pagina in paginas if pagina.visible %}
<li class="{{ pagina.active or pagina.activeChild ? 'active' : '' }}">
<a href="{{ pagina.url }}">
{{ pagina.menu|default(pagina.title) }}
</a>
{% if pagina.children.count > 0 %}
{{ _self.menu_recursivo(pagina.children, nivel + 1) }}
{% endif %}
</li>
{% endfor %}
</ul>
{% endmacro %}
{# Uso #}
{% import _self as nav %}
{{ nav.menu_recursivo(pages.children) }}
Ejemplo 3: Grid de imágenes con lightbox
{% verbatim %}{% macro galeria(imagenes, columnas, id_galeria) %}
{% set columnas = columnas|default(3) %}
{% set clase_columna = 'col-md-' ~ (12 / columnas) %}
<div class="row galeria" id="{{ id_galeria|default('galeria') }}">
{% for imagen in imagenes %}
<div class="{{ clase_columna }} mb-4">
<div class="imagen-container">
<a href="{{ imagen.url }}"
data-lightbox="{{ id_galeria }}"
data-title="{{ imagen.meta.alt_text|default('') }}">
<img src="{{ imagen.cropResize(400, 300).url }}"
alt="{{ imagen.meta.alt_text|default('Imagen ' ~ loop.index) }}"
class="img-fluid">
</a>
{% if imagen.meta.caption %}
<p class="caption">{{ imagen.meta.caption }}</p>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endmacro %}
{# Uso con las imágenes de una página en Grav #}
{% import _self as media %}
{{ media.galeria(page.media.images, 4, 'galeria-principal') }}