Blog de Raúl Salinas

De Markdown a WordPress sin dolor: así funciona mi sistema de blog en Python

Durante mucho tiempo escribí artículos en Markdown y, cuando tocaba publicarlos en WordPress, aparecía siempre la misma fricción: los diagramas (sobre todo Mermaid) no entraban bien, las imágenes había que prepararlas a mano y el flujo acababa siendo una mezcla de pasos repetitivos y fáciles de romper.

La motivación inicial fue muy simple: quería una herramienta que, dado un documento Markdown con diagramas, me generara las imágenes y me preparara un contenido listo para embeber en WordPress. Sin copiar-pegar eternos, sin scripts sueltos y sin depender de acordarme de todos los pasos.

Con el tiempo, ese script inicial evolucionó a algo más ambicioso: un sistema completo para gestionar varios blogs desde terminal, cada uno con sus artículos versionados en varios idiomas, publicación controlada en uno o más servidores WordPress, traducción asistida, comprobaciones de estado y copias de seguridad.

El resultado es una CLI en Python llamada blog.py.

Qué problema resuelve

En mi caso, el cuello de botella no era escribir, era operar:

  • convertir diagramas Mermaid a imágenes,
  • generar una versión compatible con WordPress,
  • mantener sincronizadas distintas versiones de idioma,
  • publicar o dejar en borrador con seguridad,
  • saber si WordPress ha cambiado desde mi último publish,
  • publicar el mismo blog en varios servidores (mirrors),
  • y poder recuperar todo con backups.

blog.py unifica todo eso bajo un único comando.

La idea central: separar programa y datos

La pieza que más ha hecho crecer el sistema es una distinción simple pero potente: el programa va por un lado y los datos por otro.

  • El programa (los .py y el Makefile) vive en la raíz del repositorio.
  • Los datos viven por site: cada blog de contenido independiente cuelga de sites/<site>/.
templates/post.md          # plantilla compartida para 'new'
siteconfig.py              # carga sites/<site>/site.toml + .env
sites/
    raulsa8/               # site personal (raulsa8.blogs.uv.es)
        site.toml          # idiomas, [[servers]], Telegram
        .env               # secretos (fuera de git): contraseñas, token
        posts/<post>/
            <base>-es.md   # versión en castellano
            <base>-ca.md   # versión en valenciano …
            <base>-<lang>.<servidor>.state.json
        build/  backup/    # generados (gitignored)
    veg/                   # otro site, otro blog

Esto separa con claridad dos tipos de configuración:

  • Lo que depende del blog (site): el contenido, el frontmatter, los diagramas y los idiomas admitidos. Se define en site.toml.
  • Lo que depende del servidor: la URL de WordPress, el usuario, la contraseña y todo el estado (post_id, medios, enlace). Por eso el estado se guarda por servidor.

Multi-site y multi-servidor

Un mismo programa gestiona varios blogs, y cada blog puede publicarse en varios servidores WordPress a la vez (mirrors):

./blog.py -s raulsa8 publish mi-post es      # site explícito
./blog.py publish mi-post es                  # site por defecto, a TODOS sus servidores
./blog.py -S mirror publish mi-post es        # a un servidor concreto
  • -s/--site elige el blog. Si no lo indicas, se usa el que tenga default = true, o $BLOG_SITE, o el único disponible.
  • -S/--server elige el servidor. En lecturas (diff, status, tg-check) se usa el primario; en escrituras (publish, unpublish) se va a todos los servidores salvo que indiques uno.

El estado por servidor (<base>-<lang>.<servidor>.state.json) guarda el post_id, los medios subidos (con su sha256) y el enlace en ese servidor. Si el fichero no existe, ese post no se ha publicado nunca allí.

Filosofía técnica: Python orquesta, Make ejecuta

La idea es esta: Python como capa de orquestación amigable, Make como motor de ejecución.

La mayoría de subcomandos delegan en make, así no se pierde funcionalidad previa ni se rehacen piezas que ya funcionaban bien. Pero la lógica más enrevesada (traducción, diff, status, tg-check, where, edit) vive directamente en Python para simplificar el mantenimiento.

En otras palabras:

  • make sigue siendo la base técnica,
  • blog.py aporta ergonomía, autocompletado y una interfaz coherente.

Idioma siempre explícito

Una decisión de diseño deliberada: no hay idioma por defecto. Cada orden exige el idioma de forma explícita (o --all-lang para todas las versiones). Así nunca se publica una versión equivocada por descuido.

Flujo de trabajo habitual

Un flujo real típico podría ser:

./blog.py new mi-post es
./blog.py edit mi-post es
./blog.py render mi-post es
./blog.py publish-draft mi-post es

Y cuando quieres escalarlo a varios idiomas:

./blog.py translate mi-post en --from es
./blog.py translate mi-post ca --from es
./blog.py publish-all

La ventaja es que no tienes que recordar comandos dispersos ni rutas internas: la CLI concentra el modelo mental.

Autocompletado con TAB

Uno de los detalles que más productividad da en el día a día es el autocompletado de sites, servidores, nombres de post e idiomas.

Como blog.py se basa en argparse, el autocompletado lo aporta argcomplete: en este sistema su hook global ya está instalado, así que basta con abrir una shell nueva y usar:

./blog.py <TAB>

Si en tu sesión no está activo, puedes habilitarlo así:

eval "$(register-python-argcomplete ./blog.py)"

Parece un detalle pequeño, pero reduce errores tontos y acelera muchísimo la operación diaria.

Subcomandos principales (y para qué sirven)

blog.py expone un conjunto de órdenes que cubren todo el ciclo de vida del contenido:

  • list: lista artículos e idiomas disponibles.
  • new: crea un post vacío desde plantilla.
  • render: genera diagramas y construye wordpress.md.
  • extract: extrae Mermaid y genera wordpress.md.
  • diagrams: renderiza únicamente los diagramas Mermaid a imagen.
  • publish: crea o actualiza y publica en vivo.
  • publish-draft: crea o actualiza como borrador.
  • publish-all: publica todas las versiones de todos los artículos.
  • unpublish: revierte un contenido a estado borrador.
  • translate: traduce con Claude y sube como borrador.
  • master: muestra o fija el idioma maestro de un artículo (origen de las traducciones).
  • diff: muestra diferencias entre local y WordPress.
  • status: comprueba (en paralelo) si WordPress ha sido modificado desde nuestro último publish.
  • pull: muestra (sin integrar) los cambios hechos a mano en WordPress.
  • bad-slugs: detecta slugs colisionados (sufijo -2/-3) para enlazar traducciones.
  • lang-check: verifica el idioma real de los posts publicados.
  • tg-check: verifica que el token del bot de Telegram funciona.
  • backup: descarga todo el blog a backup/.
  • export-static: genera un blog HTML estático en static/.
  • clean / distclean: limpian el build/ de una versión o entero.
  • where: imprime la ruta exacta del fichero (por ejemplo, para abrirlo con vim).
  • edit: abre el fichero en tu $EDITOR.

No todos los comandos se usan cada día, pero tenerlos unificados evita soluciones ad hoc.

Multiidioma y versionado: la parte que más valor aporta

Conforme crece un blog técnico, mantener versiones en varios idiomas se vuelve difícil si no hay estructura.

Este sistema lo resuelve apoyándose en dos pilares:

  • ficheros de artículos gestionados de forma explícita por idioma,
  • versionado con Git para conservar trazabilidad, revisión y rollback.

Eso permite trabajar de forma incremental: escribes en un idioma base, traduces cuando conviene, revisas diferencias y publicas cada versión con control.

Como uso Polylang gratuito, la asignación de idioma y el enlace entre traducciones se hace una sola vez a mano en el wp-admin. A partir de ahí, publish actualiza el contenido sin perder ese enlace.

Publicar con red de seguridad

En publishing, la pregunta no es si habrá un susto, sino cuándo. El sistema lo tiene en cuenta de dos formas.

Primero, publish aborta si WordPress ha sido modificado desde el último publicado, para no pisar cambios manuales (puedes forzar con --force si sabes lo que haces). El comando status te avisa de esas modificaciones antes de tocar nada, y pull te las muestra como un diff (sin integrarlas). Además, publish es idempotente: guarda una firma del contenido y solo sube lo que ha cambiado, así que dos publish-all seguidos no rehacen trabajo. Y publish-all publica los artículos en paralelo (por defecto de 4 en 4).

Segundo, el comando backup no es accesorio: descargar el estado completo del blog a una carpeta local (backup/) añade una red de seguridad real para migraciones, auditorías rápidas o recuperación tras errores.

Exportar a HTML estático

Además de publicar en WordPress, el sistema puede generar una versión HTML estática completa del blog con export-static.

Renderiza todos los artículos y produce un sitio estático en sites/<site>/static/: una página por artículo e idioma, índices por idioma, selector de idioma y sin comentarios. La búsqueda se apoya en Google con el operador site:, donde base_url es la URL pública donde alojarás el estático (configurable en site.toml).

Es ideal para tener una copia navegable e independiente del blog, sin depender de WordPress para leerlo.

Integración con Telegram

Además del flujo de publicación, el sistema contempla la parte operativa de Telegram: al publicar puede notificar en un canal, con chat configurable por idioma.

El subcomando tg-check valida que el token del bot funciona correctamente, algo muy útil cuando tienes automatizaciones de notificación alrededor del proceso editorial. En la práctica, te evita descubrir un problema de credenciales demasiado tarde: puedes comprobar la integración antes de publicar y mantener la cadena de herramientas en estado saludable.

Por qué me funciona este enfoque

No es un framework gigante ni una plataforma cerrada. Es un sistema pequeño, práctico y compuesto por piezas conocidas:

  • Markdown como fuente de verdad,
  • Mermaid para diagramas,
  • Python para orquestar,
  • Make para ejecutar,
  • WordPress (uno o varios) como destino,
  • y HTML estático como copia navegable.

Esa combinación tiene una virtud importante: reduce fricción sin sacrificar control técnico.

Cierre

La herramienta nació para resolver un problema puntual (pasar Markdown con diagramas a un formato publicable en WordPress) y acabó convirtiéndose en una cadena completa de producción editorial desde terminal: varios blogs, varios idiomas, varios servidores y exportación estática, todo bajo un único comando.

Si te gusta escribir en local, versionar todo y publicar con confianza, un enfoque así marca la diferencia: menos trabajo mecánico, menos errores y más foco en lo que realmente importa, que es el contenido.

Figura 1
Figura 1