Documentación · Referencia · El formato de un proyecto
7. Plantillas
Twig: layouts, parciales, variables, funciones y filtros.
Versión 0.16
Motor: Twig. Los layouts viven en plantillas/ y los fragmentos
reutilizables en parciales/.
7.1 Plantillas y parciales¶
Dentro de Twig, las plantillas y los parciales se nombran con su ruta desde la
raíz del proyecto, y Twig no puede leer nada fuera de esas dos carpetas, salvo las
plantillas de los atajos que trae el motor, bajo @ehundu/: son suyas, un sitio
no debe incluirlas (para cambiar un atajo, §8.1) y pueden cambiar o quitarse en
cualquier versión (decisión 145):
{% extends 'plantillas/base.twig' %}
{% include 'parciales/cabecera.twig' %}
El layout de una página se elige con plantilla, por su nombre sin carpeta ni
extensión: plantilla: articulo es plantillas/articulo.twig. Si no se
indica, se usa pagina. Con plantilla: false la página sale tal cual, sin
layout, que es lo que necesita un robots.txt. Si la plantilla que usa una
página no existe, el build se detiene: publicar el contenido sin su layout
sería peor que no publicarlo.
Las plantillas no llevan front matter. Se anidan con {% extends %} y
bloques, como en cualquier proyecto de Twig. El cuerpo de la página está
siempre en {{ pagina.contenido }}:
{# plantillas/articulo.twig #}
{% extends 'plantillas/base.twig' %}
{% block cuerpo %}
<article>{{ pagina.contenido }}</article>
{% endblock %}
Las páginas .twig de contenido/ son plantillas de Twig con las mismas
variables; su resultado es pagina.contenido y va dentro de su layout como el
de cualquier otra página.
7.2 Variables¶
sitio: lo que hay ensitio.ymlsin la seccióndespliegue, conurlsin la barra final y conidioma(el predeterminado) eidiomas(la lista de{codigo, nombre, url}, §15.8) siempre, aunque el fichero no los declare. Los días sin hora, en la zona del sitio (§4).datos: los ficheros dedatos/, con los del idioma de la página (§15.6).pagina: el front matter de la página actual, máspagina.url,pagina.contenido(el cuerpo ya convertido a HTML),pagina.ruta,pagina.idiomaypagina.traducciones(§15.8).
Las páginas que dan coleccion() y sus filtros tienen la misma forma:
articulo.titulo, articulo.url. También articulo.contenido, que se
convierte la primera vez que alguien lo pide. Si una página acaba necesitando
su propio contenido para construirse (una página .twig que lista una
colección en la que está ella misma y pide el contenido de cada una), el build
se detiene con un error que muestra la cadena de páginas.
pagina.markdown es el cuerpo de una página en Markdown tal como está
escrito: sin el front matter y sin convertir, con los atajos sin resolver.
Las páginas de una colección la tienen también (articulo.markdown). Es lo
que necesita un llms-full.txt, que reúne el texto de varios artículos. Twig
la escapa como cualquier texto: en un fichero que no es HTML se escribe con
|raw. No existe en una página .twig, cuyo cuerpo es una plantilla. Quien
la lee depende del fichero de esa página: si cambia, se rehace la que la
muestra. Como contenido, tapa un campo del front matter que se llame igual,
y no sirve para filtrar ni ordenar (§6.2).
pagina.encabezados son los encabezados del cuerpo que reciben id (§2, §8),
en el orden en que aparecen, cada uno con nivel, texto e id. El texto es
texto plano, sin enlaces ni énfasis y sin el enlace permanente. Es lo que
necesita un índice de apartados. Las páginas de una colección la tienen también
(articulo.encabezados) y, como contenido, se calcula la primera vez que
alguien la pide, con el mismo error si una página acaba necesitándose a sí
misma. Sin la sección encabezados en sitio.yml está vacía; en una página
.twig, cuyo cuerpo es una plantilla, también. Quien la lee depende del
contenido de esa página: si cambia, se rehace la que la muestra. Como
contenido, tapa un campo del front matter que se llame igual, y no sirve para
filtrar ni ordenar (§6.2).
Una página que genera otra con agrupar (§6.3) tiene además pagina.grupo,
el valor, y pagina.paginas, las páginas del grupo. Las páginas generadas
las tienen también cuando salen de coleccion(). Una página con paginar
(§6.4), y las que genera, tienen pagina.paginas, las de su trozo, y
pagina.paginacion.
Los nombres que el motor escribe en pagina son estos, y solo estos. Si el
front matter de una página tiene un campo con el mismo nombre, el del motor
tapa el suyo (decisión 133):
| Nombre | Qué es | Cuándo | Un campo propio con ese nombre |
|---|---|---|---|
url |
su dirección (§5) | siempre | reservado (§4) |
idioma |
su idioma (§15.2) | siempre | reservado: se avisa y se ignora |
pdf |
la dirección de su PDF (§10.3) | con pdf: sí |
reservado: en el front matter es sí/no |
ruta |
su fichero, desde contenido/; en lo que genera agrupar o paginar, el del fichero que lo define con # y el slug del grupo o el número (etiqueta.md#cultura-pop, blog/index.md#2) |
siempre | se avisa y se ignora |
contenido |
su cuerpo ya convertido a HTML | siempre | se avisa y se ignora |
markdown |
su cuerpo en Markdown, sin convertir | en una página .md |
se avisa y se ignora |
encabezados |
sus encabezados con id (§8) |
siempre, vacía si no hay | se avisa y se ignora |
traducciones |
sus traducciones (§15.8) | siempre, vacía si no hay | se avisa y se ignora |
grupo |
el valor de su grupo (§6.3) | en lo que genera agrupar |
solo se tapa en esas páginas |
paginas |
las páginas del grupo o del trozo (§6.3, §6.4) | en lo que genera agrupar o paginar |
solo se tapa en esas páginas |
paginacion |
dónde está en la lista partida (§6.4) | en lo que genera paginar |
solo se tapa en esas páginas |
Un nombre nuevo no se añade sin anunciarlo (§12, regla 5).
Un campo que falta sale vacío, como en Nunjucks: {% if pagina.imagen %}
funciona sin comprobar antes si existe. Twig escapa el HTML de todo lo que
escribe, salvo lo que ya es HTML hecho por el motor: pagina.contenido (también
el articulo.contenido de las páginas de una colección y el contenido y el
pie que reciben los atajos, §8.1), lo que devuelven svg(), css(), js(),
css_fichero() y js_fichero(), y el resultado del filtro markdown (§7.3).
Cualquier otro texto, un campo del front matter por ejemplo, se escapa salvo
que la plantilla pida lo contrario con |raw.
7.3 Funciones y filtros¶
coleccion(nombre)y los filtros de colección del §6.2.
Las funciones que nombran un fichero de publico/ (svg, dimensiones,
variantes, css, js, css_fichero, js_fichero y version) admiten la
ruta con o sin / delante.
-
svg(ruta)incrusta un SVG depublico/, tal cual; solo le quita la declaración<?xml ?>y elDOCTYPE(con su subconjunto interno, si lo lleva), que no caben dentro de HTML, estén donde estén antes de la etiqueta<svg>; los comentarios se dejan:{{ svg('svg/telefono.svg') }}. Si el fichero no existe, no es un.svgo la ruta sale depublico/, se avisa y no se inserta nada. -
dimensiones(ruta)da el ancho y el alto de una imagen depublico/, en píxeles, parawidth,heightoaspect-ratio:{% set d = dimensiones('img/sede.jpg') %}y luego{{ d.ancho }}y{{ d.alto }}. Lee la cabecera del fichero con lo que trae PHP (JPEG, PNG, GIF, WebP, AVIF); no procesa la imagen. De un SVG o de un fichero que no es imagen danull, sin aviso. Si el fichero no existe o la ruta sale depublico/, avisa y danull. -
variantes(ruta)da las variantes de ancho de una imagen, las que declara la secciónimagenesdesitio.yml(§2), para escribirsrcset. Admite el tronco (img/foto) o el fichero (img/foto.jpg): quita la extensión si es la de una imagen. Da un mapa con:src, la dirección de la variante más ancha del último formato, con/delante, yanchoyaltode ese fichero;srcset, las variantes del último formato, de menor a mayor ancho y con su ancho real:/img/foto-480.webp 480w, /img/foto-960.webp 960w;fuentes, una lista con un elemento por cada formato anterior al último, en el orden deformatos, cada uno con sutipo(image/avif) y susrcset. Con un solo formato está vacía.
La plantilla escribe el HTML y decide el
sizes, que el motor no sabe:{% set f = variantes('img/foto') %} <picture> {% for fuente in f.fuentes %}<source type="{{ fuente.tipo }}" srcset="{{ fuente.srcset }}" sizes="100vw">{% endfor %} <img src="{{ f.src }}" srcset="{{ f.srcset }}" sizes="100vw" width="{{ f.ancho }}" height="{{ f.alto }}" alt=""> </picture>Avisa y da
nullsi el sitio no declaraimagenes, si la ruta sale depublico/o si no hay ninguna variante de la imagen. Avisa de cada fichero de variante que falta, y sigue con los que hay; un formato sin ninguna variante no entra. De un SVG no da nada, sin aviso. -
activo(url)dice si la página actual es esa URL o está dentro de ella:activo('/blog/')es cierto en/blog/y en/blog/un-articulo/./solo es activo en la portada, y la raíz de cada idioma (/eu/), en la suya (§15.8). Para comparar exacto,pagina.url == url. -
version(ruta)da la dirección de un fichero depublico/con su huella:{{ version('css/estilo.css') }}da/css/estilo.css?v=3fa9c1d2ab. La huella son los 10 primeros caracteres hexadecimales del SHA-256 del fichero, así que cambia cuando cambia su contenido y no antes. Es para enlazar los CSS, los JS, las tipografías y las imágenes que no pasan porcss()nicss_fichero()(§9.1): el navegador puede guardarlos mucho tiempo sin que, tras un despliegue, nadie vea una versión vieja. Admite la ruta con o sin/delante. Si el fichero no existe o la ruta sale depublico/, se avisa y da la dirección sin huella. Quien la usa depende del fichero: si cambia, se rehace la página. -
slug(§5.2). -
fecha(formato)escribe una fecha con las mismas letras que el filtrodatede Twig, pero con los nombres de meses y días en el idioma de la página (§15.9). En castellano,pagina.fecha|fecha('d F Y')da18 marzo 2025, y sin formato,18 de marzo de 2025. Una letra que tiene que salir tal cual se escapa con dos barras dentro de la plantilla, igual que condate:fecha('j \\d\\e F')da18 de marzo. Un valor vacío no da nada; uno que no es una fecha detiene el build. -
markdownconvierte a HTML un texto en Markdown, para los campos del front matter o dedatos/que llevan formato: un titular con una palabra en cursiva, un resumen con una negrita. Contitular: "Una cocina que *viaja*",{{ pagina.titular|markdown }}daUna cocina que <em>viaja</em>. Sin argumento es de línea, el Markdown del §8 sin bloques: no hay<p>, ni encabezados, listas, citas, tablas ni figuras, y los atajos (§8.1) no se reconocen, porque solo viven en el cuerpo. Conmarkdown('bloque')es de bloque: lo que haya en el campo se convierte como el cuerpo, con párrafos, listas, citas y encabezados, para un texto con varios párrafos dentro de un bloque que la plantilla pone ({{ pagina.intro|markdown('bloque') }}). El modo de bloque sigue sin atajos, y tampoco convierte las tablas ni las notas al pie, ni daida los encabezados (§2, §8), ni hace figuras de las imágenes: todo eso es del cuerpo.markdown('linea')es lo mismo que sin argumento; cualquier otro modo avisa y se usa el de línea. En los dos modos valen el énfasis, la negrita, el tachado, el código, los enlaces (los externos se tratan como en el cuerpo, con sutargety surel, y el aviso deenlacesExternos), los enlaces automáticos y el HTML escrito a mano (<mark>,<br>). Un salto de línea con una barra invertida o con dos espacios al final de la línea sale como<br>, seguido del salto de línea del texto, como en el cuerpo; uno suelto, como un salto en el HTML, que el navegador lee como un espacio. El resultado es HTML ya hecho: Twig no lo escapa de nuevo, y lo escrito a mano pasa tal cual, como en el cuerpo. Un valor vacío o que falta no da nada, sin aviso; un número se toma como texto; una lista, un mapa o un booleano avisan y no dan nada. En un texto sin formato, la plantilla sigue usando{{ campo }}, que sí escapa.
Todas las fechas se escriben en la zona horaria del sitio (§2), también las
del filtro date de Twig, que por sí solo usaría la de la máquina.
Las plantillas pueden usar random() de Twig para elegir al azar, y la fecha
del momento ('now'|date('Y'), para el año del pie). Con la misma versión del
motor y de sus dependencias, son las dos únicas formas de que el mismo proyecto dé otro HTML en las
páginas que salen (§12, regla 3), y tienen un precio: con
random(), cada compilación completa elige de nuevo, así que esa página
cambia y se vuelve a subir en cada despliegue. La previsualización, que
compila de forma incremental, conserva lo elegido, o la fecha, mientras no
cambie nada de lo que la página usa.