Documentación · Referencia · El formato de un proyecto
Registro de decisiones
Cada decisión del formato, con su motivo, en el orden en que se tomaron.
Versión 0.16
Las decisiones conservan las palabras con que se escribieron: «la v1» y «la v2» en las anteriores a la 132 quieren decir lo que Ehundu hacía o dejaba para más adelante entonces, no el formato 1 (decisión 132), y «hasta ahora» o «como hasta ahora» se refiere a la versión en que se tomó cada una. Cuando una decisión posterior cambia lo que dice una anterior, lo anota la posterior y, si hace falta, la anterior lleva una nota entre paréntesis.
-
Cerrada. Campos del front matter en español, con los nombres ingleses aceptados como alias solo para facilitar la migración.
-
Cerrada. El Markdown no pasa por Twig; hay atajos (§8.1).
-
Cerrada. Multiidioma por fichero de idioma (§15). No entró en la primera versión, pero el contrato reservó lo necesario; entra en la 0.3 (decisiones 72 a 81).
Por qué un fichero por idioma. Había tres formas de hacerlo. En las tres, el idioma por defecto va sin prefijo en la URL, los demás con prefijo, y cada página emite sus
hreflangapuntando a sus traducciones.A. Carpeta por idioma (
contenido/es/contacto.md,contenido/eu/kontaktua.md). Cada idioma es un árbol completo e independiente. No exige nada nuevo al motor, porque la cascada ya funciona por carpeta, y una página que solo existe en un idioma no es un caso especial. Pero nada relaciona una página con su traducción, hay que declararlo a mano con un campo; un editor muestra dos árboles separados y es fácil publicar en uno y olvidar el otro; y lo que no se traduce (imágenes, enlaces, teléfonos) se duplica y acaba desincronizándose.B. Fichero por idioma, mismo árbol (
contenido/contacto.md,contenido/contacto.eu.md). La relación entre traducciones es la propia convención, sin declarar nada. Cada página y sus traducciones viven juntas, así que un editor puede mostrarlas como pestañas de idioma sobre la misma página y es imposible no ver que falta una; los slugs por idioma se resuelven con el campourlde cada fichero. El motor tiene que entender el idioma en el nombre del fichero, que es código nuevo aunque poco, y el árbol se ve más cargado; lo no traducible se saca a los campos comunes de la página (§15.7).C. Un fichero con campos por idioma (
titulo: { es: Contacto, eu: Kontaktua }). Cero duplicación de lo no traducible, y un editor con pestañas sale casi gratis. Pero un cuerpo largo en Markdown dentro de un YAML es inmanejable, y los cambios son ilegibles al compararlos. Sirve para páginas hechas de campos, no para artículos.Se eligió B: es la que mejor encaja con un editor, que es donde el multiidioma se rompe en la práctica.
-
Cerrada.
sitio.ymles obligatorio, connombreyurl(§2). -
Cerrada. En la cascada,
etiquetasse suman; el resto se sustituye (§3). -
Cerrada. El motor valida el valor de los campos reservados y avisa; los campos desconocidos pasan sin comprobar (§4).
-
Cerrada. Los campos de sí/no admiten
sí,si,no,trueyfalse(§4). -
Cerrada.
etiquetasadmite un texto suelto como lista de uno (§4). -
Cerrada. Nuevos campos reservados
descripcionylistada, y aliassubtitleydescription(§4, §6). -
Cerrada.
plantilla: false(o vacía) publica la página sin layout (§4, §7). -
Cerrada. Un front matter que no es YAML válido detiene el build (§4).
-
Cerrada. Lo que empieza por punto se ignora en
contenido/ydatos/; endatos/, el.ymlgana al.json(§3, §4). -
Cerrada. Filtro
slugpropio, pensado para el español, en lugar de copiar el de Eleventy (§5.2). -
Cerrada. Los patrones de
urltienen un vocabulario cerrado:{{ campo }}y{{ campo|slug }}, con los alias ingleses (§5.1). -
Cerrada. Una URL sin barra final ni extensión se toma como carpeta, con aviso (§5).
-
Cerrada. La URL derivada usa la ruta tal cual y avisa si da una mala URL (§5).
-
Cerrada. Dos páginas que se publican en el mismo fichero detienen el build (§5.3).
-
Cerrada.
coleccion()ordena por fecha ascendente y, a igual fecha, por ruta, como Eleventy (§6.1). -
Cerrada. Las páginas sin fecha van al final de una colección, por ruta (§6.1).
-
Cerrada.
ordenadmite varios campos, deja al final los valores que faltan y compara los textos como el diccionario (§6.2). -
Cerrada.
dondesobre una lista significa «contiene» (§6.2). -
Cerrada. Filtros
anteriorysiguiente(§6.2). -
Cerrada.
todono se puede usar como etiqueta (§6). -
Cerrada. Plantillas y parciales se nombran desde la raíz del proyecto, y Twig solo lee
plantillas/yparciales/(§7.1). -
Cerrada. Las plantillas no llevan front matter; se anidan con
extends(§7.1). -
Cerrada; la sustituye la 47. Una compilación completa vacía
salida/antes de escribir (§10). -
Cerrada.
svg()inserta el fichero tal cual, sin declaración XML niDOCTYPE(§7.3). -
Cerrada.
activo()es cierto también en las páginas de dentro de esa URL;/solo en la portada (§7.3). -
Cerrada. Las fechas son días en la zona horaria del sitio, UTC si no se indica, y el filtro
fechalas escribe en español (§2, §4, §7.3). -
Cerrada. El contenido de otras páginas se convierte cuando se pide; si una página se necesita a sí misma, error (§7.2).
-
Cerrada. Si falta la plantilla de una página, el build se detiene (§7.1).
-
Cerrada. Una imagen sola en su párrafo se construye como el atajo
imagen, con elalten texto plano y el pie en HTML (§8). -
Cerrada. Los atajos se reconocen al convertir el Markdown y su HTML se inserta después (§8.1).
-
Cerrada. Las plantillas de atajos reciben los atributos como variables,
contenido,sitio,datosypagina(§8.1). -
Cerrada. Cuarto atajo incluido,
dato; y atributos y resultado deimagen,videoyarchivo(§8.2). -
Cerrada. Enlaces externos: otro dominio que el del sitio, con o sin
www., solo en Markdown (§8). -
Cerrada. El Markdown admite tachado y enlaces automáticos (§8).
-
Cerrada. Etiquetas vacías de HTML5, sin barra final (§8).
-
Cerrada. El CSS y el JS se declaran donde se usan, con
css('…')yjs('…'), y se escriben donde marcancss()yjs()(§9.1). -
Cerrada. Los campos
cssyjsse suman en la cascada, comoetiquetas(§3, §9.1). -
Cerrada.
publico/se copia entero, también lo que empieza por punto; decontenido/se copia lo que no es página ni empieza por_(§4, §9). -
Cerrada. Dos cosas que van al mismo fichero de salida detienen el build (§10).
-
Cerrada. Qué lleva el sitemap, y que un sitemap o feed del proyecto gana al del motor (§10, §10.1).
-
Cerrada. Feed Atom solo si
sitio.ymltiene la secciónfeed, con la colección de la que sale (§2, §10.2). -
Cerrada. Las plantillas y los datos de otros generadores que haya en
contenido/no se copian, y se avisa (§4). -
Cerrada. Una compilación completa sincroniza
salida/en lugar de vaciarla: borra lo que sobra y escribe o copia solo lo que cambia (§10). -
Cerrada. La previsualización compila de forma incremental, con el mismo resultado que una compilación completa;
--completorehace todo en cada cambio.compilares siempre completa (§10,compilacion.md). -
Cerrada. Qué se rehace se decide con lo que usó cada página y cada cuerpo la vez anterior. Si cambian
sitio.ymlodatos/, o si la compilación anterior falló, se rehace todo (compilacion.md). -
Cerrada; en parte la sustituye la 126. Lo que se recuerda de una compilación a otra vive en memoria y no se guarda en disco; son datos simples, que un programa que incruste el motor podrá guardar donde le convenga (
compilacion.md). (Desde la 126,CompiladorIncrementalEnProcesolo guarda en.ehundu/memoria.bin; la previsualización sigue sin hacerlo.) -
Cerrada. Compilar por lotes queda para más adelante; la compilación ya separa decidir qué se rehace de rehacerlo (
compilacion.md). Entra en la 0.12 (decisiones 124 y 125). -
Cerrada.
ehundu desplegarcompila entero y, si no hay errores, publica en el destino desitio.yml;--simulardice qué haría sin tocar el destino y--todolo sube todo (despliegue.md). -
Cerrada. Lo que hay en el destino se sabe por un manifiesto que vive allí mismo,
.ehundu.json, con el MD5 y el tamaño de cada fichero que subió Ehundu. Se guarda durante el despliegue, así que uno cortado sigue donde se quedó; sin él, se sube todo (despliegue.md). -
Cerrada. Solo se borra lo que subió Ehundu y ya no se genera; lo demás que haya en el destino no se toca nunca (
despliegue.md). -
Cerrada. Se suben primero los ficheros, después las páginas y al final el sitemap y el feed; los borrados van después (
despliegue.md). -
Cerrada. FTP con la extensión
ftpde PHP, cifrado (FTPS) salvocifrado: no(§2,despliegue.md). -
Cerrada. SFTP con phpseclib, que es PHP puro; la huella del servidor es obligatoria y se comprueba antes de mandar la clave (§2,
despliegue.md). -
Cerrada. S3 con la firma de AWS hecha en el motor, sin SDK; cada fichero con su tipo MIME y su MD5 (§2,
despliegue.md). -
Cerrada. Campos de
desplieguey de.secretos.yml; un secreto ensitio.ymldetiene el despliegue, y no hay variables de entorno (§2). -
Cerrada. Nada después de desplegar en la v1: ni avisos a otros servicios ni vaciado de cachés de una CDN (
despliegue.md). -
Cerrada. Los secretos se escriben entre comillas simples, y los errores de
.secretos.ymlno enseñan su contenido (§2). -
Cerrada.
dimensiones()lee el ancho y el alto de una imagen depublico/sin procesarla, y el atajoimageny las figuras del Markdown los ponen comowidthyheight(§7.3, §8, §8.2). -
Cerrada. Las plantillas pueden usar
random(); es, con la fecha del momento, la excepción a que el mismo proyecto dé el mismo HTML (§7.3, §12, regla 3). -
Cerrada. Una página que genera HTML sin
tituloavisa; un fragmento o unrobots.txtno lo necesitan (§4). -
Cerrada. Las páginas cuyo nombre o carpeta empieza por
_se compilan como las demás (§4). -
Cerrada. Los alias ingleses valen para el nombre del campo, no para su valor:
layout: post.njkno es una plantilla de Ehundu, y el error dice cómo quitar la extensión. Valen también en_datos.yml(§4). -
Cerrada.
dondecompara los textos exactos (§6.2). -
Cerrada.
componentesqueda reservado para el catálogo de la v2 (§13). (Hoy se dice «el catálogo de componentes», sin versión: decisión 132.) -
Cerrada. El Markdown sigue CommonMark también en los bloques de código sangrados, aunque Eleventy los desactive; al migrar se quita la sangría accidental (§8).
-
Cerrada. El manifiesto del destino guarda también la
urldel sitio. Si la del destino es otra, el despliegue se detiene antes de tocar nada, porque lo normal es que larutaapunte a otro sitio;--todoconfirma que es el mismo sitio con otra dirección (despliegue.md). -
Cerrada.
ordenydondeven tambiénurlyruta, con los mismos valores que las plantillas;contenido, no (§6.2). -
Cerrada. Los idiomas se declaran en
idiomas, y el primero es el predeterminado; sinprefijo, que es siempre/codigo/. Un sitio en un solo idioma lo dice conidioma, que esessi falta (§2, §15.1). -
Cerrada. Un fichero sin código es del idioma predeterminado; con el código del predeterminado es lo mismo, y los dos a la vez detienen el build. Las traducciones se emparejan por carpeta y nombre (§15.2).
-
Cerrada. El motor pone el prefijo del idioma a toda URL que no es del predeterminado, también a la de
urly a la de un patrón (§15.3). -
Cerrada.
idiomaes un campo reservado que sale del nombre del fichero; las plantillas venpagina.idioma,pagina.traduccionesysitio.idiomas, yordenydondevenidioma(§15.2, §15.8). -
Cerrada. Las colecciones se acotan al idioma de la página;
coleccion(nombre, idioma)pide otro, otodos(§15.5). -
Cerrada. Un fichero de datos por idioma se mezcla con el común por las claves de primer nivel, y un
_datos.eu.yml, encima del_datos.ymlde su carpeta (§15.6). -
Cerrada. Un
.ymlcon el nombre de una página son sus campos comunes a todos los idiomas, y no se copia asalida/(§4, §15.7). -
Cerrada.
fecha()escribe en el idioma de la página; el motor trae castellano, euskera e inglés, y con otro idioma avisa y usa los nombres en inglés (§7.3, §15.9). -
Cerrada. Un solo sitemap con todos los idiomas; un feed por idioma, el del predeterminado siempre y los demás si tienen entradas (§15.10).
-
Cerrada.
activo()trata la raíz de cada idioma como la portada, y se avisa si esa raíz no es la URL de ninguna página (§7.3, §15.3). -
Cerrada. Un tipo del esquema puede vivir en varias carpetas:
carpetaadmite una lista, y una ruta que acaba en/*son sus subcarpetas directas. El editor agrupa por carpeta, pregunta dónde crear y no crea carpetas (§11). -
Cerrada.
listaygrupodeclaran lo que llevan en su propiocampos, y ahí dentro solo caben campos simples (§11). -
Cerrada. En tipos y campos,
nombrees la clave ytitulolo que se enseña; los campos admitenayuday los tipos,plural(§11). -
Cerrada. Nuevo tipo de campo
enlace: una dirección completa o una ruta del propio sitio (§11). -
Cerrada. Una página que crea un editor se llama según el patrón
ficherodel tipo,{{ titulo|slug }}si falta; el nombre se fija al crearla y no cambia después (§11). -
Cerrada.
cuerpoadmite también la forma de un campo, con sutituloy suayuda;"markdown"a secas es la forma corta (§11). -
Cerrada. Una fecha sin comillas que no existe (
2026-13-01,2026-01-32) se lee como el texto que es y nunca detiene el build: enfechaypublicaravisa, como cualquier fecha imposible; en los demás campos llega así a la plantilla, sin aviso (§4). -
Cerrada. El motor trae también el alemán para
fecha(): meses y días con mayúscula, como se escriben, y «18. März 2025» por defecto (§15.9). Amplía la decisión 79. -
Cerrada. Un feed sin entradas lleva como fecha el 1 de enero de 1970 en la zona del sitio, y no el momento de la compilación: así no cambia de una compilación a otra (§10.2).
-
Cerrada. Una
listapuede llevar dentro otralistade campos simples, con dos niveles como máximo; ungruposigue admitiendo solo campos simples. Amplía la 83 (§11, §11.1). -
Cerrada.
traducibleen los campostextoyparrafo: el valor es un mapa por código de idioma, y elegir idioma y qué hacer si falta es cosa de la plantilla. En un sitio con idiomas, el editor guarda lo de cada traducción en su fichero y todo lo demás en el.ymlcomún de la página; los campos reservados de texto no pueden ser traducibles (§11.2). -
Cerrada. Nuevo tipo de campo
seleccion-multiple, conopcionesen un fichero dedatos/o en el esquema. Guarda la lista deidelegidos; una lista vacía es una respuesta y un campo que falta no lo es, yrequeridopide una respuesta (§11.3). -
Cerrada.
recuerdaen unalista: un fichero dedatos/con los campos simples de sus elementos, por una clave, que el editor completa al guardar y usa para sugerir. Se copia, no se enlaza, y el motor no lo lee (§11.4). -
Cerrada. El esquema puede declarar ficheros de
datos/editables, con la seccióndatos; un solo fichero por entrada, sin variantes por idioma (§11.5). -
Cerrada.
pdf: síhace que una página salga también como PDF, con su plantilla<plantilla>.pdf.twig, en la dirección de la página con.pdf. Es una salida más de la página y no un procesador del HTML; se genera con dompdf, una dependencia opcional (§4, §10.3, §13). -
Cerrada. El PDF es reproducible: lleva la fecha de la página (o el 1 de enero de 1970) y un identificador sacado de su contenido, y su caché va a la carpeta temporal del sistema. Si no se puede generar, el build se detiene (§10.3).
-
Cerrada. Los ficheros PHP no se copian: uno en
publico/o encontenido/detiene el build, también si.phpno es la última extensión (§4, §9). -
Cerrada. Un tipo del esquema que declara
orden, de tiponumero, se ordena a mano: el editor lo cambia arrastrando en el listado y no lo enseña en el formulario (§11). No cambia nada en el motor. -
Cerrada. Un tipo del esquema con
"unico": truees una página fija: el editor no deja añadir otra en su carpeta mientras haya una, ni quitar la que hay (§11). No cambia nada en el motor. -
Cerrada. El menú lateral del editor se ordena en secciones:
grupoen un tipo o en un fichero de datos,rotuloen un tipo de una sola carpeta yexcluiren un tipo, todos opcionales (§11). No cambia nada en el motor. -
Cerrada. Un tipo del esquema con
"borradores": truedeja que el editor pase sus páginas a borrador y las vuelva a publicar, en todos los idiomas a la vez, y solo deja quitar las que son borradores (§11). No cambia nada en el motor. -
Cerrada. Un tipo del esquema con
"programar": truedeja que el editor fije la fecha depublicarde sus páginas, en todos los idiomas a la vez;borradormanda sobrepublicary conunicono se ofrece (§11). No cambia nada en el motor. -
Cerrada.
pagina.markdownda el Markdown de una página sin el front matter y sin convertir, también en las páginas de una colección. Es lo que necesitan ficheros comollms-full.txt, que reúnen el texto de varios artículos. No existe en las páginas.twig(§7.2). -
Cerrada. Una página con
agrupares una plantilla que el motor repite por cada valor de un campo de las páginas de una colección: una página por etiqueta sin escribirla a mano, para vocabularios abiertos. Agrupa porslug, dagrupo(la variante más usada) ypaginas, no genera página para un grupo sin páginas publicadas y no es paginación, que sigue fuera de la v1. Es declarativa: no ejecuta código del sitio (§6.3). (La paginación entró después, en la 0.11: decisión 121.) -
Cerrada.
publicarEnSuFecha: sí, que se hereda por la cascada, hace que una página no se publique antes de sufecha, como si llevarapublicarcon ella. Por defecto unafechafutura sigue sin ocultar nada. Unpublicarexplícito manda sobre él, yborradorsobre los dos (§4). -
Cerrada. El patrón de
urltiene{{ nombreDeFichero }}, el nombre del fichero sin carpeta, extensión ni código de idioma, para publicar las páginas de una carpeta sin la carpeta en la URL con un solourlen su_datos.yml. No es un campo, y manda sobre uno que se llame igual; se llama así y noarchivoporquearchivoes un campo probable (§5.1). -
Cerrada. La sección
enlacesExternosdesitio.ymlañade a los enlaces externos del Markdownnoreferrery un texto para lectores de pantalla (aviso, uno por idioma en un sitio con varios, con unaclaseque define el sitio). Sin ella, los enlaces salen como hasta ahora. No es una plantilla: el vocabulario sigue cerrado, y el HTML y los enlaces de las plantillas no se tocan (§2, §8). -
Cerrada. La sección
encabezadosdesitio.ymlda unid(elslugdel texto, sin repetirse en la página) a los encabezados del Markdown de los niveles que se indiquen, añade al final de cada uno un enlace permanente si se pide (enlace,clase,etiqueta) y da a las plantillaspagina.encabezados, connivel,textoeid, para construir un índice. Es opcional: sin la sección, el Markdown sale como hasta ahora. No es un procesador sobre el HTML generado: actúa al convertir el Markdown, solo sobre sus encabezados, y no toca los de un atajo, los del HTML escrito a mano ni los de las plantillas (§2, §7.2, §8). -
Cerrada. Al ordenar textos (
orden, §6.2), los signos de apertura¿y¡no cuentan: el título «¿Cómo llegar?» se ordena como «Cómo llegar?», no como un texto que empieza por un signo que va antes que cualquier letra. En español esos signos solo abren una pregunta o una exclamación, y el diccionario no los tiene en cuenta. El resto de la puntuación sigue contando como hasta ahora. No cambia ningún texto, solo su posición en una lista. -
Cerrada.
css_fichero()yjs_fichero()enlazan el CSS y el JS que ha declarado la página en lugar de incrustarlos: el motor escribe el contenido que habría incrustado encss/<huella>.cssojs/<huella>.jsdesalida/, y las páginas que declaran lo mismo comparten el fichero. Lo elige la plantilla, nositio.yml, como todo lo que decide qué lleva una página;css()yjs()no cambian. El nombre lleva la huella del contenido, para que se pueda guardar un año sin riesgo de servir uno viejo. No minifica ni procesa nada: el contenido es el mismo que habría incrustado. No hay un fichero por cada declarado (con<link>por fichero se guardaría el común entre páginas, a costa de más peticiones): un paquete por combinación repite el CSS común en cada una, pero son pocos kilobytes comprimidos (§9.1). -
Cerrada.
version(ruta)da la dirección de un fichero depublico/con?v=y la huella de su contenido, para los ficheros que se enlazan sin pasar porcss()(§7.3, §9.1). Se llamaversiony noactivo, que ya es el del menú; y lleva la huella en la consulta y no en el nombre porque así funciona en cualquier alojamiento, sin reglas de reescritura. -
Cerrada. El filtro
markdownconvierte a HTML un texto en Markdown de línea, para los campos con formato (§7.3, §8, §11). El esquema ya ofrecía el tipomarkdownpara cualquier campo, pero solo el cuerpo se podía convertir: un titular con una palabra en cursiva, o un resumen con una negrita, no tenía forma de llegar a la plantilla como HTML. Por defecto es de línea porque la plantilla ya pone el contenedor (<h1>,<p>,<li>); el modo de bloque es la decisión 114. No reconoce los atajos, para que el vocabulario cerrado siga viviendo en un solo sitio y para que un campo no dependa de plantillas. Admite HTML escrito a mano, como el cuerpo, porque quien lo escribe es la misma persona. No es un procesador sobre el HTML generado: actúa sobre el texto de un campo, antes de que llegue a la plantilla, y no cambia ningún sitio que no lo use. -
Cerrada. El filtro
markdownadmite un modo de bloque,markdown('bloque')(§7.3). La 113 lo dejó fuera pensando que un texto con párrafos era el cuerpo de una página, pero una página de aterrizaje se compone de bloques (un manifiesto, un apartado de servicio) que llevan varios párrafos cada uno, y el cuerpo es uno solo y va entero en un sitio de la plantilla. Se descartaron dos salidas sin tocar el motor: una lista de párrafos en el front matter, que obliga a quien edita a escribir cada párrafo en su caja y a renunciar a encabezados y listas; y una ficha por bloque, con su propio cuerpo, que reparte una página en varias. Es un argumento del mismo filtro y no un filtro aparte porque lo único que cambia es si hay bloques, y el modo por defecto, el de línea, no cambia. El modo de bloque deja fuera lo que depende de ser el cuerpo de una página: los atajos, las figuras, las tablas, las notas al pie y losidde los encabezados. -
Cerrada. Los tipos fijos (
unico) de una misma sección del menú lateral pueden compartir una entrada con la claveentrada(§11). Salió de un sitio con muchas páginas distintas, cada una con sus campos y por tanto cada una su tipo: el menú tenía una entrada por página y el cliente, que solo quiere ver «Páginas», «Servicios» y «Diario», lo encontraba largo. Se descartaron dos salidas: un solo tipo con todos los campos de todas las páginas, que obligaría a un formulario enorme y a campos que no son de la página que se abre; y un submenú desplegable, que cambia solo cómo se pinta el menú pero deja el menú tan largo en cuanto se abre y es distinto de cómo funcionan las demás entradas (una entrada, un listado, una ficha). La entrada es solo para tipos fijos porque no hay un «añadir» que valga para varios tipos a la vez. No cambia nada en el motor. -
Cerrada. Un campo
imagenpuede declararcarpeta,anchoyaltpara que el editor deje elegir y subir imágenes en lugar de pedir la ruta escrita (§11, §11.6). Salió de un sitio cuyo titular edita casi todos los textos y lleva un blog: quien no escribe rutas no puede cambiar una foto, y las fotos del móvil pesan entre 3 y 10 MB. Se descartaron tres salidas: que el editor las reduzca por su cuenta, sin que el esquema diga cuánto, porque un fondo a todo el ancho y una foto de una ficha no piden lo mismo; guardarlas fuera de la carpeta del proyecto, que rompería la regla de que un proyecto exportado compila en cualquier máquina (§12); y generar varios tamaños, que sigue fuera de la v1 (§13):anchoes un solo máximo, no una familia de tamaños.carpetaes el interruptor: un sitio que ya usaimagencon la ruta escrita no cambia.altse declara y no se deduce del nombre porque no es el mismo en todos los sitios (imagenAlten una página,alten una lista de fotos), y una convención por nombre habría obligado a renombrar campos que ya están publicados. Las dimensiones no se guardan en el front matter, porque se desharían en cuanto se cambiara la imagen:dimensiones()(decisión 62) las da. No cambia nada en el motor. -
Cerrada. Nuevo tipo de campo
seleccion, una sola elección entre unasopciones(un fichero dedatos/o la lista del esquema, como enseleccion-multiple) que guarda elidde la elegida (§11, §11.3). Salió de un sitio con una lista agrupada a partir de un campo que escribe quien edita, y con unos filtros que comparan un texto exacto: con un texto libre, una errata o una mayúscula de más sacan la página del grupo o del filtro sin que nadie lo vea. Se descartaronseleccion-multiplecon una sola casilla (el valor sería una lista, y la plantilla tendría que sacar el primero), un texto libre con la ayuda de qué valores valen (la errata se publica) y una comprobación en el motor (el esquema no es cosa suya, §11). Unidque ya no está entre las opciones se conserva y se avisa, como en la múltiple, para que quitar una opción de la lista no cambie en silencio las páginas que la usaban. No cambia nada en el motor. -
Cerrada. Nuevo tipo de campo
fechas, un conjunto de días que el editor enseña como un calendario de meses y que se guarda como una lista YAML de fechas, ordenada, sin repetidos y una por línea (§11, §11.7). Salió de un sitio con un calendario de ocupación cuyo titular solo cambia qué días está ocupado, dos o tres veces al mes: con unalistade grupos con un campofechaserían unas ciento cincuenta filas al año, cada una con su «Añadir» y su «Quitar», y con unalista-textoquien edita escribiría las fechas a mano y la errata se publicaría. Una lista ordenada y sin repetidos hace que un cambio sea una línea en Git y que quien edita a mano las encuentre donde espera. Se descartaron los rangos (desdeyhasta), que ya se escriben con unalistay que un local que se alquila por días no necesita, y una nota por día (de quién es la reserva), que llevaría datos de personas al repositorio del sitio. Sinrequerido, porque una lista vacía es el estado normal; sintraducible, y hoy solo en el primer nivel de un tipo o de un fichero de datos, porque un calendario dentro de la fila de una lista no lo pide ningún sitio; se abrirá cuando alguno lo pida. No cambia nada en el motor. -
Cerrada. Clave nueva de un fichero de datos editable,
publicarAlGuardar(§11.5): al guardarlo, el editor publica el sitio en el acto. Salió del mismo sitio que la 118. Separar guardar de publicar es lo normal en un editor, para que nada salga sin que alguien lo decida, pero con los días ocupados de un calendario es una trampa: el titular marca el día, no pulsa «Publicar» y la web sigue enseñando libre un día ya cobrado. La clave se pone en el fichero que no puede esperar y no en todo el sitio, porque un aviso de cierre pide lo mismo y una ficha a medias, no. Publica el sitio tal como está guardado, no solo ese fichero, y si no puede, el editor lo dice y no da el cambio por publicado. Se descartaron una clave general del esquema (no distingue un dato urgente de una ficha a medias), un ajuste del editor por sitio fuera del proyecto (el zip del proyecto no se llevaría cómo se comporta) y un botón único «Guardar y publicar» para todo. No cambia nada en el motor. -
Cerrada. El motor lee las variantes de ancho de una imagen, con la sección
imagenesdesitio.yml(§2), la funciónvariantes()(§7.3) y elsrcsetdel atajoimageny de las figuras del Markdown (§8.2). No genera ninguna: la generación sigue fuera del build (§9, §13). Salió de que varios sitios, cada uno por su lado, preparaban sus fotos en WebP en varios anchos con un programa propio y las leían con una plantilla de<img srcset>copiada de uno a otro, con los anchos escritos a mano, y de que otro sirve otros anchos en AVIF: las convenciones empezaban a divergir antes de congelar el formato. Se descartaron: generar las variantes al compilar, porque un proyecto en zip dejaría de dar el mismo HTML en cualquier máquina (§12, regla 3: sin la extensión GD no habríasrcset, y con otra versión de ella los ficheros serían otros), exigiría una caché en disco que la compilación incremental no tiene y cargaría el proceso web con la decodificación de fotos de varios megapíxeles; descubrir los anchos buscandofoto-*.webpen la carpeta, porque uniphone-12.webpo unhoja-2.webppasaría por variante y la compilación incremental necesitaría un tipo de dependencia nuevo (el listado de una carpeta); escribir los anchos en cada llamada, porque repetiría la lista en cada plantilla y el atajo y las figuras no podrían usarla; y un solo formato, porque un sitio que ya sirve AVIF tendría que escribir su propia plantilla. La lista de anchos va ensitio.ymly no enesquema.jsonporque es del sitio y no del editor (§11): quien genere las variantes, sea un editor al subir la imagen o un programa aparte, y el motor leen la misma lista. El número del nombre es el ancho pedido y el delsrcsetel real del fichero, porque quien genera no amplía una foto estrecha. Una imagen sin ninguna variante sale en el atajo y las figuras como siempre y sin aviso, porque no todas se procesan; envariantes()avisa, porque quien la llama las pide.sizesno lo sabe el motor: lo da la plantilla, o la sección, o el atributo del atajo, y por defecto es100vw, que no pide menos imagen de la que hace falta. Los sitios sin la sección no cambian. -
Cerrada. Una página con
paginarparte una colección en trozos y se repite una vez por cada uno (§6.4): la primera está en suurly el motor genera las demás. Sale a la vez que la 120 por la misma razón, que la 1.0 congela el formato y esto toca la URL, que es contrato. Hasta ahora, un sitio que venía de Eleventy con una lista paginada la sustituía por páginas sueltas escritas por el programa de migración, y esas páginas no se rehacen cuando se publica una entrada: la última página se queda corta y la siguiente nunca se crea. Es una clave nueva,paginar, y no una ampliación deagrupar: esta es lo contrario (reparte por valores, no por tamaño, y sucampono tiene sentido para el blog entero, §6.3). Se descartó dejarlo a mano con filtros (desde(n)), porque no resuelve que una entrada nueva cree su página. La URL de las páginas a partir de la segunda es, por defecto, la de la primera máspagina/N/, en español y sin chocar con un artículo llamado2; y se puede cambiar conpaginar.url, con{{ numero }}(§5.1), porque quien migra no puede perder direcciones y cada generador numera a su manera (/blog/2/,/page/2/). Las páginas generadas no entran en colecciones ni en el sitemap, para que «Blog, página 2» no salga en un menú ni en un listado. Partir cada grupo deagruparqueda reservado: una página con las dos claves avisa ypaginarse ignora. Ningún sitio lo usa hoy, y definirlo después no rompe nada, mientras que definirlo ahora fijaría algo que nadie ha probado. La paginación se calcula antes de cualquier plantilla y no ejecuta código del sitio (§12, regla 4). -
Cerrada. Un proyecto declara para qué versión del formato está escrito con
formato: 1ensitio.yml(§2), y un editor con"formato": 1enesquema.json(§11). Salió de preparar la 1.0: sin una declaración, una versión mayor futura no podría reconocer los proyectos antiguos y tendría que adivinarlos. Es solo la versión mayor, porque dentro de una el formato solo crece (§12, regla 5), y una menor ("1.3") haría fallar en un motor más viejo a un proyecto que no usa nada de lo nuevo, y cada sitio tendría que subirla a mano. Sin la clave, el formato es el 1 y no se avisa: los proyectos escritos antes no cambian. Un valor que no es un entero mayor que cero se avisa y se toma como 1. Si es mayor que el que entiende el motor, se detiene el build con un error (Formato): la regla 10 del motor deja parar cuando es imprescindible, y compilar a ciegas otro formato daría un sitio mal hecho sin que nadie lo notara; un aviso se perdería entre los demás. Se descartó dejar el esquema fuera de la promesa de la 1.0, porque otro editor no tendría ninguna garantía sobre él: el §11 es contrato como el resto, y crecer con un tipo de campo nuevo no lo rompe. -
Cerrada. La promesa de compatibilidad de la 1.0 cubre tres cosas: el formato (este documento), la orden de consola y una lista corta de la biblioteca PHP, documentada en
biblioteca.md; el resto queda@internaly puede cambiar en cualquier versión (§12, reglas 5 y 6). La lista sale de lo que usa el primer programa que incrusta el motor, un editor web: compilar (Compilador,CompiladorEnProceso,Informe), leer un proyecto (Proyecto,Lector,Lectura,Sitio,Pagina,Formato), los avisos y los errores (Aviso,Avisos,ErrorDeProyecto),Slugy el despliegue. Se descartaron cubrir todo lo que no es@internal, unas treinta clases con todos sus métodos, porque cualquier refactor interno sería un cambio mayor; y no prometer nada de la biblioteca, porque quien la incrusta quedaría fijado a una versión menor del motor y habría que revisarlo en cada una. De cada clase se promete lo que el documento nombra y no lo demás aunque PHP lo permita (Sitioes público, pero noSitio::$encabezados).FrontMatteryYamlsiguen internos: el editor web solo los cita en comentarios. La regla de retirada: lo que va a dejar de valer avisa durante, al menos, una versión menor antes de quitarse. -
Cerrada. La compilación por lotes (§10.4) entra en la 1.0. Estaba aplazada desde la decisión 51 y la web de Ehundu la prometía para la 1.0: es la razón de ser del proyecto, compilar también desde un proceso web con los límites que eso trae, y deja una huella en el formato, la carpeta
.ehundu/(§1), que conviene reservar antes de congelarlo. Se mide así: en los sitios de prueba, volver a leer el proyecto en cada llamada cuesta 0,7 s en el mayor (1.200 páginas, 40 a 50 s de compilación), así que el estado no necesita guardar nada del sitio, solo por dónde va. Es una interfaz nueva,CompiladorPorLotes, conLotecomo resultado:CompiladoryCompiladorEnProcesono cambian, y añadir un parámetro o un resultado aCompilador::compilarhabría roto a quien la implemente. El estado lo guarda el motor en.ehundu/lote.json, no quien llama: así funciona igual desde un editor web, un cron o un programa de otra persona, sin que cada uno invente dónde guardarlo. Cuesta la regla de que el motor solo escribe ensalida/, que pasa a ser «y en.ehundu/», y se evita que ensucie un repositorio con una carpeta que escribe él mismo con su propio.gitignore. Se descartó guardar cada página en una carpeta aparte y cambiarla de golpe al final, que haríasalida/siempre coherente pero copiaríapublico/otra vez (puede ser de gigas), y escribir cada página al hacerla dejasalida/mezclada hasta el último lote, que el lote reconoce concompleto(). La compilación incremental guardada entre peticiones no entra: es otra función, que los lotes no impiden. (La decisión 128 los sacó después de lo que promete la 1.0.) -
Cerrada. El despliegue también se reparte en lotes.
Desplegadoracepta un presupuesto de tiempo ($segundos): cuando se agota, guarda el manifiesto y para, y lo dice, y la llamada siguiente sigue donde se quedó. Y acepta unInformeya hecho ($compilado), para no volver a compilar en cada llamada cuando la compilación se hizo por lotes. El manifiesto del destino ya guarda lo subido cada 50 ficheros o 15 segundos (decisión 53), así que cortar la subida y retomarla ya era posible; lo nuevo es parar a tiempo y decirlo (InformeDeDespliegue::$completo). Salió de que una petición que compila en lotes y luego sube de golpe se pasaría igual del límite en un sitio con muchos ficheros. -
Cerrada. La compilación incremental sale del proceso:
.ehundu/memoria.binguarda lo que ya recuerda, yCompiladorIncrementalEnProcesolo usa (§10.5). Sustituye en parte a la decisión 50. La decisión 124 la había dejado fuera, y salió de usar los lotes en un editor web con el sitio más grande de los de prueba (1.200 páginas): guardar un cambio de una página costaba una compilación entera, de 35 a 45 s, porque los lotes no recuerdan nada, y la incremental, que dentro de un proceso rehace esa página en 0,7 s, no se podía usar porque lo que recuerda no sobrevivía a la petición. Se midió que la memoria entera pesa 97 MB y se carga en 0,06 s: no hacía falta inventar nada más pequeño. Se guarda tal cual, con la versión del motor, y se ignora sin error si no vale, porque el resultado tiene que ser siempre el de una completa y el motor no puede fiarse de algo que escribió otra versión de sí mismo. El trabajo por presupuesto no necesita un estado como el de los lotes: una memoria con la mitad de las páginas es una memoria a la que le faltan páginas, y el plan de la incremental ya rehace las que faltan y aprovecha las que valen, también si el proyecto cambia entre dos llamadas (con los lotes, empezaba de nuevo). Los lotes se quedan como están, porque la decisión 124 ya los promete (la 128 retiró después esa promesa), y esto los complementa: son los que reparten una compilación completa sin recordar nada. Cuesta lo que pesa la memoria, en disco y en el proceso que la carga, y que la primera compilación tras cada cambio de versión del motor sea completa. Se descartó guardar solo lo que usó cada página y volver a leer el HTML desalida/(ahorra la mitad, pero depende de que nadie toquesalida/y complica el motor), y dejar el trabajo al editor con una compilación posterior a la respuesta, que esconde la espera pero no la quita y deja la copia ocupada. -
Cerrada. El motor marca las páginas con
<meta name="generator" content="Ehundu">, por defecto y congenerador: nopara quitarla (§2). Salió de querer que los sitios hechos con Ehundu se reconozcan, como los de otros generadores. Se decidió que el contenido sea soloEhundu, sin la versión: con ella, cada actualización cambiaría el HTML de todas las páginas de todos los sitios y obligaría a volver a subirlas, y en un editor todo aparecería como cambios sin publicar sin que nadie hubiera tocado nada; además, daría a quien mira el código de una web la versión exacta del motor. Y que la añada el motor, antes de</head>, en vez de una función que cada plantilla tenga que escribir, porque la marca solo sirve si sale en todos los sitios sin que cada uno se acuerde de ponerla. Es una excepción a que el motor no toca el HTML de la página, que son las plantillas del sitio; por eso se limita a una sola línea, se respeta la etiqueta de quien ya la tiene y se puede quitar con una clave. Cuesta que, al actualizar el motor, cada página de cada sitio lleve una línea más, así que cada sitio tiene que volver a publicarse una vez; no pasa de una vez a otra, porque la etiqueta no cambia con la versión. Se descartó una función de plantilla ({{ generador() }}) por no ser por defecto, y poner también la versión en el feed (<generator version="…">de Atom), porque el feed cambiaría con cada versión como las páginas. -
Cerrada. La compilación por lotes (§10.4) deja de ser parte de lo que promete la 1.0:
CompiladorPorLotes,CompiladorPorLotesEnProcesoy su estado en.ehundu/lote.jsonpasan a@internal, y lo que se promete para repartir una compilación en llamadas con un presupuesto de tiempo esCompiladorIncrementalEnProceso::avanzar(§10.5), que devuelve el mismoLote. Corrige la decisión 124, que los prometía, y la 126, que los dejaba «como están, porque la 124 ya los promete». Salió de revisar lo que se iba a congelar: la lista pública tendría tres maneras de compilar, y dos de ellas con unavanzarcasi igual, así que quien incruste el motor se encontraría con dos puertas que hacen lo mismo. Y los lotes traen rarezas que la incremental no podría imitar sin cambiar de significado:iniciar,estadoyabandonar(un estado que se pueda leer sin cargar la memoria entera, descartar un lote),Lote::reiniciado()(siempre falso en la incremental) y unLote::$totalque cuenta tareas en unos y páginas en la otra. Además, mantener las dos obliga a mantener dos implementaciones del bucle de páginas hasta una versión mayor. Prometer es irreversible dentro de una versión mayor y no prometer no lo es: añadir algo a la lista en una versión menor no cuesta nada y quitarlo exige una mayor. Se descartaron mantener la clase pública prometiendo soloavanzaryLote(conserva lo único que los lotes hacen mejor, compilar un sitio grande con unas decenas de megas por llamada frente a las centenas de la incremental, pero deja dos clases públicas con el mismoavanzar) y dejarlo como estaba (congela las rarezas). El código se queda,@internal, y se decidirá si se borra cuando la incremental pueda guardar su memoria por partes. No cambia nada para quien los use hoy, solo que no se promete. La decisión 125 (el despliegue repartido en llamadas) no cambia. -
Cerrada. Una fecha (
fecha,publicar) es un día y la hora no cuenta (§4). Si se escribe con hora o con zona (2026-10-01 18:30:00,2026-10-01T18:30,2026-10-01T18:30:00+02:00), se avisa y se toma solo el día, desde su medianoche en la zona del sitio. Salió de revisar lo que se iba a congelar: el motor ya aceptaba la hora sin que el contrato lo dijera (la tenían en cuenta el orden de los listados y el feed, ypublicarypublicarEnSuFechapublicaban a esa hora, contra lo que decía el §4), pero solo con segundos o en formato ISO:09:00lo leía YAML como texto, el motor avisaba de que no era una fecha y publicaba la página en el acto. Documentarla habría prometido la forma exacta que acepta la dependencia que lee el YAML, y habría cambiado lo que tendría que ofrecer un editor. Y una hora sirve de poco en un sitio estático, que solo saca la página cuando se vuelve a compilar después de ese momento (§4,publicarEnSuFecha), es decir, si hay una compilación a esa hora. Ningún sitio de prueba la usaba. Se descartó documentarla y dejarla sin decir, que funciona por accidente y podría cambiar con una actualización de esa dependencia sin que nadie lo notara. Cuesta no poder publicar a una hora concreta ni ordenar por hora dos entradas del mismo día, que quedan en el orden que tenían. Añadirla más adelante es crecer: lo que hoy avisa pasaría a valer. Un día escrito con hora cero y sin zona (2026-10-01 00:00:00) no se distingue de un día y no avisa. -
Cerrada. La
urldesitio.ymles la del dominio, sin ruta (§2):https://www.ejemplo.com, con o sin la barra final; con una ruta o con parámetros es un error, ysitio.url, en las plantillas, no acaba nunca en/. Salió de lo mismo: el motor aceptabahttps://ejemplo.com/sub/y dejaba el sitio a medias, con el sitemap, el feed y lo que se escribe con{{ sitio.url }}bajo/sub/, pero conpagina.url, los enlaces internos,css_fichero(),version()y las imágenes colgando de la raíz. Admitir la ruta de verdad pide prefijar todas las direcciones internas y decidir qué valepagina.url: si lleva el prefijo, las plantillas que escriben{{ sitio.url }}{{ pagina.url }}lo repetirían, y si no, no serviría de enlace. Es un cambio grande, que retrasaría la 1.0 y que ningún sitio pedía. Documentar el soporte a medias habría sido prometer lo que no funciona. Cuesta que un sitio en una subcarpeta no se pueda hacer con Ehundu hasta que se soporte, y que el build se detenga a quien tuviera hoy una ruta en laurl. Pasar de error a soportado es crecer y no cierra ninguna puerta. -
Cerrada. Una imagen enlazada a otro dominio lleva
target="_blank"yrelcomo cualquier enlace escrito en Markdown (§8): la figura de una imagen sola enlazada y el atajoimagenconenlace. Salían sin ellos, contra lo que decía el contrato («un enlace escrito en Markdown a otro dominio recibetargetyrel»). La plantillaimagenrecibe dos variables nuevas,externoyrel, y la de Ehundu las usa; una plantillaimagendel sitio que no las use queda como la escribió, porque lo que escribe el sitio no se toca (§8). Es externo por la misma regla que los enlaces del Markdown: tiene dominio y no es el del sitio, con y sinwww.; una ruta, un ancla o unmailto:no lo son. No lleva el aviso para lectores de pantalla, como ya decía el §2: un enlace que solo envuelve una imagen no tiene texto al que añadirlo. Ninguno de los sitios de prueba cambia: las tres figuras enlazadas a otro dominio que tenían ya llevabantargetyrelescritos a mano. Se descartó dejar las figuras como estaban y corregir el contrato, porque la regla de abrir en otra pestaña los enlaces a otros dominios no se cumpliría en las figuras, y una clave nuevafiguras: síenenlacesExternos, que sería una opción para algo que casi todos querrán. -
Cerrada. «La v1» deja de significar el alcance de las primeras versiones: el formato es el formato 1 (
formato: 1, §2), y lo que el contrato admite hoy o no hace todavía se dice «hoy» o «todavía». Salió de revisar lo que se iba a congelar: «la v1» se usaba para tres cosas (el formato 1, el alcance de las primeras versiones y la propia 1.0), y leída como el formato 1, el §13 prometía que en toda la 1.x no llegarían la generación de imágenes, los componentes ni la búsqueda, lo que choca con la regla 5, que deja crecer, y con lo que ya estaba planeado después de la 1.0. Los destinos, los tipos de campo y el aviso de los ficheros PHP dicen «hoy»; el §13 se llama «Lo que Ehundu no hace (todavía)» y dice que lo que lista puede llegar dentro de una versión mayor si es aditivo;componentessigue reservado, sin decir «v2». La regla 4, que el motor no ejecuta código propio del sitio, se queda como promesa explícita de la versión mayor: es lo que permite alojar el motor en cualquier hosting compartido sin riesgo. Las decisiones anteriores conservan sus palabras, con una nota al empezar este apartado. Se descartó tomar «v1» al pie de la letra, que convertiría el §13 en la promesa de que eso solo llegará con un formato 2, coherente pero con puertas cerradas que el plan quiere abrir, y partir el §13 en «para siempre» y «por ahora», que obliga a decidir hoy qué es para siempre y casi nada lo es salvo la regla 4. -
Cerrada. «Solo crece» (§12, regla 5) tiene una excepción, la de los nombres: un campo reservado nuevo o un nombre nuevo que el motor escriba en
paginapuede tapar un campo propio con el mismo nombre que un sitio ya tuviera, y por eso un nombre nuevo se anuncia en una versión menor y el motor que lo añade avisa a los sitios que ya lo usan. Los nombres que el motor escribe enpaginase juntan en una tabla (§7.2), y los cinco que escribe siempre (ruta,contenido,markdown,encabezadosytraducciones) avisan ahora si el front matter los usa, y el campo se ignora, como ya hacíaidioma: hasta ahora lo tapaban en silencio. Salió de revisar lo que se iba a congelar: había once nombres repartidos por el documento, cada párrafo avisaba por su lado de que tapaba un campo, y la regla 5 prometía lo contrario. Un sitio de prueba usagrupocomo campo propio, sinagrupar: es el tipo de nombre que un sitio escoge, y por esogrupo,paginasypaginacionsolo tapan en las páginas que generaagruparopaginary no avisan antes. Se descartó reservar ahora un conjunto amplio de nombres, que reservaría para siempre palabras comunes y avisaría a sitios que hoy funcionan, y mover lo que añade el motor a un espacio propio (pagina.motor.grupo), que es lo más limpio pero rompe las plantillas escritas y sería una versión 2. Ningún sitio de prueba usa los cinco nombres, así que no cambia ninguno. -
Cerrada. La tabla de letras del filtro
slugy la conversión de&eny(§5.2) son parte del contrato y no cambian dentro de una versión mayor. El §5.2 enumeraba unas pocas letras y la tabla real tenía bastantes más (ð,đ,ø,ł,ı,ſ,þ,ij…), así que dos implementaciones que siguieran solo el texto habrían dado direcciones distintas; y la tabla no solo fija las URL, también el orden alfabético de los textos, los grupos deagrupary losidde los encabezados. Se copia entera al §5.2, con una prueba que compara la del documento con la del código, letra a letra. Cerrarla cuesta que no se pueda añadir una letra, porque cambiaría la dirección de toda página cuyo título la lleve, que hoy sale con un guion: las de otros alfabetos y las del latino que la tabla no recoge (el vietnamita) siguen desapareciendo, y quien las necesite podrá tener más adelante una opción por sitio, que añade y no toca esta tabla.&seguirá dandoy, también en un título en inglés. Se descartó ampliarla ahora (griego, cirílico,ə), que exige elegir convenciones de transliteración que ningún sitio necesita, y dejarla como detalle de implementación, que era lo que había y que impide reproducir las direcciones de otro. Ninguna dirección cambia: la tabla es la que ya tenía el código. -
Cerrada. Un punto en el nombre de una página, de un fichero de
datos/o de un_datos.ymlque no es el de un código de idioma detiene el build con un error que nombra el fichero (§15.2), también en un sitio de un solo idioma. Antes se saltaba el fichero con un aviso y código de salida 0:guia.v2.mdno salía en el sitio, y el siguiente despliegue la borraba del servidor, que es justo lo que el §4 evita para un front matter roto. Lo mismo valía para_datos.v2.yml, que quita de una carpeta lo que tenía en común. Salió de revisar lo que se iba a congelar: el §15 dice que un sitio de un solo idioma «no cambia en nada», y no era verdad para los nombres con puntos, que afectarán a migraciones de blogs con nombres como2024.10.01-titulo.md. Es un error cuando lo que hay tras el último punto no tiene forma de código (dos o tres letras minúsculas), o cuando hay otro punto antes; si tiene forma de código pero el sitio no lo declara (contacto.fr.md), sigue siendo un aviso, porque puede ser una traducción preparada de antemano. Queda por tanto un caso en aviso que se parece: una plantillasitemap.xml.twig, cuyoxmltiene forma de código. Se descartó aceptar los puntos en los sitios de un solo idioma, porque uno que pasara a bilingüe vería cambiar el significado de sus nombres y dejaría de ser cierto que se puede pasar a bilingüe sin renombrar nada, y dejar el aviso y corregir el texto del §15, que quedaría documentado pero con el mismo riesgo de borrar una página sin querer. Ningún sitio de prueba tiene un punto que no sea el de un idioma, así que no cambia ninguno. -
Cerrada. Lo que el motor hacía y solo estaba en el código queda escrito en el contrato, sin cambiar lo que hace, porque con la 1.0 pasa a ser promesa. Salió de revisar el contrato contra el código: en una página generada,
traduccionessolo trae la propia (§6.3, §6.4, §15.8) y el selector de idioma que nunca enlaza a una URL que no existe es de la plantilla recomendada, no del motor; las páginas deagruparsalen en el sitemap y entodoy heredanlistada: no(§6.3); qué es un enlace a otro dominio, que hasta ahora decidía la biblioteca de Markdown: una dirección con dominio, o que empieza por//, que no es el del sitio, y sale sin dirección si su esquema es inseguro (§8); las reglas exactas de los atajos (nombres de atributo, comillas sin escape, las 30 etiquetas que cuentan como bloque, que gana la variable del motor sobre el atributo del mismo nombre, que el pie del atajo es texto y el de la figura Markdown, y que lo que escriben las plantillas de Ehundu va en castellano:Vídeo,1,2 MB,sí/no; §8.1, §8.2); el orden de los textos, que compara el valor de cada carácter y no es el de un diccionario entero (§6.2); que{{ grupo }}y{{ grupo|slug }}valen en el patrón deagrupar; quesí/nono distingue mayúsculas y"true"entre comillas no vale (§4); cómo se leen los nombres de los ficheros dedatos/en Twig (§3); qué es una sección vacía desitio.ymly qué errores detienen el build y cuáles se avisan (§2). Y lo que no es contrato sale de él: las medidas de rendimiento van acompilacion.md, ylote.jsonymemoria.binno se prometen, mientras que el manifiesto del despliegue sí se lee en cualquier 1.x (§10). Se descartó cambiar el comportamiento antes de escribirlo, que ninguna de estas rarezas merece, y dejarlas sin decir, que es dejar que dos implementaciones las resuelvan distinto. Con una excepción, que sí cambia lo que hace: unformatoque no es un entero mayor que cero ("2"entre comillas,0) detiene el build en vez de avisarse y tomarse por 1, porque la decisión 122 existe para no leer a ciegas un proyecto de otro formato; ningún sitio de prueba lo escribía así. -
Cerrada. El contrato promete el mismo HTML con la misma versión del motor y de sus dependencias, y no entre versiones: lo que cambie al actualizar se anota en las notas de la versión (§12, regla 3). Salió de revisar lo que se iba a congelar: «las dos únicas formas» de que el mismo proyecto dé otro HTML (
random()y la fecha del momento) estaban incompletas, porque qué páginas salen también depende del reloj (publicar,publicarEnSuFecha), y la regla de que todo lo que afecta al resultado vive en la carpeta no incluía el reloj; y el contrato no decía si el mismo proyecto da el mismo HTML con dos versiones del motor, cuando Twig, CommonMark y dompdf, que Composer deja actualizar dentro de su versión mayor (^3.30,^2.10,^3.1), deciden partes del resultado. Importa no obligar a subir de nuevo todas las páginas (la decisión 127 lo muestra), y por eso el motor lo cuida, pero no lo promete. Se descartó prometer estabilidad byte a byte, que exigiría un corpus de referencia y fijar las dependencias a una versión menor, de modo que una corrección de seguridad de Twig obligaría a publicar una versión de Ehundu, y prometer solo lo que el contrato nombra, que deja sin decir justo lo que más puede cambiar. Cuesta que uncomposer updatepuede cambiar bytes de un sitio sin que Ehundu cambie; el aviso es la nota de la versión. -
Cerrada. Cuatro ajustes pequeños que, con el contrato congelado, ya no se podrían hacer sin una versión mayor.
publico/deja de copiar lo que dejan el sistema y Git (.DS_Store,.gitkeep,Thumbs.db,desktop.iniy cualquier.git), ycontenido/Thumbs.dbydesktop.ini: una lista corta y cerrada, porque añadir una exclusión después haría desaparecer ficheros de la salida;.htaccessy.well-known/se copian, y ningún sitio de prueba tenía ninguno de esos ficheros (§9). El feed escribe como<id>de cada entrada su dirección completa y como autor elnombredel sitio, y eso se promete (§10.2), porque un lector de feeds identifica las entradas por<id>y, si el criterio cambiara, cada una aparecería como nueva; no se añade una clave de autoría, que sería crecer. Un código de idioma con región (pt-br) ya era un error y se escribe que esa forma queda reservada, sin implementarla (§15.1); y se escriben el formato de fecha por defecto de los idiomas sin tabla y que solo cuatro letras se traducen (§15.9). Ycomponentes, reservado desde la decisión 68, avisa si un sitio lo usa como campo propio, aunque sigue llegando a la plantilla (§13). Se descartó ignorar todo lo que empieza por punto salvo.htaccessy.well-known/, que haría desaparecer ficheros que alguien puso a propósito, y dejarpublico/como estaba avisando de que se limpie. -
Cerrada. El esquema (
esquema.json) se describe en un documento aparte,esquema.md, que conserva la numeración del §11 y separa el formato, que es contrato, de cómo se comporta el editor, que es orientación. Salió de revisar lo que se iba a congelar: el §11 mezclaba las claves y los tipos de campo con decisiones de interfaz de un editor concreto (el calendario de trece meses, cómo se trata una imagen al subirla, cómo se rotula el menú lateral, que el editor no crea carpetas), y congelarlas todas habría fijado en un contrato lo que otros editores tendrían que implementar tal cual. Ahora lo que declara el esquema se promete y solo crece, lo que cuenta el primer editor va en citas marcadas «Orientación para el editor» y no vincula, y el documento añade lo que faltaba: un editor conserva y avisa de las claves y los tipos de campo que no conoce, para no tener que entender todos los 1.x; «error del esquema» queda definido, y lo detecta el editor al leerlo; y una tabla junta las palabras que significan cosas distintas según dónde estén (carpeta,fichero,grupo,excluir,orden), que ya no se pueden renombrar.Formato::leerqueda descrita para que un editor la use con su esquema. Se descartó dejarlo todo como contrato, que congela la interfaz del primer editor, y solo marcar los párrafos en el propio §11, que deja el contrato más largo sin separar nada. -
Cerrada. Estructura y documentación antes de la 1.0. El registro de decisiones pasa al final del documento, como apéndice A, para que quien lee el contrato encuentre la norma antes que su historia; no se renumeran las secciones, porque los mensajes del código y otros documentos citan
§15.x, y el §14 queda como un resumen que lo enlaza, con su título de siempre. La documentación no se promete en inglés: el contrato y el README dicen que está en español, porque entregarla con la 1.0 es mucho trabajo y una promesa sin fecha no es contrato.biblioteca.mdnombra los parámetros deDesplegador::desplegary deejecutar, que se pasan por nombre y por tanto son API, y describe qué hay dentro dePagina::$camposy deLectura::$datos, que quien los lee necesitaba saber del código. Y la orden de consola traeehundu --version, que es aditivo pero que una orden 1.0 no debería no tener, porque dificulta dar el dato en un informe de error y la etiquetageneratorno lleva la versión a propósito (decisión 127). La versión es una constante,Version::ACTUAL, que se cambia al preparar cada versión y lleva-deventre una y otra. Se descartó sacar las motivaciones largas del registro a otro documento, que deja una línea por decisión pero pierde el porqué junto a lo decidido, y no tocarlo. -
Cerrada. La memoria de la compilación incremental se guarda y se lee registro a registro, y si no cabe en el
memory_limitde PHP se avisa en vez de agotarlo (§10.5). Salió de preguntarse si la incremental servía para lo que Ehundu promete, compilar en cualquier alojamiento compartido, donde 128 o 256 MB son lo corriente. En el sitio más pesado de los de prueba (97 MB de memoria guardada), el pico de PHP era de 332 MB en la primera compilación, 352 al cambiar una página y 416 al cambiar una plantilla, de los que unos 160 eran solo la cadena entera que se construía al guardar y al cargar. Ahora un registro por cada cuerpo y por cada página, con la cabecera delante: los picos bajan a 170, 190 y 252 MB (en sitios de 27 a 29 MB, de 102-134 a 50-82 MB), con el mismo fichero. El formato del fichero es interno (Almacen::VERSION), no se promete (decisión 136), y la primera compilación tras actualizar es completa, como siempre que cambia. El resguardo compara lo que mide el fichero con lo que queda dememory_limitantes de cargarlo, con tres veces su tamaño más 16 MB (lo medido es 2,5 y unos 10, con un 20 % de holgura): sin él, un cron con el límite de consola de 128 MB que encuentra la memoria de un proceso web de 512 terminaba con el error de PHP, que no dice qué hacer.compilarpuede dar lo mismo con otro coste, así que compila entero y avisa;avanzarno, porque repartir el trabajo sin memoria obligaría a empezar de cero en cada llamada. No cubre la primera compilación, que no tiene fichero que medir. Se descartó quitar los cuerpos que ninguna otra página lee (el fichero baja un 28 %): reconvertirlos al cambiar una plantilla duplicaba el tiempo de rehacer (de 9 a 20 s en el sitio más pesado), y no guardar en memoria el HTML de las páginas que no cambian, que bajaría el pico a unos 50 MB, queda para si hace falta: obliga a cambiar cómo el compilador entrega las páginas a quien las escribe. -
Cerrada. Los nombres siguen una regla, escrita en el §12 (regla 7), y
paginacion.cantidadpasa a llamarsepaginacion.entradas. Salió de revisar lo que se iba a congelar: quedan para siempre los nombres que hay, porque quitar uno es una versión mayor (§12, regla 5), y se miró cuáles confundían.css_fichero()yjs_fichero()son la única mezcla de guion bajo y camelCase que hay, pero no es un descuido: las funciones y los filtros de Twig se escriben así (number_format,json_encode,date_modify), las demás funciones de Ehundu son de una palabra, y el camelCase es de los campos, las claves y las variables (porPagina,imagenAlt). Quedan como están, con la regla escrita para que lo sea. Renombrarlas obligaba a cambiar la plantilla base de nueve sitios, dos de ellos del panel, a la vez que el motor, porque un sitio con el nombre viejo dejaría de compilar; un alias (cssFichero) deja dos nombres para siempre sin que nadie lo haya pedido, y se puede añadir en cualquier versión menor si alguien lo echa en falta. Enpaginacion,totales el número de páginas ycantidadera el de entradas de la colección, y esa palabra no decía de qué. Como ningún sitio usapaginar(llegó en la 0.11 y solo se ha probado en copias), el cambio no cuesta nada ahora y después sí: se renombra, sin alias, que dejaría el nombre ambiguo para siempre.totalsigue siendo el de páginas, que es lo que pide el uso corriente («Página 2 de 5» esactualytotal). Quien usarapagina.paginacion.cantidaddesde la 0.11 tiene que cambiarlo, y no lo avisa nada: Twig pinta vacío un nombre que no existe. No se tocapaginar.ordenfrente al campoorden: son de la misma familia (el criterio de un orden y el número con que se ordena a mano) y ya están en la tabla de palabras repetidas deesquema.md, que ahora enlaza la regla 7. Tampoco queprimera,ultima,anteriorysiguientedepaginacionsean URL mientras que los filtrosanterior()ysiguiente()devuelven una página: está dicho en el §6.4, yenlacesda lo necesario para un índice. Se descartaron conservarcantidadcomo alias y renombrar tambiéntotal(totalPaginas,totalEntradas), que rompe el uso más común para ganar poco. Ningún sitio de prueba cambia. -
Cerrada. Un nombre con la extensión de un fichero que se quiere generar (
robots.txt.twig,sitemap.xml.twig,llms.txt.md,tabla.csv.yml) detiene el build, y el error dice cómo hacerlo: una páginarobots.twigconurl: /robots.txt(§4, §15.2). Salió de la relectura final del contrato: el punto del nombre está reservado para el idioma, y tres letras minúsculas tienen forma de código, así querobots.txt.twigse saltaba con un aviso y un código de salida 0; el siguiente despliegue borraba elrobots.txtpublicado, que es justo lo que la decisión 135 evita conguia.v2.md. La 135 lo dejó como el caso que se parecía y no resolvió. Es un patrón corriente al migrar desde Eleventy, donde el fichero se llamarobots.txt.njk. La regla es una lista corta y cerrada de extensiones (txt,xml,json,css,js,html,htm,svg,csv,yml,yaml,md,php,ico,pdf), que no pueden ser un idioma: añadir una a la lista después sería hacer error lo que hoy es un aviso, así que se escribe completa. Un código de idioma que el sitio no declara (contacto.fr.md) sigue siendo un aviso, porque puede ser una traducción preparada. Se descartó dejarlo como estaba y documentar la salida, que mantiene el riesgo de borrar lo publicado sin que nadie lo vea. Ninguno de los sitios de prueba tenía un nombre así. -
Cerrada. Un día sin hora y sin comillas es ese día en la zona del sitio en cualquier campo, no solo en
fechaypublicar(§4). YAML lee2025-03-19como su medianoche en UTC; el contrato decía que todas las fechas se escriben en la zona del sitio, pero solofechaypublicarse movían. Con una zona al oeste de UTC,|fechade un campo propio, dedatos/o desitio.ymlescribía el día anterior (America/Mexico_City: el 18 de marzo para el 19). Ahora esos días son medianoche en la zona del sitio, a cualquier profundidad de listas y mapas. Una fecha con hora (2025-03-19 10:30:00) se deja como la lee YAML, un instante: la decisión 129 es defechaypublicar, y partir en dos lo que hace un campo propio con una hora no es necesario hoy. Una fecha llega a la plantilla como una fecha, no como un texto, y se escribe confechaodate;{{ pagina.fecha }}sin filtro detiene el build. Se descartó dejar las fechas de los campos propios como instantes UTC y escribirlo en el contrato, que obligaba a cada plantilla a corregir la zona. Ninguno de los sitios de prueba cambia: están enEurope/Madrido sin zona, y no usan fechas propias al oeste de UTC. -
Cerrada. Tres reglas que el contrato no decía. (a) Lo único de
@ehundu/que una plantilla puede incluir son las plantillas de los atajos que trae el motor, y no se prometen: un sitio no debe incluirlas, y pueden cambiar o quitarse en cualquier versión; el contrato decía que Twig no lee fuera deplantillas/yparciales/, y sí lee el espacio@ehundu. Se descartó bloquearlo, porque el motor dibuja sus atajos con esas mismas plantillas y el cargador no sabe quién las pide. (b) Un campo en blanco (plantilla:) cuenta como un valor y sustituye al heredado de un_datos.yml, salvo enetiquetas,cssyjs, que se suman: es lo que hacía el motor y cambiarlo era reinterpretar proyectos escritos. (c) La excepción de la regla 5 de §12 (los nombres) cubre también una clave nueva desitio.ymly una variable nueva de un atajo, que pueden tapar una clave o un atributo propios; y §2 lista las claves que el motor lee desitio.yml, para que se sepa cuáles son suyas. -
Cerrada. Cuatro ajustes de la biblioteca y del despliegue que salieron de leer
biblioteca.mdcontra el código. (a)Desplegador::desplegarcon$todoignora$segundos: repartido, cada llamada volvía a subir el primer fichero y no terminaba nunca; la documentación ya decía que con$todono se reparte, pero el código lo repartía. (b)Version::ACTUALse promete: hasta ahora la versión solo se leía conehundu --version, y un editor que quiera ponerla en un informe de error no tenía cómo. (c)Lote::progreso()de un loteocupado()vale 0, y no 1: una barra de progreso lo enseñaba al 100 % sin haber hecho nada. (d) La carpeta de destino decarpetase crea al subir el primer fichero, no al abrirla, para que--simularno deje nada; el sitemap y el feed de cada idioma (eu/feed.xml) se suben los últimos, como los de la raíz; y un volcado deConfiguraciontampoco enseña dónde está la clave privada.biblioteca.mdgana lo que se promete y no estaba: las firmas deDestino,ErrorDeProyecto,trazar,anotaryquitar, y la semántica deProyectoyPagina::$campos. -
Cerrada. Tres avisos nuevos, que no cambian lo que se compila. Un atajo registrado cuyos atributos no se pueden leer (
[imagen fichero=a.png]sin comillas) se deja como texto, como antes, y ahora se avisa: sin el aviso se quedaba en la página y nadie lo notaba. Unfeed.coleccionque ninguna página usa (publicada o no) avisa, porque seguramente está mal escrito, y el feed sale vacío; una colección cuyas páginas son todas borradores no avisa, porque no es una errata. Una página con la extensión en mayúsculas o.markdown(LEEME.MD,post.markdown) avisa de que no se compila y se copia tal cual. Y los atajosimagen,videoyarchivoya no leen unficherode fuera depublico/: avisan y no insertan nada, comosvg()ydimensiones(), que ya lo hacían. -
Cerrada. La relectura final del contrato antes de la 1.0 corrige el texto de unos noventa sitios sin cambiar lo que hace el motor, y unas pocas cosas del código para que cumpla lo que el contrato ya decía. Tres lectores independientes comprobaron unas 250 afirmaciones contra el código, y unas 200 eran ciertas. Lo que queda escrito: los ficheros de texto son UTF-8 (BOM fuera,
\r\na\n) y uno que no lo es detiene el build; un[nombre]registrado es siempre una llamada; un atajo envuelve si la página tiene algún[/nombre], y dos del mismo nombre no se anidan;pagina.rutade una página generada esetiqueta.md#cultura-pop;dondecompara como números si los dos valores lo parecen; las colecciones se nombran exactas; el empate de fecha se resuelve por la ruta del fichero byte a byte;&da-y-; el orden del sitemap y del feed; el algoritmo delidde un encabezado; qué cuenta como vacío enrequerido; los tipos de campo del esquema; quecarpetasin/*son solo las páginas directas; las cuatro claves del menú (grupo,rotulo,entrada,excluir) pasan de la orientación para el editor al formato, con sus errores. El código se arregla donde el contrato mandaba:slugifyvale en el patrón deagrupary depaginar;paginaravisa de unaurlsin barra final aunque todo quepa en un trozo;svg()quita la declaración XML y el DOCTYPE aunque un comentario los separe; el dominio de un enlace no distingue mayúsculas; unaurlde solo espacios es una en blanco. La justificación de «un fichero por idioma» pasa a la decisión 4, porque era historia dentro de la parte normativa. No cambia ninguno de los veintitrés sitios de prueba.