Este sitio no es una plantilla ni un WordPress con tema. Es una sola aplicación Next.js con Payload CMS dentro, una base de datos en Turso y las imágenes en Cloudflare R2. Lo armamos así porque es la forma de trabajar que ofrecemos a clientes, y preferimos contarla con lo que de verdad nos pasó que con un diagrama bonito.
Una sola aplicación
Next.js 16 con App Router y regeneración incremental para lo público, y Payload 3 instalado en la misma app: el panel vive en /admin y no hay un servidor de CMS aparte. Eso significa un solo repositorio, un solo despliegue en Vercel y tipos de TypeScript compartidos entre el contenido y las páginas. Si cambiamos un campo en una colección, el compilador nos avisa dónde se rompe.
Las consultas no pasan por HTTP. Las páginas usan la Local API de Payload, que es una llamada a función dentro del mismo proceso. Es más rápido y nos ahorra montar una API que solo consumiríamos nosotros.
Por qué elegimos Payload y no Strapi o Directus lo contamos en Payload CMS vs Strapi vs Directus en 2026, y la comparación con WordPress está en WordPress headless vs Payload CMS.
Qué va en código y qué en el CMS
No todo está en el panel, y es a propósito.
- En código: la home, las páginas de servicios, el retainer, contacto y los textos legales. Cambian poco y queremos que pasen por Git y revisión.
- En Payload: el blog, los proyectos, los menús, los datos del sitio, los formularios y los redirects. Es lo que se edita seguido.
El sitio está en español y en inglés. Las URLs salen de un único archivo de rutas y nunca se escriben a mano en los componentes, porque una URL mal armada en un sitio con dos idiomas se nota tarde y cuesta posiciones.
La base de datos: SQLite en local, Turso en producción
En desarrollo usamos un archivo SQLite. En producción, Turso. El código es el mismo adaptador de Payload en los dos casos; solo cambian dos variables de entorno:
db: sqliteAdapter({
client: {
url: process.env.DATABASE_URL ?? 'file:./data/payload.db',
authToken: process.env.DATABASE_AUTH_TOKEN,
},
}),DATABASE_URL apunta a file:./data/payload.db en local y a libsql://… en producción. En desarrollo el adaptador sincroniza el esquema solo; en producción únicamente corren migraciones, y el script de build las ejecuta antes de compilar: payload migrate && next build.
Turso no es la única opción ni siempre la mejor. Lo explicamos con calma en los artículos de esta serie sobre Turso y sobre qué bases de datos soporta Payload.
Las imágenes: Cloudflare R2
Las imágenes no se guardan en el servidor. Payload las sube a un bucket compatible con S3 y, en producción, ese bucket es Cloudflare R2 con dominio propio. Usamos el plugin oficial de almacenamiento S3 y, en local, MinIO en un contenedor Docker, que habla el mismo protocolo. Para el código es indiferente.
Cuando hay un dominio público configurado, el navegador pide la imagen directo al bucket y Payload no interviene. Sin ese dominio, que es lo que pasa en local, las sirve el propio Payload.
Cloudflare: lo que usamos y lo que no
Conviene ser claros: de Cloudflare usamos DNS y R2. codegea.com no pasa por el proxy de Cloudflare ni tiene reglas de WAF encendidas. Cloudflare como firewall es otra historia que sí hemos montado en proyectos de clientes, y la contaremos aparte.
Detalles que nos gustan
- Live Preview: al editar un artículo se ve el resultado real mientras se escribe, usando el Draft Mode de Next.js.
- Versión Markdown de cada artículo: añadir
.mda la URL de un post o proyecto devuelve el texto limpio, pensado para agentes y herramientas de IA. - MCP: el CMS expone un servidor MCP con llaves de API, así que un asistente puede leer y crear contenido sin pasar por el panel.
- Redirects heredados: el sitio anterior tenía URLs indexadas en Google. Las conservamos idénticas y las que ya no existen responden 410 en vez de redirigir a la home, para que Google las olvide sin crear soft-404. Eso lo desarrollamos en cómo migrar tu web sin perder posiciones en Google.
Lo que nos falló
Hay cosas que no salieron a la primera y vale la pena anotarlas.
disk I/O erroral sembrar datos. SQLite local se comparte entre el servidor de desarrollo y el script de seed. Si el servidor está abierto y sembramos la misma base, falla. Ahora verificamos con una base y un build aparte.- Subidas que se perdían. Al crear imágenes en un script, la segunda subida del proceso no llegaba al bucket. El plugin S3 guarda estado en el
contextde la petición; reutilizarlo entre llamadas lo dejaba mal. La solución fue pasar uncontextnuevo en cada subida. - Textos que no existen en un idioma. Un artículo sin traducción no se anuncia en el otro idioma, en lugar de mostrar el texto en el idioma equivocado. Es menos contenido, pero es contenido correcto.
¿Esto es para tu proyecto?
Depende. Si tu sitio es un blog con pocos cambios, WordPress sigue siendo razonable. Si tienes dos idiomas, contenido estructurado, un equipo que usa TypeScript y quieres un solo despliegue, esta combinación se defiende. Si necesitas una base de datos con mucha escritura concurrente, Turso no es la primera opción que miraríamos.
Si quieres algo parecido, mira cómo trabajamos en desarrollo de software a medida.
Escrito por
Derian Pinto
Founder & Full Stack Developer
Fundador de Codegea. Desarrolla sitios, tiendas y plataformas a medida, e integra y da soporte a Arc XP para medios.