31 jul 2023

Cómo utilizar Notion como gestor de contenidos en tu sitio web

desarrollo
howto
☕️☕️ (Léelo en 9 minutos)
Cómo utilizar Notion como gestor de contenidos en tu sitio web

Cuando me planteé crear este sitio web, me propuse una serie de requerimientos a tener en consideración antes de comenzar:

  • Debía ser muy rápido de desarrollar (máximo 2 semanas dedicando horas sueltas).
  • Debía ser fácil de mantener y gestionar (ni ficheros Markdown, ni CMS’s complejos).
  • Debía usar herramientas y servicios que no me supusieran costes recurrentes.
  • Debía huir de servicios que me impusieran limitaciones, en cuanto a forma, función o uso.

Con todo ello en mente, eché un vistazo rápido a las diferentes alternativas disponibles en el mercado (como Wordpress, Medium, Squarespace, Framer, Wix o Webflow) y a posibles headless CMS’s (como Ghost, Contentful, Strapi o Storyblok). Lamentablemente, no encontré ninguna que me encajara o, más bien, que me convenciera a decidirme usarla. Tras esto, tuve que acallar una voz en mi interior que me decía: “😈 vamos, hazlo, crea tu propio CMS”, ya que esto me habría supuesto incumplir el primer requerimiento y salía totalmente del alcance del objetivo de este proyecto. Fue entonces cuando me vino a la cabeza que Notion, una de las herramientas que suelo usar diariamente para documentar, gestionar y organizar mis ideas, dispone de API, y pensé: ¿podría usarla para esto 🤔?

Notion es una herramienta muy flexible y maleable, lo que me permitiría saltarme limitaciones en cuanto a forma. Además de esto, estoy muy acostumbrado a usarla en mi día a día y la manejo con bastante fluidez. Esto me permitiría mantener y gestionar el contenido de manera rápida y sencilla. De momento, no me requiere coste alguno y, hasta la fecha, tampoco me ha impuesto limitaciones a nada de lo que me he planteado hacer (y eso que tengo plantillas bastante locas 🤪) por lo que, voilà, parecía que había encontrado el match perfecto.

¡Ya tenía gestor de contenidos! 🥳 Tocaba analizar aquellos elementos que pudieran requerir de un mantenimiento periódico en base a los prototipos que había diseñado previamente:

Estos fueron: la línea de tiempo para la trayectoria académica y profesional, las noticias del blog y los proyectos. Además de estos, las tecnologías usadas en componentes como los items de la línea de tiempo o los proyectos también podrían encajar. Dicho esto, creé la siguiente estructura de páginas en Notion:

databasesPage.png

Cada una de ellas, asociada a una base de datos como la que se muestra a continuación:

timelineDatabase.png

Como podrás observar en la imagen superior, cada uno de los elementos del timeline de la trayectoria, a su vez, podría tener relación con la base de datos de tecnologías:

techsDatabase.png

Con la estructura de contenidos creada, tocaba analizar cómo podía acceder a estos datos y engancharlos a mi sitio web. He desarrollado mi sitio usando Sveltekit ❤️, pero esto podría ser fácilmente replicable con otras tecnologías como NextJS, Nuxt, Astro, etc.

Y ¿por dónde empezar? ✋ Espera… antes ponte cómod@, prepárate una buena taza de café ☕️ (ya que se viene tutorial completito) y, cuando estés list@, te espero en el siguiente enlace: https://developers.notion.com/

List@… La documentación es simple, intuitiva y bastante fácil de digerir. Además, dispone de bastantes ejemplos que cubren la gran mayoría de operaciones comunes (consultar bases de datos, filtrar, ordenar, acceder al detalle de páginas, etc.). Si tu sitio web utiliza Javascript, estás de suerte, ya que disponen de un SDK oficial bastante majo que podrás encontrar en https://github.com/makenotion/notion-sdk-js. Si usas otra tecnología, no te preocupes, también podrás hacer uso de su API o una sencilla consulta a Google, y seguro encontrarás SDKs desarrollados por la comunidad como el que he podido encontrar para PHP https://github.com/mariosimao/notion-sdk-php

Yo he usado su SDK para Javascript, el cual, para instalarlo:

npm install @notionhq/client

Tras esto, podemos comenzar creando el cliente que usaremos para interactuar con nuestros contenidos:

const { Client } = require("@notionhq/client")

const notion = new Client({
  auth: process.env.NOTION_TOKEN,
})

¿Autenticación? 😱 ¿Cómo lo harán estos de Notion? Muy simple: si estás loguead@ con tu cuenta, debes acceder a: https://www.notion.so/my-integrations y una vez allí, debes pulsar en “+ nueva integración” (yo ya tengo la mía creada, pero no te preocupes, te acompaño 😉):

integrations.png

Seguidamente, deberás otorgarle un nombre y un logotipo (opcional):

createIntegration.png

Verás que también aparece un campo extraño llamado tipo o type que, por defecto, aparece como Internal. Una integración interna está vinculada a un espacio de trabajo específico y solo los miembros del espacio de trabajo pueden utilizarla. Son creadas por los propietarios del espacio de trabajo y estos deben dar permisos a la integración para acceder a páginas o bases de datos específicas a través del portal de Notion (lo veremos en un momento). Las integraciones públicas están pensadas para que cualquier usuario de Notion pueda utilizarlas en cualquier espacio de trabajo (no es lo que necesito en mi caso). Independientemente de que esta opción aparezca deshabilitada en esta pantalla, podrás modificarla tras crear la integración.

Le daremos un nombre reconocible, agregaremos un bonito emoji 😜 y pulsaremos submit para acceder a la última pantalla, donde podremos revelar y copiar nuestro ansiado token de autenticación:

integrationToken.png

¡Listo! podemos volver raud@s a nuestro código para terminar de configurar nuestro cliente y ejecutar nuestra primera consulta 🥳. Comenzaremos listando los elementos de alguna de nuestras bases de datos creadas previamente. Para ello, deberemos antes dirigirnos de nuevo a la interfaz de Notion y acceder a alguna de nuestras bases de datos. En la URL, nos debería aparecer algo similar a lo siguiente:

databaseID.png

Copiaremos el fragmento que encontraremos destacado en amarillo, entre nuestro nombre de usuario y el primer parámetro V. Este es el ID de nuestra base de datos, por lo que, con este ID y ayudándonos de la documentación verás que, para listar los items de una base de datos, podemos usar la siguiente función:

const { Client } = require('@notionhq/client');

const notion = new Client({ auth: process.env.NOTION_API_KEY });

(async () => {
  const databaseId = '9peUASgyYZdWcY7yF2okE8ajJwUyRuju';
  const response = await notion.databases.query({
    database_id: databaseId,
    // Aquí podríamos agregar filtros y ordenaciones 😉
  });
  console.log(response);
})();

Ejecutamos nuestro código y ¡¡¡BOOOM!!! 💥

@notionhq/client warn: request fail {
  code: 'object_not_found',
  message: 'Could not find database with ID: efa677e0-3547-406d-a47d-b4c13803d79d. Make sure the relevant pages and databases are shared with your integration.'
}

¿Recuerdas que hace un momento te comentaba que, como usamos un token configurado como interno, debíamos otorgarle permisos para poder leer los recursos privados de nuestro espacio de trabajo? Pues esto es lo que viene a decirte este error. ¿Cómo hacerlo? Accederemos de nuevo a nuestra base de datos y, en la esquina superior derecha, pulsamos la siguiente secuencia:

integrationConnect.png

Tras conectar nuestra integración con el espacio de trabajo, ya podremos ejecutar nuestra consulta sin problemas:

{
    "object": "list",
    "results":
    [
        {
            "object": "page",
            "id": "f8837355-63b7-49eb-b70f-d138158f23a2",
            "created_time": "2023-07-21T23:44:00.000Z",
            "last_edited_time": "2023-07-26T17:02:00.000Z",
            "created_by":
            {
                "object": "user",
                "id": "470d597f-82e7-4648-9eba-1f6f795598db"
            },
            "last_edited_by":
            {
                "object": "user",
                "id": "470d597f-82e7-4648-9eba-1f6f795598db"
            },
            "cover": null,
            "icon": null,
            "parent":
            {
                "type": "database_id",
                "database_id": "aca64d67-354d-443d-880d-53b84d18577f"
            },
            "archived": false,
            "properties":
            {
                "company":
                {
                    "id": "%3DB%60%7C",
                    "type": "rich_text",
                    "rich_text":
                    [
                        {
                            "type": "text",
                            "text":
                            {
                                "content": "IEBS",
                                "link": null
                            },
                            "annotations":
                            {
                                "bold": false,
                                "italic": false,
                                "strikethrough": false,
                                "underline": false,
                                "code": false,
                                "color": "default"
                            },
                            "plain_text": "IEBS",
                            "href": null
                        }
                    ]
                },
                "end":
                {
                    "id": "D%3ALS",
                    "type": "date",
                    "date": null
                },
                "start":
                {
                    "id": "%5Dg_Y",
                    "type": "date",
                    "date":
                    {
                        "start": "2022-10-01",
                        "end": null,
                        "time_zone": null
                    }
                },
                "brand":
                {
                    "id": "e%7CsX",
                    "type": "files",
                    "files":
                    [
                        {
                            "name": "iebs.png",
                            "type": "file",
                            "file":
                            {
                                "url": "https://s3.us-west-2.amazonaws.com/secure.notion-static.com/d7698eb8-7467-4151-9baf-a207a996be06/iebs.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230729%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230729T210414Z&X-Amz-Expires=3600&X-Amz-Signature=d4417260aee7206a607376896bbb3aa99bf74640d82c1dc8c3837973f4f05c1c&X-Amz-SignedHeaders=host&x-id=GetObject",
                                "expiry_time": "2023-07-29T22:04:14.183Z"
                            }
                        }
                    ]
                },
                "type":
                {
                    "id": "jG~%60",
                    "type": "select",
                    "select":
                    {
                        "id": "3a599913-8a3e-4d40-b698-51b13afc7f80",
                        "name": "academic",
                        "color": "brown"
                    }
                },
                "description":
                {
                    "id": "or%5D%7D",
                    "type": "rich_text",
                    "rich_text":
                    [
                        {
                            "type": "text",
                            "text":
                            {
                                "content": "En este posgrado se estudia en detalle la programación de contratos inteligentes y el desarrollo de aplicaciones descentralizadas mediante el uso de Solidity y Truffle Suite. Se despliega en redes ERC-20 (como Ethereum, Polygon o BNB Chain). Se configuran, desarrollan y despliegan redes permisionadas mediante Hyperledger Fabric.",
                                "link": null
                            },
                            "annotations":
                            {
                                "bold": false,
                                "italic": false,
                                "strikethrough": false,
                                "underline": false,
                                "code": false,
                                "color": "default"
                            },
                            "plain_text": "En este posgrado se estudia en detalle la programación de contratos inteligentes y el desarrollo de aplicaciones descentralizadas mediante el uso de Solidity y Truffle Suite. Se despliega en redes ERC-20 (como Ethereum, Polygon o BNB Chain). Se configuran, desarrollan y despliegan redes permisionadas mediante Hyperledger Fabric.",
                            "href": null
                        }
                    ]
                },
                "techs":
                {
                    "id": "yITS",
                    "type": "relation",
                    "relation":
                    [
                        {
                            "id": "b0d96656-08d1-4682-8d9f-5b87b99a3035"
                        },
                        {
                            "id": "50896b54-bd93-46de-8766-babd099e7604"
                        },
                        {
                            "id": "f37b547d-b980-4419-9b3e-3300cb89247e"
                        },
                        {
                            "id": "052853d7-a200-41df-9c0a-7526ca59c89d"
                        }
                    ],
                    "has_more": false
                },
                "title":
                {
                    "id": "title",
                    "type": "title",
                    "title":
                    [
                        {
                            "type": "text",
                            "text":
                            {
                                "content": "Postgrado en Ingeniería y arquitectura Blockchain",
                                "link": null
                            },
                            "annotations":
                            {
                                "bold": false,
                                "italic": false,
                                "strikethrough": false,
                                "underline": false,
                                "code": false,
                                "color": "default"
                            },
                            "plain_text": "Postgrado en Ingeniería y arquitectura Blockchain",
                            "href": null
                        }
                    ]
                }
            },
            "url": "https://www.notion.so/Postgrado-en-Ingenier-a-y-arquitectura-Blockchain-f883735563b749ebb70fd138158f23a2",
            "public_url": null
        },
        ...
    ],
    "next_cursor": null,
    "has_more": false,
    "type": "page_or_database",
    "page_or_database":
    {}
}

Bueno, sin problemas… 🙄 con la primera consulta seguro que te habrás dado cuenta de un par de cosas que, como a mí, te habrán dejado con la mosca detrás de la oreja. La primera de ellas es que, si nos fijamos en las propiedades de cada uno de los items de nuestra base de datos, verás que el formato es bastante particular… Existen paquetes que podrían ayudarte a transformar estas verbosas respuestas, pero nada que, con la ayuda de una interfaz de Typescript y Github Copilot (o pacientemente a mano), no podamos solventar a través de una simple función que nos permita parsear los resultados del siguiente modo:

export const parseTimelineResult = (
  item: any
) => {
  return {
    title: item.properties.title.title[0].plain_text,
    description: item.properties.description.rich_text[0].plain_text,
    company: item.properties.company.rich_text[0].plain_text,
    brand: item.properties.brand.files[0].file.url,
    start: new Date(item.properties.start.date.start),
    end: item.properties.end?.date?.start
      ? new Date(item.properties.end?.date?.start)
      : null,
    type: item.properties.type.select.name,
    techs: item.properties.techs.relation.map((tech: any) => tech.name)
  } as TimelineItem;
};

Esta solución podría ayudarte para parsear los registros de una o varias bases de datos pero, cuando quieras hacerlo con el detalle de una página, te darás cuenta de que la cosa se complica un poco más. Para estos casos, sí que te recomendaría usar un paquete como https://github.com/souvikinator/notion-to-md. Este paquete te permitirá transformar el contenido de una página de Notion a formato Markdown para que, de este modo, puedas renderizarlo estilizado en tu sitio web.

Otra de las cosas que podrás observar, si prestas atención, es lo siguiente:

 "files": [
      {
          "name": "iebs.png",
          "type": "file",
          "file":
          {
              "url": "https://s3.us-west-2.amazonaws.com/secure.notion-static.com/d7698eb8-7467-4151-9baf-a207a996be06/iebs.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230729%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230729T210414Z&X-Amz-Expires=3600&X-Amz-Signature=d4417260aee7206a607376896bbb3aa99bf74640d82c1dc8c3837973f4f05c1c&X-Amz-SignedHeaders=host&x-id=GetObject",
              "expiry_time": "2023-07-29T22:04:14.183Z"
          }
      }
  ]

¿Ves esa fecha de expiración? Pues parece que, por motivos de seguridad, los ficheros que agregues a tus contenidos en Notion utilizan URL’s que expiran cada hora, por lo que, o bien actualizas la ruta de tus ficheros cuando esto ocurra, o bien los descargas y copias en algún directorio bajo tu control. En mi caso, he decidido hospedar una copia de estas imágenes en Cloudinary. De este modo, no tengo que lidiar con la expiración de URL’s. ¿Cómo? Cuando hago el parseo de los contenidos, al procesar los elementos de tipo imagen, subo previamente una copia a Cloudinary y utilizo su URL en lugar de la de Notion. Recuerda que esto también deberás hacerlo con los contenidos de tus páginas (aquellos que te recomendaba transformar a formato Markdown).

Cuando por fin parecía que todo había acabado, me topé con otra sorpresita 🥵… ¿Te has dado cuenta de lo que ha tardado la API en contestar? Mira lo que ha tardado mi petición:

responseTime.png

Estos tiempos de respuesta no son buenos para tu sitio web. ¿Cómo he resuelto este contratiempo? Para trabajar con los datos localmente en mi equipo, cacheo las respuestas usando listas y mapas en memoria. También podría copiarlos en ficheros, usar Redis o cualquier otro servicio similar, pero existe una alternativa mucho más sencilla. ¿Cuál? Renderizar el contenido de manera estática en tiempo de compilación (y sí, en Svelte se compila 🤪). Con Sveltekit, es tan sencillo como utilizar el adaptador para renderizado estático, pero esto no es exclusivo de Sveltekit, también podrás realizarlo con cualquier otro tipo de tecnología o framework.

Con esto parece que, ahora sí, ya lo tenemos todo. Tras transformar el contenido de todas las bases de datos a objetos JSON y el detalle de cada una de nuestras páginas a formato Markdown, solo nos quedaría pintar la información en nuestras plantillas (esto puede variar en función de la tecnología que estés utilizando para crear tu proyecto, por lo que no entraré en mayor detalle sobre esto).

¿Cuál ha sido el resultado final? Puedes encontrarlo disponible en el siguiente repositorio: https://github.com/medinamarquezp/medinamarquezp.com

Donde podrás revisar en detalle cómo he afrontado todos los temas tratados en este artículo ¿Es la mejor de las soluciones? Quizás no, pero es la que me ha servido para cumplir el reto de crear mi sitio en dos semanas y cumple con todas mis necesidades.

¿Te han quedado dudas al respecto? ¿Necesitas que te aclare algún punto en particular? ¿Tienes cualquier feedback o aportación que creas que me podría ayudar a mejorar mi proyecto? ¿O simplemente te apetece darme tu opinión? No dudes en hacerlo usando cualquiera de mis redes sociales o email. Estaré encantado de escucharte o de ayudarte en lo que necesites.

Por si eres curios@, Sveltekit y Notion han sido dos piezas clave en este proyecto, pero no son las únicas que he utilizado. Además de estas:

Ahora sí, creo que ya ha sido suficiente por hoy (reconozco que este artículo se me ha ido un poco de las manos en cuanto a extensión 🙃). En mi próximo post me gustaría contarte más detalles sobre substracking, el proyecto en el que ando trabajando y del cual espero poder publicar próximamente su roadmap. De nuevo, gracias por pasarte por aquí y espero verte de vuelta pronto 🖖.

© Pedro Medina Márquez 2024, 39°33'38"N 2°40'7"E ツ

Hecho con ❤️ usando y