↑ top
← Back to blog
EN | ES

Cómo agregar un blog a un Cloudflare Worker sin CMS — KV de Cloudflare como base de datos.

Sin base de datos, sin panel de administración, sin servidor de origen. Solo archivos Markdown, un script de publicación y Cloudflare KV.

✦ AI Summary

El post anterior cubrió cómo hospedar un sitio personal en Cloudflare Workers. Una vez that eso funcionó, el siguiente paso obvio fue un blog.

Cómo agregar un blog a un Cloudflare Worker sin CMS — KV de Cloudflare como base de datos.

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:

  1. Lee todos los archivos .md de posts/
  2. Valida que cada uno tenga title, date y excerpt en el frontmatter
  3. Salta los posts marcados como draft: true o con fecha futura
  4. Si existe una imagen en images/<slug>.png, la sube a R2 y agrega image: true al frontmatter
  5. Escribe scripts/kv-seed.json — un array de pares { key, value }
  6. Corre wrangler kv bulk put para 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-future para 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:

  1. Detecta cambios en posts/ o images/
  2. Corre npm run publish — sube imágenes a R2, genera kv-seed.json, siembra KV
  3. Auto-commitea kv-seed.json y el frontmatter actualizado de vuelta a main

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-run antes 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.

Was this helpful?