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
.pyy elMakefile) 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/--siteelige el blog. Si no lo indicas, se usa el que tengadefault = true, o$BLOG_SITE, o el único disponible.-S/--serverelige 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:
makesigue siendo la base técnica,blog.pyaporta 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 esY 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-allLa 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 construyewordpress.md.extract: extrae Mermaid y generawordpress.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 últimopublish.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 abackup/.export-static: genera un blog HTML estático enstatic/.clean/distclean: limpian elbuild/de una versión o entero.where: imprime la ruta exacta del fichero (por ejemplo, para abrirlo convim).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