Transparencia. Como el anterior, este artículo lo escribió Claude, la IA de Anthropic con la que trabajo en mi homelab, a partir de mi documentación y del historial del proyecto, y también lo tradujo del francés. Yo revisé y validé el original en francés. La sección «El punto de vista de la IA» es de Claude.
El blog que estás leyendo funcionó durante mucho tiempo con WordPress. Desde el 22 de septiembre de 2026 es un sitio estático generado por Hugo, y publicar un artículo se reduce a un git push. Te cuento por qué tomé esta decisión, cómo funciona la cadena de publicación, los obstáculos que encontré y, sobre todo, qué ha cambiado esta migración en la seguridad y la higiene de mi homelab.
Por qué dejar WordPress
WordPress es una herramienta excelente. Mueve una parte enorme de la web y, para un sitio editado por varias personas, con roles, revisiones y extensiones específicas, tiene todo el sentido.
Pero mi necesidad es sencilla: un solo autor, sin coedición, texto y algunas imágenes. Para eso, WordPress me imponía:
- un motor PHP que ejecuta código en cada visita;
- una base de datos MySQL que respaldar, actualizar y vigilar;
- una interfaz de administración expuesta en Internet, objetivo permanente de los bots;
- extensiones y un tema, cada uno con su propio ritmo de fallos y parches.
Además, en mi caso la situación se había degradado: el contenedor de WordPress funcionaba sin versión fijada, y la imagen mysql:8.0 había dejado de recibir parches de seguridad en abril de 2026. Todo ello era público. El punto llevaba tiempo en mi backlog con el título «Decidir qué hacer con WordPress».
Tenía tres opciones: migrar la base de datos a una versión mantenida y seguir manteniendo un CMS del que apenas usaba la décima parte, pasar a otro CMS más ligero o eliminar la parte dinámica. Elegí la tercera.
Por qué Hugo
Un generador de sitios estáticos toma archivos de texto (Markdown) y produce un sitio en HTML puro. No hay nada que ejecutar cuando alguien visita el sitio: el servidor web se limita a servir archivos.
Elegí Hugo con el tema PaperMod: rápido, maduro y sin dependencias en tiempo de ejecución. Las alternativas que descarté, y por qué, están anotadas en mis decisiones de arquitectura:
- Ghost o WriteFreely: más ligeros que WordPress, pero vuelven a introducir una base de datos y un proceso de aplicación permanente. Justo lo que quería eliminar.
- Mantener WordPress y migrar MySQL: suponía seguir manteniendo un servicio que se había quedado sobredimensionado.
Lo que me inspiró
Dos creadores de contenido me convencieron para dar el paso.
Chris Titus Tech lleva mucho tiempo defendiendo los sitios estáticos frente a WordPress. Su vídeo, con un título deliberadamente provocador, WordPress is Dead | Static Sites are the Future, resume bien el argumento: para un sitio de contenido, un CMS dinámico suele ser más una carga que un servicio. Curiosidad: desde entonces ha pasado de Hugo a Astro, otro generador estático. El principio sigue siendo el mismo.
Christian Lempa, cuyos vídeos sobre homelab y DevOps son una mina, muestra en Building a static website in Markdown with Hugo cómo él mismo dejó un CMS clásico por Hugo y Markdown. Es un muy buen punto de partida para ponerse manos a la obra.
La migración del contenido
El contenido se exportó con la herramienta wordpress-export-to-markdown: 17 artículos y una página, con sus imágenes. Hicieron falta dos retoques:
- la herramienta trunca la hora de publicación, así que volví a introducir las fechas a mano para conservar el orden cronológico;
- las imágenes de portada necesitaron un ajuste para mostrarse bien en las tarjetas de la página de inicio.
El repositorio de Hugo se creó a principios de septiembre; el proxy inverso pasó al nuevo sitio el día 22. WordPress se quedó detenido pero intacto mientras confirmaba la estabilidad, y después se eliminaron sus contenedores y sus datos, tras una última instantánea ZFS.
La cadena de publicación
Esto es lo que ocurre hoy cuando publico un artículo:
Editor (VSCodium) ── git push ──▶ Forgejo ── webhook ──▶ Dockhand
(forja Git (clon, build,
autoalojada) redespliegue)
│
Internet ──▶ proxy inverso ──▶ contenedor nginx (HTML estático)
Forgejo es una forja Git libre, autoalojada en mi NAS. Sustituye a GitHub para este proyecto: el código fuente del blog se queda en casa. No está expuesta en Internet, solo en la red local y a través de mi VPN.
Dockhand gestiona los contenedores Docker del NAS. La pila del blog está declarada como una «stack Git»: Dockhand conoce la dirección del repositorio, y Forgejo le avisa en cada push mediante un webhook, una simple petición HTTP enviada automáticamente. Dockhand clona entonces el repositorio, reconstruye la imagen y vuelve a desplegar el contenedor.
La imagen se construye en dos etapas, con un Dockerfile de unas pocas líneas:
FROM hugomods/hugo:exts AS build
WORKDIR /src
COPY . .
RUN hugo --minify
FROM nginx:1.27-alpine
COPY --from=build /src/public /usr/share/nginx/html
La primera etapa genera el sitio con Hugo; la segunda solo conserva el HTML resultante, servido por un nginx mínimo. Hugo no está en la imagen final: en producción solo hay archivos y un servidor web.
Es lo que se llama despliegue continuo: entre el momento en que guardo un artículo y su publicación, no hay ninguna acción manual.
Los obstáculos que encontré
Nada funcionó a la primera, y estos detalles suelen ser los que más tiempo hacen perder. Aquí los tienes.
El primer mecanismo era demasiado casero. Al principio, un repositorio Git «desnudo» en el NAS y un script que se ejecutaba al recibir cambios (post-receive) reconstruían el sitio. Funcionaba, pero era frágil y engorroso de editar a distancia. Forgejo y Dockhand lo sustituyeron.
La caché de build de Docker. Docker reutiliza las etapas ya construidas cuando cree que nada ha cambiado. Resultado: un artículo nuevo enviado… y ausente del sitio. La solución fue desactivar la caché de build para esta pila. El build tarda unos segundos más, pero siempre refleja el contenido real del repositorio.
El tema como submódulo de Git. PaperMod estaba integrado como submódulo, y el clon que hacía Dockhand no lo recuperaba entero. Sustituí el submódulo por una copia del tema versionada directamente en el repositorio: menos elegante, pero sin sorpresas.
Los webhooks de Forgejo. Tres ajustes me hicieron perder tiempo:
- Por defecto, Forgejo se niega a enviar webhooks a direcciones IP privadas, para protegerse de ataques de tipo SSRF. Pero Dockhand está en mi red local. Hay que permitirlo en
app.ini(sección[webhook], parámetroALLOWED_HOST_LIST). El valorprivateautoriza toda la red privada; restringirlo a la dirección exacta de Dockhand es más estricto. - El tiempo de entrega por defecto (5 segundos) era demasiado corto, porque Dockhand solo responde después de procesar la petición. Lo subí a 30 segundos (
DELIVER_TIMEOUT). - Un webhook creado en los ajustes de la cuenta se aplica a todos los repositorios. Para un solo proyecto, hay que crearlo en los ajustes del repositorio.
La fecha en el futuro. El último obstáculo es muy reciente: el artículo anterior de este blog no aparecía tras publicarlo. Estaba fechado a las 18:00 sin zona horaria, es decir, en el futuro, y Hugo no publica los artículos con fecha futura. Una fecha correcta con su zona horaria (+02:00) resolvió el problema.
Una superficie de exposición menor
Para mí, es la mayor ganancia.
| Antes (WordPress) | Después (Hugo) | |
|---|---|---|
| Lo que se ejecuta en cada visita | PHP, consultas MySQL | Nada: archivos servidos por nginx |
| Base de datos | MySQL, sin parches desde abril de 2026 | Ninguna |
| Interfaz de administración | Pública | Ninguna |
| Extensiones y tema | Código de terceros ejecutado en el servidor | Tema usado solo durante el build |
| Origen del contenido | En la base de datos | Repositorio Git, no expuesto en Internet |
| Versión del servicio público | Sin fijar | nginx fijado en 1.27 (la imagen de build de Hugo todavía no) |
Un ejemplo concreto: al día siguiente del cambio, mi sistema de detección (CrowdSec) detectó un bot que sondeaba el blog en ráfagas. Buscaba archivos .env, intentaba subir por el árbol de directorios hasta /proc/self/environ y sondeaba interfaces de administración. Solo obtuvo errores 400 y 404: no había nada que encontrar. Quedó bloqueado unas horas, y ahí terminó todo. Frente a un WordPress, las mismas peticiones habrían apuntado a código que realmente se ejecuta.
Un homelab más sano
Más allá de la seguridad, esta migración ha aligerado el conjunto:
- Una base de datos menos que respaldar, vigilar y actualizar.
- Un servicio público sin versión fijada menos, y una imagen de base de datos sin soporte eliminada.
- Un contenido versionado. Cada cambio es un commit, con su historial y la posibilidad de volver atrás. El repositorio vive en mi NAS, que a su vez se replica fuera de casa, y hay una copia en mi ordenador.
- Un punto cerrado en el backlog. «Decidir qué hacer con WordPress» pasó a Resuelto, le siguió el desmantelamiento, y la decisión está documentada con sus alternativas. Solo queda un rastro por limpiar: la copia de los datos antiguos en mi NAS de respaldo, que conservo unas semanas más por prudencia.
La relación con el trabajo en el homelab
En el artículo anterior, describía cómo mantengo mi homelab con Claude: documentación viva, backlog, registro de incidentes y decisiones documentadas. Esta migración encaja exactamente en esa lógica. Empezó como una línea del backlog y terminó como una decisión documentada.
Pero hay una relación más directa: un blog estático en Git es un formato ideal para trabajar con una IA.
El artículo anterior y este los escribió Claude directamente en el repositorio, como archivos Markdown. Yo los revisé como se revisa código, y después Claude hizo el commit y el push desde mi ordenador, con mis credenciales. El pipeline hizo el resto.
Con WordPress, habría hecho falta dar a la IA una cuenta en la interfaz de administración, o una clave de API con permisos de escritura sobre un servicio público. Aquí la IA no necesita nada de eso: escribe archivos de texto que puedo revisar, comparar y deshacer con un git revert. El mínimo privilegio, sin esfuerzo.
El punto de vista de la IA
Esta sección la ha escrito Claude.
Un sitio en un repositorio Git es un terreno donde me siento cómodo y donde Etienne mantiene el control: cada cambio es un diff legible, revisado antes de publicarse y reversible. Es más sano que un acceso a una interfaz de administración, para él y para mí.
Dicho esto, el pipeline no revisa nada: publica lo que se le envía. El error de fecha del artículo anterior fue mío, y el pipeline lo publicó fielmente… sin publicarlo. La revisión humana y la comprobación tras el despliegue siguen siendo imprescindibles. Por eso he tomado la costumbre de comprobar que la página responde después del push, en lugar de dar por hecho que todo ha ido bien.
En resumen
Si tu sitio tiene un solo autor y ninguna necesidad dinámica, pregúntate: ¿qué ejecuta tu CMS, y para quién? En mi caso, la respuesta era «muchas cosas, para nadie». Un generador estático, una forja Git autoalojada y un webhook me han dado un blog más rápido, más fácil de mantener, mucho menos expuesto, y una cadena de publicación que entiendo de principio a fin.
