Cómo crear un blog sin servidores con dominio propio usando Hugo + GitHub + Decap CMS + Cloudflare Pages
Este sitio llevaba años con el dominio propio enlazado a Google Sites (de la época de Google Apps). Cuando el servicio dejó de estar disponible, terminé dejando https://www.ixam.net aparcado. Decidí renovarlo y aprovechar para darle un uso real: quería algo serverless y que no dependiera de que un servicio tipo Google Sites desapareciera. Así que me lancé a intentarlo con la ayuda de ChatGPT.
Pensé que sería fácil con solo ChatGPT, pero no. Se dejaba cabos sueltos en los requisitos básicos, el troubleshooting era un bucle infinito… Al final tuve que buscar repositorios en GitHub, como https://github.com/patrickgrey/website/, y con eso y trabajo en equipo logramos terminarlo.
La sensación fue la de guiar a un subordinado con talento técnico pero lleno de malentendidos hasta que el proyecto salió adelante. De alguna manera me sorprendió que la IA haya llegado a ese nivel. Y cuando las cosas se complican, saca su lado desesperante y comete errores absurdos que ponen a prueba la paciencia. Quizá eso también sea muy humano.
Por ahora parece que funciona mejor no pretender que entienda todo el contexto tras una charla larga, sino que cuando la conversación se enreda, conviene que la persona haga un resumen, empiece un hilo nuevo y dé instrucciones frescas. Aun así, sinceramente creo que, sin la IA, yo solo no habría podido con el volumen de investigación, trabajo y aprendizaje. La productividad de la IA generativa es una locura.
Objetivo
- Mostrar el blog en
https://www.example.com/
- Poder iniciar sesión en DecapCMS desde
https://www.example.com/admin/
y crear artículos - Que los artículos se confirmen en GitHub y Cloudflare los despliegue de forma automática
- Por cierto, a día de hoy no hay gastos adicionales de operación (el dominio ya estaba contratado)
0. Supuestos y estructura de carpetas
0-1. Supuestos
-
El dominio (
example.com
) ya está adquirido -
El sistema operativo puede ser Windows o macOS (las órdenes se muestran para ambos)
-
Servicios a utilizar (basta con las capas gratuitas)
- Cuenta de GitHub
- Cuenta de Cloudflare (usaremos Pages)
- Git (en local)
- Hugo (para previsualizar en local y para la construcción en Cloudflare)
0-2. Estructura principal del repositorio (ejemplo)
hugo.toml
content/
blog/
posts/
data/
functions/
api/
auth.js
callback.js
layouts/
_default/
baseof.html
index.html
list.html
single.html
static/
_headers
admin/
config.yml
index.html
css/
main.css
Esta estructura cumple con los siguientes roles:
hugo.toml
… Configuración global del sitio (sustituyebaseURL
por la URL de producción)functions/api/*.js
… Cloudflare Pages Functions (para el OAuth de GitHub en/api/auth
y/api/callback
)layouts/_default/*.html
… Plantillas de Hugostatic/admin/
… Pantalla y configuración de DecapCMSstatic/css/main.css
… Apariencia (CSS)static/_headers
… Configuración de cabeceras HTTP en Cloudflare Pages (opcional)
Nota: en este manual, los lugares que requieren sustituciones según el entorno se marcan como “puntos de reemplazo”.
1. Preparación previa
1-1. Crear una cuenta de GitHub
- Abre GitHub en el navegador y regístrate
1-2. Crear una cuenta de Cloudflare
- Accede a Cloudflare desde el navegador y regístrate
1-3. Instalar Git y Hugo
-
Windows (PowerShell)
PS C:\Users\alice> winget install Git.Git PS C:\Users\alice> winget install Hugo.Hugo.Extended
-
macOS (Terminal)
mac:~ dev$ brew install git mac:~ dev$ brew install hugo
2. Procedimiento principal (hasta el despliegue)
2-1. Preparar el repositorio local
-
Crea un repositorio vacío en GitHub
- Navegación: GitHub > New repository
- Ejemplo de nombre:
my-hugo-blog
-
Clónalo en local
-
Windows
PS C:\work> git clone https://github.com/<YOUR_GH_USERNAME>/my-hugo-blog.git PS C:\work> cd .\my-hugo-blog
-
macOS
mac:~/work dev$ git clone https://github.com/<YOUR_GH_USERNAME>/my-hugo-blog.git mac:~/work dev$ cd my-hugo-blog
-
-
Coloca todas las carpetas y archivos anteriores en la raíz de ese repositorio
Puntos de reemplazo (no lo dejes sin modificar)
-
hugo.toml
baseURL = "https://www.example.com" # ← Sustituye por la URL de producción (ej.: https://www.example.com) languageCode = "ja-jp" title = "Example Blog" publishDir = "public" [permalinks] blog = "/blog/:year/:month/:slug/"
-
static/admin/config.yml
backend: name: github repo: <YOUR_GH_USERNAME>/my-hugo-blog # ← Sustituye por tu repositorio en GitHub branch: master # ← Rama por defecto (main o master) base_url: https://www.example.com # ← Sustituye por la URL de producción auth_endpoint: /api/auth # ← Endpoint de Functions (fijo) media_folder: static/uploads public_folder: /uploads
Referencia:
functions/api/auth.js
yfunctions/api/callback.js
pueden dejarse tal cual (usanurl.origin
, así que no hay valores dependientes del entorno escritos a mano).
-
Primer commit y push
-
Windows
PS C:\work\my-hugo-blog> git add -A PS C:\work\my-hugo-blog> git commit -m "Initial commit: Hugo + [DecapCMS](https://decapcms.org/) + CF Pages" PS C:\work\my-hugo-blog> git push -u origin master
-
macOS
mac:~/work/my-hugo-blog dev$ git add -A mac:~/work/my-hugo-blog dev$ git commit -m "Initial commit: Hugo + [DecapCMS](https://decapcms.org/) + CF Pages" mac:~/work/my-hugo-blog dev$ git push -u origin master
-
2-2. Crear la aplicación OAuth de GitHub (para el inicio de sesión en DecapCMS)
-
Navegación: GitHub > Settings > Developer settings > OAuth Apps > New OAuth App
-
Campos:
- Application name:
[DecapCMS](https://decapcms.org/) for my-hugo-blog
- Homepage URL:
https://www.example.com
- Authorization callback URL:
https://www.example.com/api/callback
← Importante
- Application name:
-
Guarda los valores mostrados:
- Client ID
- Client Secret (genera uno nuevo y consérvalo)
Estos valores se establecen como variables de entorno en Cloudflare Pages.
2-3. Crear el proyecto de Cloudflare Pages
-
Navegación: Panel de Cloudflare > Pages > Create a project > Connect to Git
-
Conecta con GitHub y selecciona el repositorio
my-hugo-blog
-
Configuración de build:
-
Framework preset:
None
(o deja que Cloudflare lo detecte) -
Build command:
hugo
-
Build output directory:
public
-
Variables de entorno:
HUGO_VERSION
=0.128.0
(ejemplo. Conviene que coincida con tu Hugo local)GITHUB_CLIENT_ID
= (valor obtenido en la sección anterior)GITHUB_CLIENT_SECRET
= (valor obtenido en la sección anterior)
-
-
Pulsa Save and Deploy y espera el primer despliegue
- Si todo va bien, se generará una URL de vista previa
*.pages.dev
- Si todo va bien, se generará una URL de vista previa
2-4. Asignar el dominio propio www.example.com
-
Navegación: Cloudflare > Pages > (proyecto correspondiente) > Custom domains > Set up a custom domain
-
Introduce
www.example.com
y añádelo -
Si no transferiste el dominio a Cloudflare:
- Anota los registros DNS que muestra Cloudflare (ej.:
CNAME www -> <project>.pages.dev
) - Ve al panel del registrador donde administras el dominio y configura los registros siguiendo esa información (consulta el procedimiento de cada registrador)
- Anota los registros DNS que muestra Cloudflare (ej.:
-
Una vez propagados los cambios, verifica que
https://www.example.com/
se abre correctamente
Consejo: si el dominio ya está añadido en Cloudflare, basta con un clic para crear los registros DNS necesarios.
2-5. Entrar al panel de DecapCMS
- Abre
https://www.example.com/admin/
en el navegador - Haz clic en el botón “Login with GitHub” o similar para autorizar el OAuth de GitHub
- En el primer intento GitHub preguntará si autorizas la aplicación: pulsa Authorize
Si el inicio de sesión falla, revisa el Callback URL del OAuth y las variables de entorno en Cloudflare (
GITHUB_CLIENT_ID/SECRET
).
3. Operación diaria (creación de artículos, sincronización local, edición de plantillas)
3-1. Crear artículos desde el CMS
- Accede a
https://www.example.com/admin/
- En el menú lateral: Blog → New blog
- Rellena
Title
,Publish Date
,Description
,Body
- Al pulsar Publish, se realiza un commit automático en GitHub → Cloudflare despliega automáticamente
Los Markdown generados se guardan en
content/blog/
.
3-2. Actualizar el repositorio local (después de publicar desde el CMS)
-
Windows
PS C:\work\my-hugo-blog> git pull origin master
-
macOS
mac:~/work/my-hugo-blog dev$ git pull origin master
Así traes las entradas recientes al entorno local y puedes modificar plantillas o CSS con seguridad.
3-3. Ajustar el diseño o las plantillas en local
-
Inicia el servidor de desarrollo en local
-
Windows
PS C:\work\my-hugo-blog> hugo server -D
-
macOS
mac:~/work/my-hugo-blog dev$ hugo server -D
-
Abre
http://localhost:1313/
en el navegador para comprobar
-
-
Puntos típicos de cambio
layouts/_default/baseof.html
…<head>
, encabezado y pie de páginalayouts/_default/index.html
… Lista de “entradas recientes” de la portadalayouts/_default/single.html
… Cuerpo de las páginas de artículostatic/css/main.css
… Colores, tipografías, márgenes
-
Haz commit y push de los cambios
-
Windows
PS C:\work\my-hugo-blog> git add -A PS C:\work\my-hugo-blog> git commit -m "Update theme/layouts" PS C:\work\my-hugo-blog> git push
-
macOS
mac:~/work/my-hugo-blog dev$ git add -A mac:~/work/my-hugo-blog dev$ git commit -m "Update theme/layouts" mac:~/work/my-hugo-blog dev$ git push
-
Pasados unos segundos o minutos, Cloudflare despliega automáticamente
-
3-4. Recomendaciones sobre el uso de ramas
- Asegúrate de que la rama por defecto (
main
omaster
) coincide entreconfig.yml
de DecapCMS y la configuración de Cloudflare - Si quieres previsualizar cambios, crea un Pull Request en GitHub: Cloudflare Pages generará automáticamente un entorno de Preview
4. Consejos útiles (opcional)
4-1. Ejemplo de static/_headers
(no cachear la consola de administración)
/admin/*
Cache-Control: no-store
4-2. Robots y sitemap (según necesites)
- Crea
static/robots.txt
para controlar los rastreadores - Puedes ampliar las salidas en
hugo.toml
añadiendooutputs
y generar RSS o sitemap
5. Problemas frecuentes y cómo resolverlos
- El login del CMS se queda cargando
→ Comprueba que el Callback URL de GitHub OAuth eshttps://www.example.com/api/callback
y que las variablesGITHUB_CLIENT_ID/SECRET
en Cloudflare son correctas - La portada funciona pero los artículos dan 404
→ Asegúrate de quebaseURL
enhugo.toml
coincide con el dominio real. Revisa en los registros de build de Cloudflare si se generópublic/
- /admin aparece en blanco
→ Comprueba que la carga de DecapCMS enstatic/admin/index.html
no está bloqueada (por ejemplo, por extensiones del navegador). Desactiva extensiones y recarga
6. Resumen de reemplazos y ajustes (recordatorio)
Archivo/configuración | Clave | Valor a establecer (ejemplo) |
---|---|---|
hugo.toml |
baseURL |
https://www.example.com |
static/admin/config.yml |
repo |
<YOUR_GH_USERNAME>/my-hugo-blog |
static/admin/config.yml |
branch |
Ajusta a main o master según corresponda |
static/admin/config.yml |
base_url |
https://www.example.com |
Cloudflare Pages | HUGO_VERSION |
Ej.: 0.128.0 |
Cloudflare Pages | GITHUB_CLIENT_ID |
Client ID de la app OAuth |
Cloudflare Pages | GITHUB_CLIENT_SECRET |
Client Secret de la app OAuth |
GitHub OAuth App | Callback URL | https://www.example.com/api/callback |
Cloudflare Pages | Custom domain | Añade www.example.com |
7. Apéndice: archivos de ejemplo (sustituye lo necesario)
7-1. functions/api/auth.js
// Cloudflare Pages Functions (/api/auth)
export async function onRequest(context) {
const { request, env } = context;
const client_id = env.GITHUB_CLIENT_ID;
try {
const url = new URL(request.url);
const redirectUrl = new URL('https://github.com/login/oauth/authorize');
redirectUrl.searchParams.set('client_id', client_id);
redirectUrl.searchParams.set('redirect_uri', `${url.origin}/api/callback`);
redirectUrl.searchParams.set('scope', 'repo user');
redirectUrl.searchParams.set('state', crypto.getRandomValues(new Uint8Array(12)).join(''));
return Response.redirect(redirectUrl.href, 302);
} catch (err) {
return new Response(String(err?.message || err), { status: 500 });
}
}
7-2. functions/api/callback.js
function renderBody(status, content) {
const html = `
<script>
const receiveMessage = (message) => {
window.opener.postMessage(
'authorization:github:${status}:${JSON.stringify(content)}',
message.origin
);
window.removeEventListener("message", receiveMessage, false);
}
window.addEventListener("message", receiveMessage, false);
window.opener.postMessage("authorizing:github", "*");
</script>`;
return new Blob([html]);
}
export async function onRequest(context) {
const { request, env } = context;
const client_id = env.GITHUB_CLIENT_ID;
const client_secret = env.GITHUB_CLIENT_SECRET;
try {
const url = new URL(request.url);
const code = url.searchParams.get('code');
const response = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: { 'content-type': 'application/json', 'user-agent': 'cf-pages-oauth', 'accept': 'application/json' },
body: JSON.stringify({ client_id, client_secret, code }),
});
const result = await response.json();
if (result.error) {
return new Response(renderBody('error', result), { headers: { 'content-type': 'text/html;charset=UTF-8' }, status: 401 });
}
const token = result.access_token;
const provider = 'github';
return new Response(renderBody('success', { token, provider }), { headers: { 'content-type': 'text/html;charset=UTF-8' }, status: 200 });
} catch (error) {
return new Response(error.message, { headers: { 'content-type': 'text/html;charset=UTF-8' }, status: 500 });
}
}
7-3. static/admin/config.yml
backend:
name: github
repo: <YOUR_GH_USERNAME>/my-hugo-blog
branch: main
base_url: https://www.example.com
auth_endpoint: /api/auth
media_folder: static/uploads
public_folder: /uploads
collections:
- name: 'blog'
label: 'Blog'
folder: 'content/blog'
create: true
slug: '{{slug}}'
editor:
preview: false
fields:
- { label: 'Title', name: 'title', widget: 'string' }
- { label: 'Publish Date', name: 'date', widget: 'datetime' }
- { label: 'Description', name: 'description', widget: 'string' }
- { label: 'Body', name: 'body', widget: 'markdown' }
9. Puntos clave de SEO y rendimiento
Después de construir el blog, estas acciones ayudan a mejorar tanto el posicionamiento como la velocidad de carga.
-
Configurar sitemap y robots.txt
- Registra
public/sitemap.xml
, generado por Hugo, en Google Search Console - Añade reglas de rastreo en
static/robots.txt
- Registra
-
Configurar OGP (Open Graph Protocol) y tarjetas de Twitter
- Inserta etiquetas como
<meta property="og:title">
o<meta property="og:image">
enlayouts/_default/baseof.html
- Mejora la apariencia cuando se comparte en redes sociales
- Inserta etiquetas como
-
Convertir imágenes a WebP
- Aprovecha
resources/_gen/images/
y la canalización de Hugo para generar WebP automáticamente - Mejora la velocidad de carga
- Aprovecha
-
Agregar funciones de categorías y etiquetas
- Añade
categories
ytags
en el Front Matter decontent/blog/
y muéstralos en la plantilla como nube de etiquetas
- Añade
-
Introducir datos estructurados
- Define información de los artículos en formato JSON-LD para aspirar a los rich results en buscadores
8. Conclusión
- Crea el repositorio en GitHub, enlázalo con Cloudflare Pages, configura el OAuth de GitHub para DecapCMS y asigna tu dominio: con eso tendrás el entorno listo
- Los artículos se crean desde
/admin/
→ se confirman en GitHub → Cloudflare despliega de forma automática - Ajusta plantillas y diseño en local con
hugo server
, y sube los cambios con Git
Así queda construido y operativo un entorno de blog serverless con Hugo + GitHub + Decap CMS + Cloudflare.
Con esto tenemos una base de sitio muy ligera en costos, pero aún falta pulir funcionalidades como la búsqueda interna, categorías o la nube de etiquetas. La comparación con otros blogs aún deja que desear, aunque es un compromiso asumible. Poco a poco seguiré ampliándolo en tándem con ChatGPT.