De Sanity a un Dashboard Propio: Por Que Tome el Control de mi Blog
Migre mi blog de Sanity a un dashboard de administracion propio construido sobre Convex. Aqui esta la arquitectura, los componentes y las decisiones detras de ello.

Hace unas semanas publique en LinkedIn que habia migrado mi blog a un panel de administracion propio y que iba a compartir la arquitectura que diseñe. Este es ese post.
Cuando construi la primera version de este portfolio, la decision natural fue usar un headless CMS. Sanity era la opcion obvia: ecosistema maduro, GROQ como lenguaje de consultas, studio integrado y buena documentacion. Me llevo un par de horas tener contenido funcionando en produccion.
Pero con el tiempo empece a notar la friccion.
El Problema con Depender de un Tercero
No hay nada malo con Sanity como producto. El problema fue que mi caso de uso fue creciendo en una direccion que el CMS no cubria bien sin configuracion adicional.
Queria soporte bilingue real, no un plugin. Queria control sobre el esquema sin pasar por el Studio. Queria integrar comentarios, likes, bookmarks y un sistema de publicacion programada, todo dentro del mismo modelo de datos. Y queria hacerlo sin pagar por un plan superior o depender de la disponibilidad de una API externa para que mi blog funcionara.
La pregunta no fue si migrar, sino a que.
Evaluando Alternativas
Considere tres caminos:
Seguir con Sanity pero extenderlo: viable, pero la deuda de configuracion crecia con cada feature nueva. El Studio es poderoso pero esta disenado para equipos de contenido, no para un developer solo que quiere moverse rapido.
Usar un CMS diferente (Contentful, Prismic, Storyblok): estaba cambiando un problema por otro del mismo tipo.
Construir un dashboard propio sobre Convex: ya usaba Convex como backend para otras partes del proyecto. Tiene queries reactivas, mutaciones tipadas, cron jobs integrados y un esquema que yo controlo completamente. El costo de entrada era escribir el editor y los formularios, pero la ganancia era un sistema cohesionado donde todo vive en el mismo lugar.
Fui con la tercera opcion.
La Arquitectura Actual
El blog hoy es un sistema de tres capas: el frontend publico en Next.js, el panel de administracion protegido por Clerk, y Convex como unica fuente de verdad.
Next.js como orquestador
Las paginas publicas (/blog, /blog/[id]) son Server Components con ISR a 60 segundos. No hay llamadas directas a Convex desde el cliente en la ruta publica, todo pasa por rutas de API que funcionan como una capa de separacion entre el frontend y el backend.
El panel de administracion esta protegido por Clerk. Solo usuarios autorizados pueden acceder a el, cualquier otro recibe una respuesta de acceso denegado antes de que se cargue cualquier componente de la interfaz.
Convex como backend
La migracion me obligo a pensar en el esquema desde cero. Defini una tabla blogPosts con soporte bilingue en cada campo relevante, control de visibilidad (published, featured), soporte para contenido premium (isPremium, price) y publicacion programada (autoPublish, publishedAt).
Las tablas de interaccion (postViews, comments, commentLikes, likes, bookmarks) viven en el mismo esquema, lo que hace que las queries sean directas y sin overhead de API externa.
El cron job de auto-publicacion se ejecuta cada 15 minutos y busca posts con autoPublish = true y publishedAt <= Date.now(). Sin webhooks, sin colas externas.
Cloudflare R2 para imagenes
En lugar de usar el CDN de Sanity o una solucion como Cloudinary, decidi usar R2. La latencia es buena, el modelo de precios es predecible y la integracion con Workers es directa si en algun momento necesito transformaciones en el edge. El upload se hace desde una ruta de API protegida que genera la URL publica y la almacena en el campo image del post.
El Calendario de Contenido
Una de las piezas que no existia en Sanity y que mas uso en el dia a dia es el calendario de contenido. Es una vista de calendario que me permite planear ideas de posts con fecha objetivo, agregar notas de referencia y vincular el recordatorio directamente con un post cuando decido publicarlo.
No es un gestor de tareas sofisticado. Es una tabla contentCalendar en Convex con titulo, fecha, notas y un campo opcional que apunta al post resultante. Lo que importa es que vive en el mismo sistema, no en un Notion aparte que eventualmente deja de sincronizarse con la realidad de lo que publico.
El Editor
La parte mas larga de implementar fue el editor. No queria un campo de texto plano, necesitaba algo que mostrara el resultado real mientras escribia.
Use @uiw/react-md-editor con un panel de preview que renderiza el mismo componente que usa el blog publico. Esto incluye resaltado de sintaxis, tablas, blockquotes y diagramas Mermaid. Hay un builder de diagramas integrado que me permite construir el Mermaid visualmente y pegar el resultado en el contenido.
El editor tiene dos tabs: ingles y espanol. Cada tab tiene su propio par de campos de titulo, extracto y contenido. Los metadatos (categoria, tags, tiempo de lectura, imagen destacada, controles de publicacion) estan en un panel lateral.
Los Componentes del Blog
La arquitectura resuelve donde vive el dato. Pero el trabajo real estuvo en construir los componentes que hacen que el blog sea util de leer y de administrar.
Lector publico
BlogShell maneja filtrado por categoria, busqueda en tiempo real por titulo, extracto y tags, y paginacion de 9 posts por pagina. El primer post con featured: true se renderiza como hero con imagen a ancho completo.
BlogPostContent orquesta el articulo individual: navegacion anterior/siguiente, posts relacionados de la misma categoria, card del autor y todos los componentes de interaccion.
CollapsibleTOC genera la tabla de contenidos a partir del markdown, extrae los headings H2 y H3, y usa un IntersectionObserver para destacar la seccion activa mientras el usuario navega. Es colapsable porque en mobile ocupa demasiado espacio si queda fija.
ScrollProgress renderiza una barra en la parte superior del viewport que refleja el porcentaje de avance de lectura calculado en cada frame.
ViewCounter incrementa el conteo al cargar el post pero guarda en localStorage la ultima visita por slug. Solo cuenta una nueva vista si pasaron mas de 24 horas, evitando inflar los numeros con recargas.
MarkdownRenderer convierte el contenido con react-markdown, remark-gfm y rehype-highlight con tema github-dark. Los headings reciben un id automatico para que el TOC pueda anclarlos. Los bloques con lenguaje mermaid se interceptan y pasan al MermaidViewer.
MermaidViewer carga Mermaid con dynamic import y ssr: false para no afectar el tiempo de carga inicial. Una vez renderizado el SVG, usa react-zoom-pan-pinch para zoom y paneo sobre diagramas grandes.
Interaccion
LikeButton persiste en Convex y es anonimo. BookmarkButton requiere sesion de Clerk y guarda una copia del titulo e imagen del post para evitar queries adicionales al mostrarlo en el perfil.
CommentsSection soporta comentarios de primer nivel y respuestas anidadas un nivel, likes por comentario, y autenticacion via Clerk para publicar. El tiempo se muestra en formato relativo calculado en el cliente.
Panel de administracion
PostEditor tiene dos tabs (ingles y espanol) con sus campos de titulo, extracto y contenido. El preview renderiza el mismo MarkdownRenderer del blog publico: lo que ves en el editor es exactamente lo que vera el lector.
El panel lateral concentra los metadatos, el upload de imagen a R2 y los controles de publicacion. Hay un generador de slug desde el titulo y un builder de diagramas Mermaid con preview en vivo antes de insertar el codigo en el contenido.
La Migracion
No habia mucho contenido en Sanity, pero decidi hacer la migracion de forma correcta de todas formas. Escribi un script en scripts/migrate-sanity-to-convex.ts que:
- Consulta todos los posts en Sanity usando GROQ
- Mapea los campos al nuevo esquema de Convex
- Descarga las imagenes del CDN de Sanity
- Las re-sube a R2
- Inserta los posts en Convex con las nuevas URLs
Se ejecuta una sola vez con pnpm migrate:blog. El script fue desechable por diseno, no hay logica de idempotencia sofisticada porque no la necesitaba.
Lo que Gane
El resultado final es un sistema donde tengo control completo sobre cada parte del stack. Puedo cambiar el esquema sin tocar un Studio externo. Puedo agregar features de interaccion directamente en Convex sin integrar APIs de terceros. El dashboard carga rapido porque es parte de la misma aplicacion.
El costo fue tiempo de desarrollo inicial, que fue mayor que simplemente usar un CMS. Pero ese tiempo fue una inversion en un sistema que ahora escala exactamente como yo necesito que escale.
No es la solucion correcta para todos los proyectos. Para un equipo de contenido grande, un CMS dedicado tiene sentido. Para un developer que quiere control total sobre su propio portfolio, construirlo es la decision correcta.
Comentarios
Inicia sesión para dejar un comentario
Sin comentarios aún. Sé el primero en compartir tu opinión.
Escrito por
Wilmar Garcia Valderrama
Líder Técnico de Desarrollo en QUIND y Fundador de WillDevp. Apasionado por la arquitectura limpia, microservicios y buenas prácticas de ingeniería.
