Volver al blog
Imagen de cabecera para Automatización e Integración de Notion en mi Blog
Desarrollo Web 10 min de lectura

Automatización e Integración de Notion en mi Blog

Índice de Contenidos

Cualquier ingeniero o desarrollador que haya mantenido un blog técnico conoce perfectamente la fricción del proceso clásico: abres el editor de código, creas un archivo Markdown, rellenas un frontmatter estricto con metadatos, redactas el contenido, haces un commit, empujas los cambios al repositorio y esperas a que el servidor despliegue.

Para un artículo de investigación exhaustivo, este flujo es aceptable. Pero para ideas rápidas, tutoriales o actualizaciones de proyectos, esta barrera técnica acaba matando la consistencia. La creación de contenido debe estar desacoplada de la base de código.

En este artículo, voy a desgranar cómo he rediseñado la arquitectura de mi portafolio personal para utilizar Notion como un Headless CMS, conectándolo a un frontend estático construido con Astro y automatizando todo el ciclo de vida del despliegue (CI/CD) mediante Netlify y GitHub Actions.

El objetivo fue escribir en cualquier dispositivo, marcar una casilla, y dejar que los sistemas distribuidos hagan el resto.

1. Diseño de la Arquitectura del Sistema

Antes de escribir una sola línea de código, es fundamental definir el modelo de datos y el flujo de la información. Nuestro ecosistema se compone de cuatro nodos principales:

  1. La Capa de Datos: Actúa como base de datos relacional y editor de texto enriquecido.
  2. La Capa de Integración: El puente seguro que expone los bloques de Notion en formato JSON.
  3. La Capa de Presentación: El framework que, en tiempo de compilación (SSG), consume la API, parsea los bloques JSON a HTML/Markdown y genera los archivos estáticos.
  4. La Capa de Infraestructura: El servidor perimetral (CDN) y los orquestadores que disparan las regeneraciones de la web sin intervención manual.

Esta separación de responsabilidades garantiza que la web final no dependa de peticiones de red a la API de Notion durante la navegación del usuario, logrando tiempos de carga de milisegundos y un SEO perfecto.

2. Modelando la Base de Datos en Notion

El primer paso es preparar la estructura en Notion. No sirve cualquier página; necesitamos una Base de datos de página completa.

Propiedades Obligatorias

Definí un esquema estricto que nuestro tipado en TypeScript consumirá más adelante:

  • Title (title): El nombre del artículo.
  • Draft (****select o status****): Fundamental para el control de versiones. Yo utilizo un estado que cambia entre "Borrador" y "Publicado".
  • Date (date): Fecha de publicación para el orden cronológico.
  • Description (rich_text): Un resumen para las tarjetas del blog y la etiqueta meta description.
  • Category (select): Para agrupar los posts.

Configuración de la API y el Token Segregado

Para que una aplicación externa lea estos datos, Notion requiere una Integración Interna:

  1. En el panel de desarrolladores de Notion, creé una nueva integración llamada "Astro Blog Connection".
  2. Esto genera un Internal Integration Secret (nuestro TOKEN).
  3. El paso de permisos: Fui a la base de datos recién creada en Notion, busqué "Conexiones" y añadí mi nueva integración. Sin esto, la API devuelve un error HTTP 404 (object_not_found).

Para identificar la base de datos, extraje el ID de 32 caracteres de la URL. Al ser una base de datos de página completa, el ID de la página y el de la base de datos son idénticos.

3. Consumiendo la API desde Astro: Resolución de Conflictos

Con el entorno preparado, pasamos a la lógica en TypeScript dentro del proyecto de Astro. Instalé los paquetes necesarios: @notionhq/client (el SDK oficial) y notion-to-md (un parseador para convertir los bloques JSON a un string Markdown compatible con Astro).

El Problema de Interoperabilidad ESM / CommonJS en Vite

Durante el desarrollo local, me topé con un error fatal en el servidor de desarrollo de Astro: [ERROR] notion.databases.query is not a function

Este es un problema de los empaquetadores modernos (como Vite) al intentar compilar dependencias antiguas o mal exportadas en modo Server-Side Rendering. El SDK de Notion a veces no exporta correctamente sus clases con llaves {} para entornos ESM.

La Solución a nivel de módulo: En lugar de importar directamente la clase, forcé al module runner a resolver el paquete completo y luego desestructuré las clases necesarias.

// src/utils/notion.ts
import notionPkg from "@notionhq/client";
const { Client, isFullPage } = notionPkg;
import { NotionToMarkdown } from "notion-to-md";

// Inicializamos los clientes de forma segura
const notion = new Client({ auth: import.meta.env.NOTION_TOKEN });
const n2m = new NotionToMarkdown({ notionClient: notion });

Construyendo el Fetcher de Artículos

La consulta a la base de datos debe ser inteligente. No queremos descargar borradores ni consumir cuota de API innecesaria. Utilicé el método databases.query con un filtro anidado y una ordenación descendente.

export async function getPublishedPosts(): Promise<UnifiedPost[]> {
  const databaseId = import.meta.env.NOTION_DATABASE_ID;

  const response = await notion.databases.query({
    database_id: databaseId,
    filter: {
      property: "draft",
      status: {
        equals: "Publicado", // Filtro estricto de estado
      },
    },
    sorts: [
      { property: "date", direction: "descending" },
    ],
  });

  const posts: UnifiedPost[] = [];

  for (const page of response.results) {
    if (!isFullPage(page)) continue; // Type guard de Notion

    const titleProperty = page.properties.title;
    const titleText = titleProperty?.type === "title" 
      ? titleProperty.title[0]?.plain_text 
      : "Sin título";

    // Generación dinámica de Slugs limpios
    const generatedSlug = titleText
      .toLowerCase()
      .normalize("NFD")
      .replace(/[\u0300-\u036f]/g, "") // Elimina tildes
      .replace(/[^a-z0-9]+/g, "-")     // Reemplaza espacios
      .replace(/^-+|-+$/g, "");        // Limpia bordes

    // ... (extracción del resto de propiedades)

    posts.push({
      id: page.id,
      slug: generatedSlug,
      title: titleText,
      // ... mapeo de datos al tipo UnifiedPost
    });
  }
  return posts;
}

Un detalle arquitectónico importante aquí es la generación dinámica del Slug. En lugar de obligarme a escribir una URL a mano en Notion para cada post, el servidor de Astro lee el título, aplica una expresión regular para limpiar caracteres latinos y tildes, y genera una URL amigable para SEO (ej. "Cómo automatizar mi blog" -> como-automatizar-mi-blog).

Parseo de Bloques a Markdown

La segunda parte de este motor extrae el contenido de la página. Notion no guarda el texto como un bloque monolítico, sino como un árbol de nodos (párrafos, imágenes, código, listas). El paquete notion-to-md recorre este árbol y lo traduce a sintaxis Markdown pura.

export async function getPostContent(pageId: string): Promise<string> {
  const mdblocks = await n2m.pageToMarkdown(pageId);
  const mdString = n2m.toMarkdownString(mdblocks);
  return mdString.parent || "";
}

4. El Paradigma Estático y el Problema de la Sincronización

Llegados a este punto, la web funciona perfectamente en local. Al compilar el sitio (astro build), Astro llama a Notion, se descarga el JSON, lo convierte en Markdown, lo pasa por su motor de plantillas y escupe archivos .html.

Sin embargo, al subir el sitio a Netlify, nos enfrentamos a la naturaleza del _S_SG. Si voy a mi tablet ahora mismo y publico un nuevo artículo en Notion, la web no mostrará nada. Notion y Netlify no se hablan. Netlify simplemente está sirviendo los archivos HTML estáticos que generó en el último commit de GitHub.

Para que el artículo nuevo aparezca en internet, alguien tiene que ordenarle a los servidores de Netlify que borren la caché, ejecuten un npm run build interno, llamen de nuevo a Notion y desplieguen los nuevos archivos atómicamente.

Ese "alguien" seremos nosotros mediante automatización.

5. Integración Continua y Webhooks

Netlify provee una herramienta excepcional para esta casuística: los Build Hooks. Es básicamente un endpoint de API secreto que, cuando recibe una petición HTTP POST, arranca la maquinaria de compilación del servidor.

El desafío era: ¿Cómo disparamos ese webhook automáticamente sin entrar al panel de Netlify?

Programación Desacoplada mediante GitHub Actions (Cron)

Como ingeniero, prefiero mantener toda la infraestructura como código dentro del propio repositorio del proyecto. Por ello, la solución definitiva fue implementar un flujo CI/CD nativo mediante GitHub Actions basado en tareas cronometradas.

Esta arquitectura asume que las publicaciones no requieren una latencia de un milisegundo. Si programamos compilaciones recurrentes, simplificamos radicalmente el sistema.

Creamos el archivo en el repositorio: .github/workflows/netlify-sync.yml

name: Sincronización Automática Notion -> Netlify

on:
  schedule:
    # Expresión Cron: Se ejecuta todos los días a medianoche y a mediodía
    - cron: '0 0,12 * * *'
  workflow_dispatch: # Permite disparar la acción manualmente si hay prisa

jobs:
  trigger-build:
    name: Llamada al Webhook de Netlify
    runs-on: ubuntu-latest
    steps:
      - name: Ejecutar petición POST
        run: |
          curl -X POST -d "" "${{ secrets.NETLIFY_BUILD_HOOK }}"
        shell: bash

Consideraciones de Seguridad: Jamás se debe hardcodear la URL del webhook en un archivo YAML público. El ID del hook de Netlify se inyectó de forma encriptada en la configuración del repositorio como un GitHub Secret (NETLIFY_BUILD_HOOK), siendo referenciado por el flujo de trabajo en tiempo de ejecución de manera segura.

6. Formalizando el Ciclo de Vida del Software

En entornos de ingeniería profesionales, una modificación de infraestructura de este calibre no se sube con un simple commit que diga "arreglado el blog". Se trata de una característica (o un conjunto de reparaciones y mejoras de pipeline) que debe documentarse y etiquetarse bajo control de versiones.

Para concluir este despliegue, empaqueté todo el código modificado (notion.ts, la reestructuración de dependencias ESM de Vite y el nuevo flujo YAML de GitHub Actions) y tracé un tag en Git para generar la Release v1.3.1.

Las notas de la versión (Changelog) reflejaron estrictamente las modificaciones a nivel de capas:

  • Fix: Ajuste del cliente de Notion para corregir el entorno SSR en Vite, garantizando la extracción de propiedades sin errores de compilación (notion.databases.query).
  • Infraestructura: Implementación del pipeline CI/CD (netlify-sync.yml) para sincronización autónoma del contenido de Notion mediante llamadas HTTP programadas, estableciendo un flujo Zero Downtime.

Conclusión

El paradigma Headless (desacoplar el sistema de gestión de contenidos del renderizado web) es una de las prácticas más liberadoras en el desarrollo web moderno.

Al combinar la robustez de Astro para compilar HTML ultra-rápido, la versatilidad de la API de Notion para gestionar los modelos de datos, y la potencia de GitHub Actions para orquestar la sincronización, he construido un sistema altamente escalable.

Ya no es necesario tocar código, gestionar repositorios, ni sufrir interfaces pesadas de WordPress. Ahora, la gestión de un blog de ingeniería es tan simple como abrir Notion desde cualquier dispositivo, redactar, y dejar que las máquinas se encarguen del despliegue.

Referencias y Documentación Oficial

Para profundizar en la implementación técnica y los detalles de los sistemas orquestados en esta arquitectura, dejo a continuación los enlaces a la documentación oficial y los recursos clave utilizados:

Compartir:
Hermes

Hermes

Asistente de Pablo Aranda

¡Hola! Soy Hermes, el asistente virtual de Pablo. ¿En qué te puedo ayudar hoy?