Después de años con WordPress, he trasladado mi blog a Hugo. El motivo no fue un hecho concreto, sino un análisis sobrio de la situación: ya no quería PHP en producción, ni una base de datos que hay que parchear constantemente, y sobre todo quería un sistema en el que las herramientas de IA puedan editar, traducir y mantener contenidos de forma automatizada sin que yo tenga que preocuparme por la superficie de ataque.
En esta entrada explico primero por qué elegí Hugo en lugar de WordPress, con especial atención a la seguridad y a la automatización con IA. Después viene la guía práctica: cómo instalé Hugo en local en Windows, incluido el tema PaperMod, y cómo llega el sitio al servidor mediante GitHub Actions.
Hugo frente a WordPress: por qué cambié
WordPress impulsa una gran parte de la web y precisamente por eso es el objetivo preferido de los ataques automatizados. Su verdadera debilidad no está tanto en el núcleo de WordPress como en el ecosistema que lo rodea. Según el informe State of WordPress Security in 2026 de Patchstack, en 2025 se encontraron 11 334 nuevas vulnerabilidades en el entorno de WordPress, un 42 % más que el año anterior. El 91 % de ellas estaba en plugins, el 9 % en temas y solo seis en el propio núcleo de WordPress. Para el 46 % de las vulnerabilidades no existía todavía ningún parche en el momento de su publicación. La velocidad es especialmente crítica: según Patchstack, las vulnerabilidades graves se explotan por primera vez, en mediana, solo cinco horas después de su publicación, y aproximadamente la mitad en menos de 24 horas. Quien no actualiza prácticamente de forma permanente llega tarde.
Esta fragilidad se explica por la arquitectura. Un sitio de WordPress suele funcionar con muchos plugins, y cada uno de ellos es código independiente de un autor distinto con su propio ciclo de actualizaciones. Cada plugin es una puerta más hacia la instalación, y cada puerta necesita su propia cerradura, que hay que mantener de forma continua. A esto se suma que los atacantes también utilizan ya la IA para encontrar y explotar más rápido las vulnerabilidades conocidas.
Por qué Hugo es estructuralmente difícil de atacar
Hugo sigue un enfoque radicalmente distinto. No hay servidor de base de datos, ni PHP, ni un panel de administración accesible desde internet. Hugo genera, a partir de archivos Markdown, plantillas y front matter, archivos HTML estáticos terminados que después solo tiene que servir un servidor web sencillo. En el servidor no se ejecuta, por tanto, ningún código en el que se pueda explotar una vulnerabilidad: ni inyección SQL, ni ejecución remota de código PHP, ni un ecosistema de plugins vulnerable. Lo único que queda expuesto, en esencia, es el propio servidor web, y ese hay que endurecerlo y mantenerlo actualizado de todos modos, sea cual sea el CMS que haya detrás.
Esto no significa que los sitios hechos con Hugo sean invulnerables. Pero desaparece toda la categoría de ataques que hace tan vulnerable a WordPress, porque la superficie de ataque correspondiente sencillamente no existe.
Velocidad: servir en lugar de renderizar
Un aspecto que se nota enseguida en el día a día es la velocidad. WordPress monta cada página de nuevo en cada visita: se ejecuta PHP, se consulta la base de datos, se renderiza el resultado y solo entonces se sirve. Para cada visitante, una y otra vez. Los plugins de caché lo atenúan, pero en el fondo no son más que un parche sobre una arquitectura costosa.
Hugo invierte el principio. Las páginas se generan una sola vez durante el build; después, el servidor solo entrega archivos HTML terminados, sin renderizado, sin consultas a la base de datos y sin procesamiento en el servidor. Difícilmente se puede servir un sitio web más rápido, y además se puede distribuir por todo el mundo a través de una CDN sin esfuerzo. A ello se suma la velocidad del propio build: Hugo está escrito en Go y es uno de los generadores de sitios estáticos más rápidos. Un blog con unos cientos de páginas se genera en pocos segundos. Para el flujo de trabajo local, esto significa que guardo un cambio y lo veo en el navegador prácticamente en el mismo instante.
El factor IA: Markdown como interfaz de automatización
La tercera gran ventaja para mí está en cómo se guardan los contenidos. WordPress almacena las entradas en una base de datos relacional a la que una IA solo puede acceder a través de una API o un plugin, con todos los riesgos y dependencias que ello conlleva. Los contenidos de Hugo, en cambio, son simples archivos Markdown en el sistema de archivos, versionados con Git.
Esto permite un tipo de automatización completamente distinto: una IA puede leer y editar archivos Markdown directamente, traducir entradas a varios idiomas, ajustar el front matter de forma coherente o mantener los metadatos SEO. Cada uno de estos cambios queda registrado como commit de Git, en lugar de desaparecer sin dejar rastro en una base de datos. Los cambios se pueden revisar mediante pull request antes de publicarlos y, en caso de duda, deshacer con un revert de Git. En mi caso, por ejemplo, dos pequeños scripts locales de Python traducen automáticamente cada entrada al inglés y al español y guardan la traducción como index.en.md e index.es.md junto al original. Para un flujo de trabajo en el que las herramientas de IA colaboran regularmente en el contenido, esta es una base mucho más robusta y transparente que un CMS con una base de datos detrás.
Las desventajas de Hugo en comparación
El cambio también tiene un precio. Hugo no incluye una interfaz de edición. Los autores sin conocimientos técnicos necesitan un headless CMS adicional o ayuda con el despliegue. Las funciones dinámicas, como comentarios, búsqueda o formularios, no se pueden resolver en el servidor, sino que solo se pueden añadir mediante servicios externos o JavaScript. La búsqueda de PaperMod, por ejemplo, funciona por completo en el navegador mediante un índice JSON que Hugo genera durante el build. Y entre un cambio y su publicación en el servidor de producción siempre hay un paso de build. En local esto apenas se nota: gracias a la recarga en vivo, el servidor de desarrollo integrado muestra cada cambio al instante en el navegador, a menudo más rápido que la vista previa del editor de WordPress.
Para un blog de una sola persona con conocimientos técnicos y centrado en el rendimiento, la seguridad y el mantenimiento asistido por IA, las ventajas pesan claramente más para mí. Para un equipo editorial sin conocimientos de Git, la valoración habría sido distinta.
Requisitos previos: instalar Git y Hugo
Antes de empezar, necesitas dos herramientas en tu ordenador con Windows, que en esta configuración sirve como entorno de desarrollo y de pruebas. Parto de la base de que todavía no tienes ninguna de ellas instalada.
1. Git para Windows
Git se necesita para el control de versiones y para incluir el tema como submódulo. El instalador oficial se descarga aquí:
https://git-scm.com/download/win
Ejecuta el .exe descargado y sigue el asistente de instalación. La configuración predeterminada es suficiente. Solo asegúrate de que Git se añada al PATH del sistema; en la selección predeterminada ya es así.
2. Hugo (Extended Edition)
Hugo es el generador de sitios estáticos propiamente dicho. Yo uso la Extended Edition. Esta incluye además LibSass. Se trata de una biblioteca de software que traduce Sass a CSS. Sass es una extensión de CSS que permite escribir hojas de estilo de forma más breve y clara, por ejemplo con variables para los colores o con reglas anidadas. Los navegadores no entienden Sass, por eso Hugo tiene que convertirlo en CSS normal durante el build. SCSS es la sintaxis de Sass que se usa hoy en día: se parece a CSS, con llaves y punto y coma, y los archivos terminan en .scss. PaperMod funciona con CSS normal, pero muchos otros temas requieren la Extended Edition. Si la usas desde el principio, más adelante podrás cambiar de tema sin reinstalar Hugo. Por el sufijo +extended en la salida de la versión sabrás si está instalada la variante correcta.
La versión actual se encuentra en la página oficial de releases. La página muestra varias decenas de archivos para cada versión. Para un PC con Windows normal necesitas el archivo con el patrón hugo_extended_<versión>_windows-amd64.zip, es decir, para la versión 0.167.0, hugo_extended_0.167.0_windows-amd64.zip. El archivo con withdeploy en el nombre incluye además funciones para subir contenido a almacenamiento en la nube como AWS S3 y no hace falta aquí. Los archivos sin extended en el nombre corresponden a la Standard Edition, sin LibSass:
https://github.com/gohugoio/hugo/releases
Descomprime el archivo ZIP en una carpeta fija, por ejemplo C:\Hugo\Bin. Para que el comando hugo funcione en cualquier símbolo del sistema, tienes que añadir esta carpeta a la variable de entorno PATH:
- Busca “variables de entorno” en el menú Inicio y abre “Editar las variables de entorno del sistema”
- Haz clic en “Variables de entorno…”
- En “Variables del sistema”, selecciona la variable
Pathy haz clic en “Editar” - Haz clic en “Nuevo” e introduce la ruta de la carpeta de Hugo, p. ej.
C:\Hugo\Bin - Cierra todas las ventanas con “Aceptar”
Alternativa con un gestor de paquetes: desde la línea de comandos es más rápido. El gestor de paquetes de Windows winget suele venir preinstalado en las versiones actuales de Windows; si no, se puede instalar desde Microsoft Store como “Instalador de aplicación” (App Installer):
winget install --id Git.Git -e --source winget
winget install --id Hugo.Hugo.Extended -e --source winget
Si usas Chocolatey, instala Hugo con choco install hugo-extended. Después de cada instalación, abre un terminal nuevo para que se aplique la variable PATH modificada.
Comprobar la instalación
A continuación puedes comprobar en el símbolo del sistema si todo está configurado correctamente:
git --version
hugo version
En mi caso, la salida fue esta:
C:\Users\info>git --version
git version 2.54.0.windows.1
C:\Users\info>hugo version
hugo v0.163.2-19a5cec0b9618163bb519487382e861d29edf383+extended windows/amd64 BuildDate=2026-06-15T14:55:00Z VendorInfo=gohugoio
Por cierto, no hace falta instalar Go. Hugo es un programa ya compilado; Go solo se necesita si quieres compilar Hugo tú mismo o incluir temas como Hugo Modules en lugar de submódulos de Git.
Paso 1: crear el repositorio y la estructura del proyecto
Creé el repositorio Git en la carpeta C:\sources\aaron_de y puse el sitio de Hugo en la subcarpeta hugo. Así queda espacio junto al sitio para archivos auxiliares, como la configuración del pipeline en .github:
cd C:\sources\aaron_de
git init
hugo new site hugo
El comando hugo new site crea todo el árbol de carpetas que Hugo necesita para un proyecto: content, layouts, static, themes y el archivo de configuración central hugo.toml.
Paso 2: incluir el tema como submódulo de Git
PaperMod no se copia, sino que se incluye como submódulo de Git. Los archivos del tema no acaban en mi repositorio. Git solo guarda allí dos datos: la dirección del repositorio de PaperMod en GitHub (en el archivo .gitmodules) y el identificador del commit, es decir, la versión exacta del tema que utiliza mi sitio. Los comandos se ejecutan en el directorio raíz del repositorio, es decir, donde se encuentra la carpeta .git:
cd C:\sources\aaron_de
git submodule add --depth=1 https://github.com/adityatelange/hugo-PaperMod.git hugo/themes/hugo-PaperMod
El parámetro --depth=1 hace que solo se clone el estado actual del repositorio del tema, sin todo el historial de commits. Así se ahorra tiempo y espacio en disco.
Un tropiezo al volver a hacer checkout: si más adelante clonas el repositorio en otro ordenador o vuelves a hacer checkout, la carpeta hugo/themes/hugo-PaperMod estará vacía al principio. Git no descarga los submódulos automáticamente. Hugo se detiene entonces al arrancar con un error por el tema que falta. La solución es este comando en el directorio raíz del repositorio:
git submodule update --init --recursive
Como alternativa, puedes descargar el tema directamente al clonar. Para ello indica la dirección de tu propio repositorio en GitHub y la carpeta de destino:
git clone --recurse-submodules https://github.com/<usuario>/<repositorio>.git C:\sources\aaron_de
Sustituye <usuario> y <repositorio> por tu nombre de usuario de GitHub y el nombre de tu repositorio. GitHub muestra la dirección correspondiente en la página del repositorio, en el botón verde “Code”.
Paso 3: ajustar la configuración
En el archivo hugo/hugo.toml se define qué tema está activo y qué ajustes básicos se aplican. El valor de theme debe coincidir exactamente con el nombre de la carpeta dentro de themes:
baseURL = 'http://localhost:1313/'
locale = 'de-DE'
title = 'aaron.de'
theme = 'hugo-PaperMod'
[markup.goldmark.renderer]
unsafe = true
Sobre el ajuste unsafe = true: por defecto, Hugo no muestra el HTML escrito directamente en Markdown y lo sustituye por un comentario. Sin embargo, mis entradas migradas desde WordPress contienen fragmentos de HTML como <br/>. Solo con unsafe = true aparecen en la página. El nombre suena más peligroso de lo que es en este caso: el ajuste permite el HTML de mis propios archivos Markdown, no el de entradas de visitantes ajenos.
Paso 4: iniciar el servidor local
Por último, inicia el servidor de desarrollo integrado en la carpeta de Hugo:
cd C:\sources\aaron_de\hugo
hugo server -D
El flag -D hace que también se muestren las entradas con draft: true, lo que resulta práctico para las pruebas locales. A continuación, la consola muestra, entre otras cosas, cuántas páginas se han generado y dónde está disponible el servidor:
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
Press Ctrl+C to stop
Mientras el servidor esté en marcha, los cambios en el contenido o la configuración aparecen automáticamente en el navegador, sin necesidad de reiniciarlo.
De la prueba local al servidor de producción: mi pipeline de despliegue
Mi ordenador con Windows sirve exclusivamente como entorno de pruebas. Aquí escribo y reviso los contenidos en local con hugo server -D antes de que nada se publique. A partir de ahí, el camino hasta el servidor es automático:
- Cambio local: ajustar contenido o diseño en Windows y comprobarlo en el navegador en
localhost:1313 - Commit y push a GitHub: en cuanto una entrada está terminada, se hace commit y push al repositorio de GitHub
- Pipeline de CI/CD: un pipeline de GitHub Actions se inicia con el push, genera el sitio con Hugo y transfiere el resultado al servidor de destino
- Servidor de producción: el servidor web sirve los archivos estáticos recién generados sin que yo tenga que ejecutar nada en el propio servidor
La ventaja de esta configuración: al servidor de producción solo llegan archivos ya generados. Nadie edita contenidos directamente allí, y el pipeline ni siquiera genera las entradas con draft: true. Así traslado al blog el principio de staging y producción que conozco de los proyectos de software clásicos.
El pipeline de GitHub Actions en detalle
El pipeline es un archivo de workflow situado en el directorio raíz del repositorio, no en la subcarpeta hugo. En mi caso se encuentra en C:\sources\aaron_de\.github\workflows\deploy.yml. GitHub solo busca workflows en la carpeta .github\workflows directamente en el directorio raíz; un archivo en cualquier otro lugar no se ejecutaría nunca. El pipeline se inicia con cada push a la rama main y además se puede lanzar manualmente con el botón “Run workflow” de la pestaña Actions en GitHub:
name: Deploy Hugo Site
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build-deploy:
runs-on: ubuntu-latest
defaults:
run:
working-directory: hugo
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: 'latest'
extended: true
- name: Build with Production Settings
env:
HUGO_PARAMS_ENV: production
HUGO_ENV: production
run: |
hugo --gc --minify --baseURL "https://example.com/"
- name: Deploy to Server
uses: easingthemes/ssh-deploy@main
with:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
ARGS: "-rlgoDzvc -i --delete"
SOURCE: "hugo/public/"
REMOTE_HOST: ${{ secrets.SERVER_IP }}
REMOTE_USER: "aaron"
REMOTE_PORT: ${{ secrets.SERVER_PORT }}
TARGET: "/var/www/aaron/aaron_de/"
Algunos detalles importantes para este proceso:
working-directory: hugo: el sitio de Hugo no está en el directorio raíz del repositorio, sino en la subcarpetahugo. Por eso esta carpeta se define como directorio de trabajo para los pasos del build. El checkout consubmodules: recursivegarantiza que también se descargue el tema PaperMod. Sin esta línea, el pipeline tendría el mismo problema con la carpeta del tema vacía que un clon nuevo en tu propio ordenador.hugo-version: 'latest'yextended: true: el pipeline utiliza siempre la versión Extended más reciente de Hugo. La versión instalada en local solo sirve para la vista previa y no influye en el sitio publicado. Por eso la mantengo actualizada conwinget upgrade Hugo.Hugo.Extended(ochoco upgrade hugo-extended), para ver en local lo mismo que el pipeline.- Build de producción:
--gcelimina de la caché los archivos que ya no se necesitan tras el build, y--minifyreduce el tamaño de HTML, CSS y JS. Sin más opciones, el comandohugogenera el sitio en el entornoproduction, mientras que elhugo serverlocal lo hace en el entornodevelopment. PaperMod solo incluye la analítica y las etiquetas de verificación en el entorno de producción, y solo allí permite que los buscadores indexen el sitio. Por eso mis visitas de prueba en local no aparecen en las estadísticas. Las variablesHUGO_ENVyHUGO_PARAMS_ENVfijan además de forma explícita el entorno de producción en el pipeline.HUGO_PARAMS_ENVsobrescribe el parámetroenv = "development"de mihugo.toml. Por cierto, los borradores no aparecen en el build simplemente porquehugose ejecuta sin-D. easingthemes/ssh-deploy: transfiere el contenido dehugo/public/al servidor de destino mediante rsync por SSH. El flag--deletede losARGSelimina en el servidor los archivos que ya no existen en el nuevo build, para que no queden páginas antiguas huérfanas.- GitHub Secrets:
SSH_PRIVATE_KEY,SERVER_IPySERVER_PORTse guardan en Settings → Secrets and variables → Actions, para que no haya credenciales en texto plano en el repositorio. La clave pública correspondiente está en el archivoauthorized_keysdel usuario de despliegue en el servidor.
Para los ejemplos de esta entrada he sustituido el dominio real por un marcador de posición. En el archivo de producción figura en ese lugar la dirección real del blog. Si solo quieres lanzar el workflow manualmente, elimina el trigger push y conserva únicamente workflow_dispatch.
Mantener actualizados el tema y Hugo
Un submódulo se queda en la versión que tenía cuando se añadió. Un simple git submodule update no descarga una nueva versión del tema, sino que solo restaura la versión a la que apunta el repositorio. Para una actualización real hace falta la opción --remote. Los comandos se ejecutan de nuevo en el directorio raíz del repositorio:
cd C:\sources\aaron_de
git submodule update --remote hugo/themes/hugo-PaperMod
cd hugo
hugo server -D
Si el sitio se ve bien en local, hago commit y push de la nueva referencia. El pipeline genera entonces el sitio con la nueva versión del tema y lo despliega en el servidor:
cd C:\sources\aaron_de
git add hugo/themes/hugo-PaperMod
git commit -m "Update PaperMod theme"
git push
Conclusión: menos complejidad, más control
Para mí, el paso de WordPress a Hugo fue sobre todo un paso hacia menos piezas móviles. Sin base de datos, sin ejecución en el servidor, sin actualizaciones de seguridad constantes para un CMS. En su lugar, un conjunto de archivos estáticos que se pueden versionar con Git y servir en prácticamente cualquier infraestructura.
Junto con el pipeline de GitHub Actions, el resultado es una configuración en la que el ordenador con Windows sirve únicamente como entorno de pruebas, GitHub es el lugar de referencia para todos los contenidos y el servidor de producción solo recibe archivos estáticos ya generados.
Entretanto he trasladado también varios proyectos más a Hugo y sigo muy contento con esta decisión.
