Automatizando las vistas previas de los enlaces para Zola
¿Alguna vez te has preguntado cómo aplicaciones como WhatsApp, Telegram o Mastodon muestran una vista previa de un enlace? A continuación, encontrarás el pantallazo de un enlace sin vista previa compartido en WhatsApp. Haz clic en la imagen para ver cómo cambia al añadir la imagen:
Mucho mejor, ¿no? Estas imágenes se obtienen de etiquetas HTML, concretamente og:image
y twitter:image
. Puedes asignar cualquier imagen a estas etiquetas.
Mientras desarrollaba el tema de mi web —tabi—, me topé con un artículo de Simon Willison que explica cómo utilizar su herramienta shot-scraper
para generar estas imágenes.
Decidí explorar si podía seguir un enfoque similar para crear las miniaturas para todos los artículos de este sitio, así como los de la demo de tabi. Y, aún más divertido, intentar automatizar el proceso para las publicaciones nuevas y modificadas.
Contexto
Este sitio está construido con Zola, que utiliza el front matter de TOML para almacenar metadatos (por ejemplo, título, fecha, etiquetas…). Por defecto, Zola no gestiona estas imágenes.
Sin embargo, es posible añadir variables personalizadas a la sección [extra]
, así que decidí añadir una clave social_media_card
a tabi (PR 130).
Ahora, cualquier sitio que utilice tabi puede añadir social_media_card = path/to/img.jpg
a un archivo. Cuando la publicación se comparta en redes sociales, esa imagen se mostrará como vista previa.
Primer paso: conseguir una buena imagen
Empecé a experimentar con shot-scraper
, tratando de encontrar el comando perfecto. Después de algunas pruebas[1], terminé con:
Este comando crea una captura de pantalla JPEG de 1400 por 800 con un factor de calidad de 60. Creo que funciona bien para las proporciones del tema:
La calidad no es excelente, pero es suficientemente buena™ para el uso que tendrán.
Eligiendo el nombre de los archivos
La documentación para desarrolladores de Meta dice:
«Las imágenes para miniaturas se almacenan en caché según la URL y no se actualizarán a menos que la URL cambie.»
«[Social media card] Images are cached based on the URL and won't be updated unless the URL changes.»
Esto significa que si actualizamos una publicación, la URL de la imagen también debe cambiar.
Primero pensé en utilizar el hash truncado SHA-1 de la captura de pantalla como prefijo para el nombre. Si la imagen era diferente, el nombre también lo sería.
Luego recordé que simplmente podía añadir el mecanismo de «cache busting» de Zola a tabi, que añade ?h=<sha256>
al final de la URL. Esto simplifica bastante el proceso, especialmente cuando se trata de actualizar los metadatos del artículo.
La lógica
Dado un archivo Markdown (un artículo) necesitamos:
- Tomar una captura de pantalla de la página en vivo
- Guardarla en la ruta adecuada (por ejemplo,
static/img/social_cards
) - Actualizar el front matter (metadatos) del archivo
.md
con la rutasocial_media_card
Así es como conseguí los dos primeros pasos usando Bash:
base_url="http://127.0.0.1:1111" # Interfaz/puerto predeterminados de Zola.
output_path="static/img/social_cards"
post="" # El primer argumento que se proporcione al script.
post_name="" # Elimina la extensión.
url="" # Elimina el prefijo "content/"; el directorio padre del contenido de Zola.
# Archivo temporal para la captura de pantalla.
temp_file=
# Genera la captura de pantalla en el archivo temporal.
# Limpia el nombre del archivo.
safe_filename= # Slugify.
image_filename=" / .jpg"
# Mueve la captura de pantalla al directorio de salida.
¡Fácil! Guardé el script como social-cards-zola
.
Empiezan los problemas
¿Qué pasa con los idiomas?
En este punto, con el script básico hecho, me encontré con un problema: los nombres de archivo no siempre coinciden con las URL.
Un post en otro idioma, por ejemplo, el-meu-primer-post.ca.md
, no estará disponible en base_url/el-meu-primer-post.ca
, sino en base_url/ca/el-meu-primer-post
.
Añadamos un poco de lógica para extraer el código de idioma y construir la URL adecuada. Usando la expansión de parámetros de Bash, obtenemos:
# Elimina la extensión y el prefijo "content/".
post_name=""
url=""
# Intenta capturar el código de idioma.
lang_code=""
if ; then
# No había código de idioma.
lang_code=""
else
# Elimina el código de idioma de la URL.
url=""
fi
url=" /"
Listo.
¿Y las secciones?
Ya hemos conseguido que los artículos tengan una captura de pantalla, pero ¿qué hay del índice principal o la página de archivo? En Zola, no son páginas, sino secciones, y su URL corresponde al nombre del directorio. Por ejemplo, content/archive/_index.fr.md
está disponible en base_url/fr/archive/
.
Podemos adaptar la lógica anterior para eliminar la parte _index
del nombre del archivo:
Así es como convert_filename_to_url
maneja diferentes archivos:
Entrada | Salida |
---|---|
content/_index.es.md | es/ |
content/blog/markdown.fr.md | fr/blog/markdown/ |
content/blog/comments.md | blog/comments/ |
content/archive/_index.md | archive/ |
content/archive/_index.ca.md | ca/archive/ |
Si añadimos la URL base antes de cada salida, obtenemos el enlace completo.
Modificando los metadatos
Ahora puedo generar fácilmente las capturas de pantalla para las entradas, pero aún necesito asociar las imágenes con los archivos Markdown.
Para actualizar los metadatos, utilicé awk
para encontrar dónde empieza el front matter, localizar o crear la sección [extra]
, y añadir o actualizar la clave social_media_card
:
# Inicializar las variables para el seguimiento del estado.
BEGIN in_extra=done=front_matter=0; }
# Función para insertar la ruta de la miniatura.
function insert_card() print "social_media_card = \"" card_path "\""; done=1; }
# Si la miniatura se ha insertado, simplemente muestra las líneas restantes.
if done print; next; }
# Cambiar la bandera front_matter al inicio, denotado por +++
if /^\+\+\+/ && front_matter == 0
front_matter = 1;
print "+++";
next;
}
# Detectar sección [extra] y establecer la bandera extra_exists.
if /^\[\]/ in_extra=1; extra_exists=1; print; next; }
# Actualizar la miniatura existente para redes sociales.
if in_extra && /^/ insert_card ; in_extra=0; next; }
# Fin del front matter o inicio de una nueva sección.
if in_extra && /^\[+\]/ || /^\+\+\+/ && front_matter == 1
insert_card ; # Añadir la miniatura faltante para redes sociales.
in_extra=0;
}
# Insertar la sección [extra] faltante.
if /^\+\+\+/ && front_matter == 1 && in_extra == 0 && extra_exists == 0
print "\n[extra]";
insert_card ;
in_extra=0;
front_matter = 0;
print "+++";
next;
}
# Mostrar todas las demás líneas tal cual.
print;
}
Agregué esta función a social-cards-zola
, que se activa con la opción -u
o --update-front-matter
.
Concurrencia
Quería crear las imágenes para todas las entradas a la vez, así que utilicé GNU parallel para procesar todos los archivos Markdown de manera concurrente:
|
Unos segundos después, tenía un montón de capturas de pantalla y archivos Markdown actualizados. 🎉
Automatizando el proceso
Naturalmente, el siguiente paso fue añadir este proceso a mi gancho pre-commit de Git, que ya estaba actualizando las fechas de las publicaciones y optimizando archivos PNG.
Cada vez que hago un commit de archivos Markdown (nuevos o modificados), genero la captura de pantalla y actualizo la metadata del post o de la sección:
# Crear/actualizar la miniatura para redes sociales para cada archivo Markdown modificado.
archivos_md_modificados=
if ; then
|
fi
Puedes ver cómo queda integrado en el gancho pre-commit completo.
El script completo de social-cards-zola
, con más ajustes (como el uso de una clave de front matter distinta), está disponible en el repositorio de este sitio.
¿Fin?
El script funciona, pero es bastante frágil: falla si lo usas fuera de la ruta raíz del sitio, requiere parallel
para manejar la concurrencia, y probablemente falle si intentas actualizar muchos archivos Markdown a la vez (Zola reconstruye el sitio después de cada modificación, devolviendo errores 404 brevemente).
Estoy contento de haber resuelto el problema, pero también lo veo como una oportunidad para convertir este script suficientemente bueno™ en un pequeño pero sólido programa en Rust —¿un buen primer proyecto en Rust, no?
Así que… continuará, quizás.
-
Inicialmente, estaba convirtiendo las capturas de pantalla PNG a WebP, ya que son significativamente más pequeñas (~40KB) que las JPEG de aspecto similar (~100KB). Sin embargo, al intentar hacer la captura para la primera imagen del artículo, me di cuenta de que WhatsApp no admite miniaturas para redes sociales en formato WebP. Lástima. ↩