Notion CMS

Integración técnica detrás de /now. Un Server Component lee directamente la Notion API con ISR. Sin SDK externo, sin MDX intermediario.

Decisiones tomadas: marzo 2026 · Sesión de contenidos.

Conexión activa62ms
Página
Now
ID
0aa0a698a15f4842898a02f5ca517d84
Bloques
9
Última edición
20/4/2026, 5:41:00
Sincronizado
30/5/2026, 13:52:35
Revalida cada
3600s (1h)

Framework — Burner List de Jake Knapp

/now no es un CV actualizado ni una lista de tareas. Es una foto honesta del estado presente: lo que tiene el foco, lo que está en segundo plano, y el ruido que hay que gestionar. La estructura viene de The Burner List de Jake Knapp.

Una hoja dividida en dos columnas. Izquierda = fuego delantero. Derecha = fuego trasero.

Front burner — Un único proyecto. El más importante. Solo uno. Las tareas concretas que lo mueven en los próximos días. El espacio en blanco restante es enfoque visible.

Back burner — El segundo proyecto más importante. Consciente pero no protagonista.

Kitchen sink — Todo lo demás sin categorizar. El fregadero donde va el resto.

La regla más importante: la Burner List es desechable. Se rehace cuando cambia el front burner. El acto de rehacer te obliga a decidir qué sigue siendo importante y qué ya no.

Building in public

/now es el canal principal de la estrategia de construir en público. Cada vez que el front burner cambia, hay una historia que contar:

  1. Pablo actualiza /now en Notion
  2. Ese estado se convierte en contenido para redes
  3. El visitante que llega referenciado ve la prueba en vivo de que el sitio está en construcción
  4. Conecta con el posicionamiento: aprende haciendo, proceso visible, no portfolio muerto

Planteamiento

/now necesitaba actualizarse desde cualquier sitio, sin tocar el código. La solución más simple: Notion como CMS. Este experimento documenta cómo se construyó esa integración en fases, qué decisiones se tomaron en cada momento y por qué.

Qué es /now

El "frontdesk" de pablobellver.com. La página que responde: ¿qué está pasando ahora mismo? No es un CV actualizado. No es una lista de tareas. Es una foto del presente — lo que tiene el foco, lo que está en segundo plano, y el ruido que hay que gestionar. Pública porque esa transparencia es parte del proceso.

Fuente de datos

Página: Now — ID 0aa0a698-a15f-4842-898a-02f5ca517d84

Una página simple a nivel workspace en Notion (no hija de ningún otro documento, no wiki, no database). Contiene la Burner List directamente como bloques: headings + callouts. Pablo la edita libremente. Sirve como CMS único de /now.

Flujo buscado

Sería el flujo ideal pero no tiene porque ser el inicial para empezar con el experimento.

Pablo edita la página de Notion
        ↓
Webhook de Notion detecta el cambio
        ↓
Script convierte el contenido a content/now.mdx
        ↓
Vercel detecta el cambio en el repo y lanza rebuild
        ↓
/now actualizado en producción

Vamos a realizar varias fases. Empezaremos por una sincronización cada cierto tiempo. Luego añadiremos script manual para leer la página de Notion vía API y escribe el content/now.mdx → commit + push → Vercel rebuild. Sin infraestructura de webhooks, sin complejidad extra.

Por último y si la frecuencia de actualización lo justifica puedo añadir el webhook después.

V1 — ISR con Notion y Vercel

Estado: ✅ Implementada

Lectura directa desde Notion vía API con ISR — Incremental Static Regeneration (revalidación cada hora). Sin script manual, sin MDX intermediario. El Server Component lee la página en build time y Next.js la regenera automáticamente.

Arquitectura

Notion API → lib/notion.ts (fetch + parse) → Server Component → /now

Variables de entorno necesarias:

NOTION_TOKEN_NOW=ntn_...           # Integration token
NOTION_NOW_PAGE_ID=0aa0a698-...    # ID de la página Now

Las variables van en Project Settings de Vercel, no en Team Settings. Las de equipo no se inyectan en los builds de proyectos individuales.

El flujo

Pablo edita la página de Notion
        ↓
Notion API devuelve los bloques
        ↓
fetchBlocks() los lee con fetch nativo (ISR: revalidate 3600s)
        ↓
Next.js sirve HTML pre-renderizado
        ↓
/now actualizado en producción (máx. 1h de latencia)

El código

// lib/notion.ts — fetch recursivo (bloques e hijos)
async function fetchBlocks(blockId: string, revalidate: number) {
  const res = await fetch(
    `https://api.notion.com/v1/blocks/${blockId}/children`,
    {
      headers: { Authorization: `Bearer ${process.env.NOTION_TOKEN_NOW}` },
      next: { revalidate }, // ← ISR automático
    }
  )
  const data = await res.json()
  const blocks = data.results ?? []

  for (const block of blocks) {
    if (block.has_children) {
      block.children = await fetchBlocks(block.id, revalidate)
    }
  }

  return blocks
}

// app/now/page.tsx
export const revalidate = 3600

export default async function NowPage() {
  const data = await getNowPage() // server-side, cacheado
  return <BlocksRenderer groups={groupBlocks(data.blocks)} />
}

Incidencias resueltas

Callouts sin contenido interior

La Notion API devuelve callouts con has_children: true pero fetchBlocks solo leía el primer nivel. El renderer pintaba el rich_text del bloque raíz —vacío— ignorando los bloques hijos. Fix: fetchBlocks ahora es recursiva. Si un bloque tiene has_children: true, fetchea sus hijos y los asigna a block.children.

H3 dentro de callouts

Los callouts con un heading_3 como primer hijo no lo mostraban como título — el H3 quedaba perdido en el cuerpo. Fix: si el primer hijo es heading_3, se extrae y se renderiza como título del callout. El resto de hijos pasa a BlocksRenderer normalmente.

Lecciones aprendidas

  • Las variables de entorno tienen scope y no es obvio. Vercel distingue entre variables de equipo y variables de proyecto. El error no te dice dónde está el problema — simplemente la variable no existe. Cuesta tiempo real hasta que lo ves.
  • Nombrar bien desde el principio tiene coste cero. NOTION_TOKEN_NOW en lugar de NOTION_TOKEN genérico. Evita ambigüedad cuando haya más integraciones y hace explícita la separación entre servicios.
  • Las APIs tienen niveles de profundidad que no se ven hasta que algo falla. La Notion API devuelve bloques con has_children: true pero no incluye los hijos en la misma llamada. El bug no es de lógica sino de arquitectura de la llamada.
  • Verificar en producción antes de dar algo por hecho. El token funcionaba desde el paso 1 pero no había forma de saberlo hasta que el error cambió. Cada error resuelto revela el siguiente. El flujo correcto: configurar → desplegar → verificar → siguiente paso.
  • La complejidad técnica se prioriza por frecuencia de uso, no por elegancia. ISR cada hora cubre el caso de uso real de /now. Añadir webhooks desde el día uno habría sido sobreingenería. La decisión correcta: hacer funcionar lo mínimo necesario, documentar la evolución posible, revisarlo cuando el uso lo justifique.

→ V1 funcionó. La integración estaba en producción, el contenido se leía desde Notion y la web se actualizaba automáticamente cada hora. El siguiente problema: editar en Notion y tener que esperar hasta 60 minutos para ver el cambio publicado dejaba margen claro de mejora.

Next step — Publicar cambios de forma inmediata

ISR revalida automáticamente cada hora. Para el uso habitual de /now es suficiente. Pero cuando se necesita publicar un cambio de forma inmediata hay dos caminos posibles.

Opción AOpción B
MecanismoDeploy Hook + script npmWebhook de Notion
AutomatizaciónManual — tú lanzas el scriptAutomática al guardar en Notion
ComplejidadBajaAlta
VersiónVersión 2.0Versión 3.0

→ Se elige Opción A para Versión 2. Coste de implementación mínimo, valor inmediato. No requiere infraestructura adicional ni configuración en el lado de Notion. La Opción B se reserva para cuando el volumen de cambios justifique la automatización completa.

V2 — Publicación bajo demanda

Estado: ✅ Implementada

Deploy Hook de Vercel + script npm. Editas en Notion, ejecutas un comando, Vercel lanza un redeploy inmediato sin esperar el ciclo de ISR.

npm run sync-now --workspace=apps/web

Implementación:

  • apps/web/package.json → script sync-now que hace POST a $VERCEL_DEPLOY_HOOK_NOW
  • apps/web/.env.local → variable VERCEL_DEPLOY_HOOK_NOW con la URL del hook (nunca va al repo)
  • .vscode/tasks.json → task "Publicar /now" para lanzar con Cmd+Shift+P desde VS Code sin abrir el terminal

Crear el Deploy Hook en Vercel: Proyecto → Settings → Git → Deploy Hooks → nombre sync-now, rama main.

Mejoras visuales (28 mar 2026)

Casos de uso resueltos

  • Front/Back burners: Título destacado a la izquierda, acción visible sin scroll
  • Kitchen Sink: Sin título = bloque único, rendering correcto
  • Mobile: Contenido legible en pantallas pequeñas (1 columna)
  • Ultrawide: Contenido no se ve aislado (proporciones optimizadas)
  • Temas: Mantiene consistencia visual en todos los temas

Soporte de nuevos tipos de bloque

Nuevos renderers añadidos en lib/notion.ts y app/now/page.tsx.

  • Toggle — renderizado con el elemento nativo <details>/<summary>. Sin JavaScript adicional — el navegador gestiona el estado abierto/cerrado. Los hijos se renderizan con BlocksRenderer, lo que permite anidamiento ilimitado.
  • Child page — cuando la página de Notion tiene páginas hijas enlazadas como bloque, se renderiza como referencia no navegable: icono + título.

Callout responsive

Se modifica el styles/now.css:

  • El callout pasó de display: flex a display: grid para permitir layout de dos columnas sin romper en mobile.
  • Grid se adapta según viewport (5 media queries desde mobile → ultrawide)
  • Nuevas clases: .notion-callout-title y .notion-callout-content.
  • Estilos para toggle y child_page incluidos.

V3 — Actualización con webhook

Estado: 🚧 Pendiente

Notion detecta el cambio al guardar → llama al Deploy Hook automáticamente → Vercel redeploy. Sin intervención manual. Más complejidad de configuración que V2 pero publicación completamente transparente.

Ver la página /now generada con esto →