Las peripecias de insertar imágenes en un post usando Hugo (y Bulma)

20 de Enero de 2023

Contexto inicial

Resulta ser que necesitaba mostrar imágenes en uno de mis posts como para que sea más entendible lo que estaba explicando (ya que cuesta “digerir” el contenido cuando es solo texto). Como no quería utilizar el recurso global (Global Resource) assets, y la estructura inicial de mis posts era /content/posts/<el_post_en_cuestion>, tuve que reestructurar mis archivos para poder utilizar el recurso de página (Page Resource).

Primer Cambio: reestructurar la carpeta de posts

Esto implicaba hacer los siguientes cambios:

  1. Mover cada archivo de post a una carpeta, y llamar a cada una de esas carpetas con el nombre del post que contienen. Ej.: si el nombre era mi_post.md, la carpeta contenedora se nombraría como mi_post.
  2. Renombrar cada archivo de post como index.md.

De esta manera, la estructura pasaba a ser de esta:

content/  
├─ posts/  
│  ├─ mi_post.md

a esta:

content/  
├─ posts/  
│  ├─ mi_post/  
│  │  ├─ index.md

Entonces, si mi_post tiene referencias a imágenes, estas simplemente deben colocarse en esta carpeta. Suponiendo que se tienen dos imágenes, primera_imagen.png y segunda_imagen.jpg, la estructura de la carpeta pasará a ser la siguiente:

mi_post/
 ├─ index.md
 ├─ primera_imagen.png
 ├─ segunda_imagen.jpg

¡Ya podía ver las imágenes! Pero empezaron los problemas… Quería que las imágenes sean adaptables (responsive) 😀

La lucha de la adaptabilidad, sin shortcodes, ni render hooks, ni unsafe = true: ¿cómo agrego CSS?

Ya conocía el atributo srcset en combinación con sizes para mostrar una misma imagen (que en realidad son distintos archivos pero en diferentes tamaños), pero por pereza de recortar varias veces la misma imagen (!) no quise implementar esto.

Otra referencia en MDN: ¿Por qué imágenes adaptables?.

Si utilizaba un shortcode o un render hook el cambio se hubiera aplicado a todas las imágenes, y esa no era mi idea: solo quería aplicar esos cambios a las imágenes de ese post en particular.

Tampoco quería activar la opción unsafe = true de [markup.goldmark.renderer] en el config.toml porque no quería usar html “puro y duro” (raw html, por si acaso). Como unsafe por defecto es false, hace que se muestre en el inspector del navegador <!-- raw HTML omitted --> en lugar del HTML puro que fue escrito.

Ej.: si en el markdown escribí

Un párrafo.
<br>
Otro párrafo.

esto se renderizará como

Un párrafo.
<!-- raw HTML omitted -->
Otro párrafo.

En mi búsqueda queriendo agregar CSS en Markdown, entre los primeros resultados aparecía este post. Aquí, se muestra que para activar los atributos en markdown, debía agregarse lo siguiente al config.toml:

[markup.goldmark.parser.attribute]
    block = true

y se mostraban varios ejemplos, pero el que seguí fue el último, el cual (modificado) quedaba de esta manera:

![Descripción de la imagen](/ruta/de/la/imagen.png)
{.la-clase}

Bien, parecía funcionar… Pero al inspeccionar la imagen noté un detalle no menor: la imagen se renderizaba dentro de un párrafo. Es decir, se mostraba en el inspector así:

<p class="la-clase">
    <img src="/ruta/de/la/imagen.png" alt="Descripción de la imagen">
</p>

¿¿¿Por qué img está dentro de p???

Detalle no menor: aún no había revisado a detalle la documentación de configuración de Markdown en Hugo.

En el mismo post, el autor (gracias RoneoOrg) mencionaba que en la versión 0.108.0 de Hugo “aporta interesantes mejoras sobre este tema” (en cuanto a las imágenes). Como el enlace redirigía a la información acerca de tal versión, la leí, y realmente era bastante clara. Traduciendo, dice más o menos así:

Con Hugo v0.108.0 puedes renderizar imágenes Markdown standalone sin un párrafo que lo envuelva. Tanto la especificación HTML como la CommonMark definen la imagen como un elemento en línea. Para Markdown, esto ha significado que si pones una imagen por sí sola (no dentro de otro párrafo), se envolvería en etiquetas <p></p>, incluso si proporcionas tu propia plantilla de render hook.
(No lo traduzco todo porque ciertas palabras pierden sentido en español), jé.

Y aquí es donde está la clave: “Ahora puedes ahorrarte esta molestia estableciendo markup.goldmark.parser.wrapStandAloneImageWithinParagraph = false.”

Así, la propia información de la versión daba el siguiente ejemplo de configuración en el config.toml:

[markup]
  [markup.goldmark]
    [markup.goldmark.parser]
      wrapStandAloneImageWithinParagraph = false
      [markup.goldmark.parser.attribute]
        block = true


Actualicé a la versión 0.108.0 (tenía la 0.106.0), e inicialmente no funcionaba. Acá entra lo que mencionaba más arriba: “aún no había revisado a detalle la documentación de Markdown”. Estaba haciendo la prueba con un párrafo, y la clase no aparecía, si no que se mostraba tal cual en el html. La prueba era la siguiente:

Test de párrafo { .test }

y se renderizaba así:

<p>Test de párrafo { .test }</p>


Luego encontré este post de Bryce Wray, y leyendo sus explicaciones, en cierta parte (en donde hacía un comparativa) decía “(…) en Hugo, requiere un salto de línea entre el final de a lo que estás dando estilo y el atributo (…)”. Y se me prendió el foquito… Faltaba el “enter” (*llora en silencio*)

Entonces, hice esto:

Test de párrafo
{ .test }

y se renderizó correctamente:

<p class="test">Test de párrafo</p>


Y leyendo acerca de la configuración attribute en la documentación, se puede ver lo que traducido sería más o menos así:

“Habilita el soporte de atributos personalizados para títulos y bloques añadiendo listas de atributos dentro de llaves simples ({.myclass class="class1 class2" }) y colocándolas después del elemento Markdown que decoran, en la misma línea para los títulos y en una nueva línea directamente debajo para los bloques.”

En una nueva línea directamente debajo para los bloques… Más claro, agua.

Pero la historia sigue…

Otro nivel de complejidad: mis requisitos para mostrar las imágenes y ¡Bulma!

Mis requisitos para mostrar las imágenes en ese post eran:

  1. En tamaños menores de pantalla, mostrar una barra horizontal para visualizar la imagen (overflow-x: auto).
  2. Mostrar las imágenes con una altura fija (ya que por defecto se redimensionan según el tamaño de la pantalla, gracias a Bulma).

    Como no se puede agregar un scroll a la imagen directamente, sino que se debe agregarlo al objeto contenedor (ver la definición formal (en inglés)), tenía dos opciones: la primera era remover wrapStandAloneImageWithinParagraph = false del config.toml para que img vuelva a renderizarse dentro de p (cosa que no quería) y la otra era hacer trampa para que img vuelva a estar contenido con p: colocando una línea antes una barra invertida, que representa un <br>. Así, esto:
\
![Descripción de la imagen](/ruta/de/la/imagen.png)
{.la-clase}

se convertía en esto:

<p class="la-clase">
	<br>
	<img src="/ruta/de/la/imagen.png" alt="Descripción de la imagen">
</p>

Teniendo en cuenta que .la-clase es lo siguiente:

.la-clase {
	overflow-x: auto;
	overflow-y: hidden;
    /*lo mismo sería escribir overflow: auto hidden;*/
}

Esto parecía funcionar en un test “casero”, pero a la hora de probarlo en el post, no se mostraba la barra de desplazamiento… ¿Por qué? ¡Por Bulma!

Para img, Bulma establece height: auto y max-width: 100%. Como erróneamente procuraba modificar la altura (más adelante explicaré por qué), inicialmente hice lo siguiente:

  1. Intenté establecer el width y el height en el markdown. Con esta sintaxis:
\
![Descripción de la imagen](/ruta/de/la/imagen.png)
{.la-clase width="600px" height="400px"}

Esto no funcionó, porque al renderizarse, se removía el width y el height del párrafo. No obstante, si lo utilizaba directamente con la imagen, sí se renderizaban las propiedades en el HTML, pero tampoco funcionaba (seguía priorizando height: auto). Hay que recordar que necesitaba agregar la clase al contenedor, no a la imagen. Además, tenía la dificultad de que height: auto no se puede sobreescribir.

  1. Como en el CSS no podía pasar por parámetros el width y el height —es decir, no se puede hacer .la-clase { width: <parametro_width>; height: <parametro_height>}; tampoco SCSS/SASS es opción porque no se puede utilizar un mixin desde Markdown—, se me ocurrió pasar a través del style las propiedades que quería agregar.
\
![Descripción de la imagen](/ruta/de/la/imagen.png)
{.la-clase style="width: 600px; height: 400px"}

Y de ahí vino el otro problema…

¿Qué es eso de ZgotmplZ?

En estas instancias, ya me había resignado a usar un render hook. La ubicación es la siguiente (tal como figura en la documentación):

<el_tema>/  
├─ layouts/  
│  ├─ _default/  
│  │  ├─ _markup/  
│  │  │  ├─ render_image.html

Copié el ejemplo en la documentación de la versión 0.108.0, hice unas modificaciones, y el contenido (test) quedó algo así como lo siguiente:

{{ if .IsBlock }}
    <img src="{{ .Destination | safeURL }}" 
        {{ with .Attributes.class }} 
            class="{{ . }}"
        {{ end }}
        {{ with .Attributes.style }} 
            style="{{ . }}"
        {{ end }}
        alt="{{- .Text -}}" />
{{ else }}
    <img src="{{ .Destination | safeURL }}" alt="{{- .Text -}}"
        {{ with .Attributes.class }} 
            class="{{ . }}"
        {{ end }}
        {{ with .Attributes.style }} 
            style="{{ . }}"
        {{ end }}
    />
{{ end }}

Al inspeccionar la imagen, noté que aparecía style="ZgotmplZ". Pensé que era algo similar a cuando olvidás (o no sabés) que se debe sobreescribir el método toString para no imprimir el hashcode de un objeto en Java… Pero nope. Haciendo dos tests rápidos, reemplacé style="width: 600px; height: 400px" por style="width: 600px" y por style="height: 400px" En ambos casos, el style se mostraba correctamente. ¿Qué pasaba entonces?

Buscando arduamente qué quería decir ZgotmplZ, me encontré con este comentario donde jmooring cita a la documentación de Go acerca de los templates, diciendo más o menos lo siguiente:

ZgotmplZ es un valor especial que indica que un contenido no seguro llega a un contexto CSS o URL en tiempo de ejecución. (…)
Sospecho que necesitas usar safeURL en tu template o shortcode.”

Súper obediente yo, coloqué style="{{ . | safeURL }}"… Claramente no funcionó. Luego usé un poco más mi cabeza, y noté que dijo contexto CSS o URL. Si existe safeURL, entonces también existe safeCSS, pensé. Efectivamente.
La documentación dice algo así como: “Declara el string proveído como una cadena CSS “segura” conocida. En este contexto, “seguro” significa el contenido CSS que coincide con alguno de los siguientes casos: (…) 3.Producciones de declaraciones CSS3, tales como color: red; margin: 2px”. ¡Ese era mi caso!

Entonces, modifiqué el render hook de tal manera que style="{{ . | safeCSS }}". Y funcionó. El render hook quedó algo similar a:

{{ if .IsBlock }}
    <img src="{{ .Destination | safeURL }}" 
        {{ with .Attributes.class }} 
            class="{{ . }}"
        {{ end }}
        {{ with .Attributes.style }} 
            style="{{ . | safeCSS }}"
        {{ end }}
        alt="{{- .Text -}}" />
{{ else }}
    <img src="{{ .Destination | safeURL }}" alt="{{- .Text -}}"
        {{ with .Attributes.class }} 
            class="{{ . }}"
        {{ end }}
        {{ with .Attributes.style }} 
            style="{{ . | safeCSS }}"
        {{ end }}
    />
{{ end }}

Luego, viendo este comentario, recordé un detalle… ¡No estoy teniendo en cuenta los atributos (como width, height, etc., solo class y style)! De hecho, podía seguir modificando el render hook (agregando range…), pero internamente insistía en que debía haber una solución más corta y sencilla.

¿Qué hice finalmente?

Como mencioné más arriba, erróneamente procuraba modificar la altura. Me fijé demasiado en el height: auto, pero en mi caso, el verdadero problema era el max-width: 100%. En mi css, bastó con colocar max-width: unset, y la imagen ya se comportaba como esperaba. Di demasiadas vueltas para liquidar con algo sencillo 😞

Como tenía 3 imágenes y necesitaba que aparezca la barra de desplazamiento en dos de ellas, en estas últimas realicé el truco (que mencioné más arriba) de renderizar la imagen dentro de un párrafo; a tal párrafo le agregué una clase (lo llamo aquí parrafo-con-scrollbar-a-imagen), con las siguientes propiedades:

/*para scss */
.parrafo-con-scrollbar-a-imagen {
    overflow: auto hidden; /* mostrar en horizontal cuando aplique, ocultar en vertical en cualquier caso */
    line-height: 0; /* para "ocultar" el espacio generado por el <br> */
    & > img {
        max-width: unset; /*para sobreescribir el "max-width: 100" de Bulma */
    }
}

/*para css*/
.parrafo-con-scrollbar-a-imagen {
    overflow: auto hidden;
    line-height: 0;
}
.parrafo-con-scrollbar-a-imagen > img {
    max-width: unset;
}

En este caso, no tengo la necesidad de pasar el width y el height al generar el párrafo (de hecho, si paso tales atributos, no se renderizan).

Bonus #1: centrar la barra de desplazamiento

Contaba con unas imágenes que debían aparecer centradas. Inicialmente coloqué las propiedades display:flex; justify-content: center (lo que en Bulma sería usar las clases is-flex y is-justify-content-center) al párrafo, pero noté un detalle: la imagen “iniciaba” desde el centro (es decir, el scrollbar podía desplazarse del medio a la derecha: no aparecía “la izquierda” de la imagen). Por mera casualidad (ya que buscaba info acerca del scrollbar centrado, no de la imagen “cortada”), encontré, gracias a la respuesta de Michael Benjamin, por qué sucedía esto: cuando el elemento flexible desborda el contenedor, parte de tal elemento queda inaccesible. No hice uso de ninguna de las soluciones propuestas, ya que, en mi caso particular, solo bastaba con centrar el scroll para que la imagen aparezca centrada. Aquí acudí a mi buen amigo JavaScript.

Como quería que la barra de desplazamiento aparezca centrada tanto al cargar la página como al redimensionar el tamaño de la ventana, implementé el siguiente código:

function scrollbarCentrado (){

  // Obtener todos los elementos con la clase "con-scroll"
  const $imagenesConScroll = Array.prototype.slice.call(document.querySelectorAll('.con-scroll'), 0);

  // Verificar si hay algún elemento
  if ($imagenesConScroll.length > 0) {

    $imagenesConScroll.forEach( imagenConScroll => {
        let scrollElement = imagenConScroll;
        scrollElement.scrollLeft =  (scrollElement.scrollWidth - scrollElement.clientWidth ) / 2; // se resta el tamaño del scroll con el ancho de la pantalla y se divide entre 2
    });

    }
}
  
  window.addEventListener('load', scrollbarCentrado);
  window.addEventListener('resize', scrollbarCentrado);

scrollLeft es la cantidad de píxeles en el que el contenido de un elemento es desplazado hacia la izquierda. Esta es la info de MDN al respecto.

Ya solo quedaba un detalle: ¿cómo invoco a ese fragmento de JS si estoy redactando el post en Markdown?

Bonus #2: Referenciar a un fragmento JS en un post hecho en Markdown, con Hugo

Justamente snapo tuvo la mismísima duda que yo: ¿Cómo insertar JavaScript en un post, pero no en todos las páginas al mismo tiempo? No quería referenciar en todos los rincones de mi sitio web un pequeñísimo fragmento de código, sino solo en ese post en particular.

Para mi caso, combiné los comentarios de moorereason y de rdwatters. Hice lo siguiente:

  1. Agregué en los metadatos de la cabecera la clave js con un array de un ítem (en este caso). El ítem es el nombre del archivo. Aquí, llamémosle prueba:
+++
js = ["prueba"]
+++
  1. En single.html, al final del archivo, incorporé:
{{ define "jsscripts" }}
  {{- range .Params.js -}}
  <script src="/js/{{ . }}.js"></script>
  {{- end -}}
{{ end }}

Utilizo un array y range para contemplar los casos en que quiera hacer referencia a más de un archivo js.

Así, al renderizarse, se mostrará de este modo:

<script src="/js/prueba.js"></script>
  1. Fui a baseof.html y agregué antes de cerrar el </body> lo siguiente:
{{ block "jsscripts" . }}

{{ end }}

La documentación referente a las plantillas base y block, acerca de este último dice más o menos lo siguiente: “La palabra clave block permite definir la estructura externa de una o más plantillas maestras de las páginas y luego rellenar o sobreescribir partes según sea necesario.


Ahora sí, todo funcional :)

Lo rescatable

  • Soy muy partidaria de “la documentación es tu buena amiga”. Esta investigación me lo recordó.
  • Hay que probar… y probar… y probar… hasta lograr tu objetivo (o al menos lograr lo más parecido posible a tu objetivo.)
Hugo Theme: "Bulma Hugo Resume", basado en  Hugo Resume