El post anterior cubrió cómo hospedar un sitio personal en Cloudflare Workers. Una vez que eso funcionó, el siguiente paso obvio fue un blog. La pregunta era: ¿cómo agregas contenido dinámico — una lista de posts, páginas individuales — a un Worker que no tiene servidor de origen ni base de datos?
La respuesta resultó ser más simple de lo esperado: Cloudflare KV.
Por qué no un CMS
Las opciones que consideré:
- Ghost / WordPress — requiere un VPS, una base de datos, actualizaciones constantes. Exagerado para un blog personal.
- CMS headless (Contentful, Sanity) — dependencia de una API externa, niveles de precio, otra cuenta que administrar.
- Generador de sitios estáticos (Hugo, Astro) — requiere un build step, más herramientas, menos control sobre el output.
Lo que realmente quería: escribir archivos Markdown localmente, correr un comando y que los posts aparecieran en el sitio. Sin panel de administración. Sin servicio externo. Sin output de build que desplegar por separado.
KV hizo eso posible.
El modelo de almacenamiento
Cloudflare KV es un almacén de clave-valor disponible para Workers en el edge. No es una base de datos relacional — no puedes hacer queries. Pero para un blog no necesitas queries: necesitas guardar un índice de posts y el contenido de cada uno.
El esquema terminó siendo dos tipos de claves:
index → array JSON con metadatos de posts (title, date, excerpt, slug, image)
post:<slug> → string Markdown crudo de cada post
Leer el índice para renderizar la lista del blog:
jsconst posts = await env.POSTS_KV.get("index", "json");
Leer un post individual:
jsconst content = await env.POSTS_KV.get(`post:${slug}`, "text");
Dos lecturas de KV por carga de página. Sin connection pooling, sin ORM, sin query planner. El Worker simplemente lee un valor y renderiza HTML.
El script de publicación es el CMS
En lugar de un panel de administración, el "CMS" es un script de Node.js que corre localmente: scripts/publish.js.
Lo que hace, en orden:
- Lee todos los archivos
.mddeposts/ - Valida que cada uno tenga
title,dateyexcerpten el frontmatter - Salta los posts marcados como
draft: trueo con fecha futura - Si existe una imagen en
images/<slug>.png, la sube a R2 y agregaimage: trueal frontmatter - Escribe
scripts/kv-seed.json— un array de pares{ key, value } - Corre
wrangler kv bulk putpara subir todo al KV de una sola vez
El comando de publicación:
bashnpm run publish
Eso es todo. Sin login, sin API keys en archivos de config, sin dashboard. Wrangler usa el mismo token de API de Cloudflare ya configurado para los deploys.
Enrutamiento en el Worker
El Worker maneja tres rutas relacionadas con el blog:
js// /blog — lista de todos los posts
if (path === "/blog") {
const posts = await env.POSTS_KV.get("index", "json");
return htmlResponse(renderBlogIndex(posts));
}
// /blog/:slug — post individual
if (path.startsWith("/blog/")) {
const slug = path.slice("/blog/".length);
const posts = await env.POSTS_KV.get("index", "json");
const meta = posts.find(p => p.slug === slug);
if (!meta) return htmlResponse(render404(), 404);
const content = await env.POSTS_KV.get(`post:${slug}`, "text");
return htmlResponse(renderPost({ ...meta, content }, posts));
}
El Markdown se convierte a HTML en el servidor usando marked:
jsimport { marked } from "marked";
const html = marked.parse(content);
Sin renderizado en el cliente. El navegador recibe HTML completo. No se necesita JavaScript para mostrar un post.
Borradores y posts programados
Dos campos del frontmatter controlan la visibilidad:
markdown---
title: "Mi Post"
date: 2026-06-01
draft: true
---
draft: true— publish salta el post completamente, sin importar la fecha- Fecha futura — publish lo salta hasta que llegue esa fecha; usa
--include-futurepara ignorar esto
Ambos se verifican al momento de publicar, no al momento de la request. El Worker nunca ve posts no publicados porque simplemente no están en KV.
Qué hace el CI
Hacer push a main con posts nuevos activa el pipeline automáticamente:
- Detecta cambios en
posts/oimages/ - Corre
npm run publish— sube imágenes a R2, generakv-seed.json, siembra KV - Auto-commitea
kv-seed.jsony el frontmatter actualizado de vuelta amain
No se necesita deploy manual después de la configuración inicial. Escribes el post, creas el PR, le haces merge.
Qué haría diferente
- Agregar syntax highlighting desde el principio. Los posts con código se ven mal sin él. Un tokenizador server-side personalizado no cuesta dependencias adicionales.
- Mantener el índice pequeño. Guardar el Markdown completo en el índice rápidamente alcanzaría el límite de 25 MB de KV. Separar metadatos del contenido es la decisión correcta desde el inicio.
- Probar con
--dry-runantes del primer publish real. El script imprime todo lo que haría sin tocar Cloudflare. Me evitó publicar posts a medias.
El resultado
Un blog completamente funcional — lista de posts, páginas individuales, feed RSS, sitemap XML — corriendo completamente en el edge de Cloudflare. Sin servidor de origen, sin base de datos, sin mantenimiento. Costo de hosting: $0. La única pieza móvil es el comando publish.
La desventaja es que publicar no es instantáneo desde el celular. Pero para un blog técnico donde los posts se escriben en un editor de texto de todas formas, un script local es la abstracción correcta.
Próximo post: automatizar la publicación de posts desde el celular con GitHub Actions.