Como migrar de Wordpress a Notion

User icon Crysfel VillaCalendar icon Mar 10, 2023Calendar iconprogramming,javascript

Como migrar de Wordpress a Notion

Comencé a usar Notion hace casi un año (en el 2021) para manejar mi contenido de Youtube, me ha gustado mucho el producto y la manera tan sencilla de organizar mis ideas, scripts, diario personal, etc. Así que decidí usarlo también para mi blog personal.

Uno de los retos fue lograr mostrar la información de Notion a mi página web, pero afortunadamente Notion cuenta con una API Rest que te da acceso a todo el contenido que tengas. En este tutorial voy a mostrar los pasos necesarios para que tú también puedas hacerlo si gustas.

Exportar el contenido actual

Durante años mi blog ha sido un Wordpress sencillo, así que ya tenía varios posts publicados y era necesario traerlos a Notion, lo cual no fue del todo sencillo porque lo intenté de varias maneras sin tener éxito.

Afortunadamente encontré un plugin para Wordpress que se llama Simple CSV/XLS Exporter. Este plugin nos permite exportar todos nuestros posts en formato CSV dando click a un botón.

Una vez que descargué mi contenido a mi local, importarlo en Notion es bastante sencillo. Solo basta ir a la colección donde lo quieres importar y dar click sobre la opción import que se encuentra en el menu superior derecho, se abre una ventana modal donde vienen varias opciones, entre ellas la opción de CSV.

Formateando el contenido

Algunos posts que importé tenían imágenes o porciones de código, el plugin exportador solamente me generó el CVS con HTML en el campo de contenido, por lo tanto algunos posts no se veían del todo bien. Una de las características de Notion es que permite usar markdown, así que usé una herramienta para convertir HTML a Markdown.

Sólo tenía unos pocos posts, de tener cientos habría automatizado el proceso, muy probablemente haciendo un script y usando el API de Notion para insertar cada registro.

Next.JS

Una vez listo el contenido ¡es hora de comenzar a programar! Para este proyecto decidí usar Next.js por las siguientes razones:

  • En primer lugar porque quería aprender a usar este framework.
  • Está basado en React, así que no tenía que aprender esa parte.
  • Permite generar sitios estáticos que se pueden deployar a Github Pages, esto es importante para el SEO, además de que no quería mantener un servidor web.
  • Es bastante popular y con buena documentación, importante para cuando es necesario buscar ayuda.
  • También consideré GatsbyJS y AstroJS. Ya he usado Gatsby, pero la verdad es que no tenía claro como integrar el API de Notion con GraphQL, además de que mis ganas por aprender Nextjs fueron más.

    AstroJS se ve bastante interesante, pero adicionalmente a aprender el framework, no usa React, así que tenía que aprender su propia manera de hacer componentes, y la verdad es que tampoco le quería dedicar mucho tiempo a este proyecto.

    Inicializando el proyecto

    Iniciarte con NextJS es bastante sencillo, yo decidí usar Typescript y solamente tuve que seguir la documentación para iniciar el proyecto.

    Siempre que es mi decisión, decido usar Tailwind CSS para la parte de los estilos. Integrar Tailwind con NextJS es bastante sencillo, solo me bastó con seguir la documentación y en cuestión de minutos tenía Tailwind funcionando correctamente.

    Integración con Notion

    Lo siguiente es sacar la información de Notion usando el API disponible, para esto es necesario crear una integración en tu cuenta de Notion. Yo le di acceso de solo lectura porque no voy a escribir nada, esta integración es para mi blog. Una vez que creaste la integración te van a dar un Secret Key, este lo vas a usar en todos tus requests para autenticarte y acceder a tu información.

    Una vez que tienes tu integración es necesario asignarla a una base de datos, Notion así les llama a las colecciones/folders que tienes para organizar tu información. Lo que tienes que hacer aquí es ir a la colección y en el menú superior derecho, hay una opción que se llama Connections, en esta sección hay una opción que dice Add Connection, te aparece un submenu con varias opciones, entre una de ellas estará la integración que creaste, selecciona esa y listo.

    Por último necesitar el ID de la base de datos. Este lo vas a usar en cada request que hagas al API. La documentación no dice nada al respecto de cómo obtener este ID, yo estuve un buen rato buscándolo y jalándome los pelos. Pero lo puedes sacar de la URL de la colección, por ejemplo, la URL de la colección de mi blog es la siguiente:

    https://www.notion.so/crysfel/4b751e1cdfe1471b97912bceee115156?v=38bb03c48f0a42c79b8c84ea4a7c2800

    Entonces, el ID de la base de datos, sería 4b751e1cdfe1471b97912bceee115156. Ese es el ID que vamos a necesitar más adelante para sacar todos los artículos publicados.

    Ya que tenemos el Secret Key y el Database ID, estamos listos para comenzar a llamar el API. Te recomiendo usar Postman para probar que tus peticiones funcionen bien.

    Leer todos los Post publicados

    En cada post, he creado una propiedad llamada Status, esta propiedad tiene varios valores, por ejemplo: Idea, Draft, Review y Publish. Tengo un lindo board donde voy moviendo las tarjetas en entre estas columnas.

    Ahora bien, quiero leer solamente aquellos posts que estén con Status igual a Publish. Además los quiere ordenados por lo últimos que he publicado.

    Aquí tuve un problema porque la documentación de Notion no es muy clara, tiene varios endpoints pero no vi por ningún lado como sacar justo la información que necesitaba, así que me puse a probar todos. Resulta que se debe usar el database query, y tiene sentido una vez que sabes que Notion le llama database a las colección, pero en ese momento no lo sabía y si me tardé un buen rato.

    Bueno, la petición para este ejemplo sería la siguiente.

    POST https://api.notion.com/v1/databases/[DATABASE_ID]/query
    
    Body Payload:
      {
        "page_size": 20,
        "filter": {
            "and": [
                {
                    "property": "Status",
                    "select": {
                        "equals": "publish"
                    }
                },
                {
                    "property": "Type",
                    "select": {
                        "equals": "post"
                    }
                }
            ]
        },
        "sorts": [
            {
                "property": "Date",
                "direction": "descending"
            }
        ]
    }

    Como se puede observar, se puede definir el número de posts que quieres traer por cada consulta, así como filtros. En mi caso tengo varios tipos de posts, proyectos, books, etc. Así que para el blog solamente quiere traer los de tipo post.

    No olvidar mandar el Secret Key en la cabecera Authorization del request, también es necesario mandar el Notion-Version como parte de los headers, de lo contrario vas a recibir un error. En la documentación viene un ejemplo.

    Esa petición trae todos los post con la mayoría de información, pero falta lo más importante, el contenido principal. Para traer el contenido tienes que hacer otra petición por cada post al siguiente endpoint.

    GET https://api.notion.com/v1/blocks/[POST_ID]/children

    No olvidar enviar las cabeceras de Authorization y Notion-Version. La respuesta de este endpoint será un JSON con todo el contenido, cada párrafo es un bloque que a su vez puede tener otros bloques, dependiendo como has formateado el contenido.

    Cachando la información en local

    El siguiente problema que me encontré fue como hacer para guardar la información en local, ya que la voy a usar en varios lugares y además solo la voy a usar al momento de generar todos los HTML estáticos, ya que no voy a usar un servidor web con Node, todo será HTML y CSS para que mi sitio vuele.

    Decidí hacer un script que consultara la API de notion, sacara toda la información y me guardara un archivo JSON en mi local, de esta manera al desarrollar solamente leería el archivo JSON desde Nextjs y listo, todo mi contenido estaría disponible y listo para ser usado.

    El script es bastante sencillo, solo hace una petición inicial a Notion para traer todos los posts publicados, luego itero esa lista para hacer una petición individual por cada post y traer el contenido principal.

    Para hacer las peticiones a Notion se puede usar cualquier cliente HTTP, pero yo decidí usar el SDK de notion, así que instale el paquete @notionhq/client. La petición petición inicial queda así:

    const Notion = require('@notionhq/client')
    
    const notion = new Notion.Client({ auth: process.env.NOTION_PK })
    
    const response = await notion.databases.query({
        database_id: process.env.NOTION_DB_ID,
        page_size: PAGE_SIZE,
        filter: {
          and: [
            {
              property: 'Status',
              select: {
                equals: 'publish'
              }
            },
            {
              property: 'Type',
              select: {
                equals: 'post'
              }
            }
          ]
        },
        sorts: [
          {
            property: 'Date',
            direction: 'descending'
          }
        ]
      })

    Una vez que responde y tengo todos los post disponibles, itero la lista y hago la siguiente petición por cada item, con esto me traigo el contenido principal de cada post.

    const postResponse = await notion.blocks.children.list({
      block_id: node.id,
    })
    
    const post = {
      id: node.id,
      title: node.properties.Content.title[0].plain_text,
      content: postResponse.results,
      createdAt: node.created_time,
      publishedAt: node.properties.Date.date.start,
      slug: node.properties.Slug?.rich_text[0].plain_text || node.id,
      permalink: node.properties.Permalink.url,
      tags: node.properties.Tags.multi_select.map(tag => tag.name.toLowerCase()),
      image, // <--- TODO: Download this image!!
    }

    Y listo, ya tengo todo la información. Simplemente creo un nuevo arreglo de posts con los fields que necesito y guardo el JSON en un archivo de texto para poder usarlo después.

    Descargar imágenes de Notion

    Otro de los problemas es que tengo definidas imágenes para cada post, esta imagen la selecciono directamente en Notion, ya sea subiendo una que yo tenga en mi disco duro o bien usando el widget de UnSplash.

    Estas imágenes son privadas, y el API de Notion te regresa la URL donde están almacenadas en Amazon S3. Pero, al ser privadas expiran en 30 minutos, así que es necesario descargarlas para que las puedas mostrar desde el servidor donde vas a deployar este sitio, en mi caso Github Pages.

    Descargar imágenes o archivos en Node, no es complicado, yo lo hice de la siguiente manera:

    const imageName = `${node.id}.${getImageFormat(node)}`
    await downloadImage(remoteImage, imageName)
    
    
    
    async function downloadImage(url, imageName) {
      const filename = `./public/images/original/${imageName}`
    
      // Only download files that don't exist, this is mainly for development only
      // when building for production, it will always be a clean instance in the CI
      if (!fs.existsSync(filename)) {
        const writer = fs.createWriteStream(filename);
    
        const response = await axios({
          url,
          method: 'GET',
          responseType: 'stream'
        });
    
        response.data.pipe(writer);
    
        return new Promise((resolve, reject) => {
          writer.on('finish', resolve);
          writer.on('error', reject);
        });
      }
    
      return Promise.resolve()
    }

    Como se puede observer descargo las imágenes en el folder público del proyecto, cuando se hace el build, todos los archivos en este folder serán parte del resultado final, así que es importante poner las imágenes aquí para que pueda ser servidas cuando se deploye este sitio a producción.

    Redimensionando las imágenes

    Otro problema que me encontré es que las imágenes que vienen de UnSplash estás muy grandes, esto hace que el sitio se ponga lento en la carga inicial. Para optimizar esto es necesario redimensionar las imágenes para crear versiones más pequeñas.

    const sharp = require('sharp');
    
    const sizes = [
      { name: 'tiny', width: 10 },
      { name: 'tns', width: 500 },
      { name: 'large', width: 1200 },
    ];
    const imageName = `${node.id}.${getImageFormat(node)}`
    await resizeImageToMultipleSizes(imageName, sizes)
    
    
    async function resizeImage(inputPath, outputPath, size) {
      // Read the input image file
      const image = sharp(inputPath);
      // Resize the image for the current size configuration
      const resizedImage = image.resize(size.width);
      // Save the resized image to a separate output file
      await resizedImage.toFile(outputPath);
    }
    
    async function resizeImageToMultipleSizes(imageName, sizes) {
      // Iterate over each size configuration and resize the image
      for (const size of sizes) {
        const inputPath = `./public/images/original/${imageName}`;
        const outputPath = `./public/images/${size.name}/${imageName}`;
        await resizeImage(inputPath, outputPath, size);
      }
    }

    Para redimensionar las imágenes, decidí usar una librería llamada sharp, es bastante rápido y las imágenes quedan con buena calidad.

    Una vez las imágenes ya estaban descargadas y redimensionadas, simplemente guardé la URL correcta en el JSON que tengo en mi local.

    const remoteImage = node.cover?.type === 'external'
    	? node.cover?.external?.url 
    	: node.cover?.file?.url
    
    if (remoteImage) {
      const sizes = [
        { name: 'tiny', width: 10 },
        { name: 'tns', width: 500 },
        { name: 'large', width: 1200 },
      ];
      const imageName = `${node.id}.${getImageFormat(node)}`
    
      await downloadImage(remoteImage, imageName)
      await resizeImageToMultipleSizes(imageName, sizes)
    
      image = {
        original: `/images/original/${imageName}`,
        large: `/images/large/${imageName}`,
        tns: `/images/tns/${imageName}`,
        tiny: `/images/tiny/${imageName}`,
      }
    }
    
    const post = {
      id: node.id,
      title: node.properties.Content.title[0].plain_text,
      content: postResponse.results,
      createdAt: node.created_time,
      publishedAt: node.properties.Date.date.start,
      slug: node.properties.Slug?.rich_text[0].plain_text || node.id,
      permalink: node.properties.Permalink.url,
      tags: node.properties.Tags.multi_select.map(tag => tag.name.toLowerCase()),
      image,
    }

    ¡Listo! Eso es toda la integración con Notion, ya tenemos en nuestro local los posts, así como las imágenes de cada uno.

    Generando la paginación

    Esto me costo entenderlo, de hecho le tuve que preguntar a mi amigo Manduks cual era la mejor manera de hacerlo, porque originalmente había pensado que un script me generara componentes en React para cada página, pero no me parecía del todo buena esa idea, aunque si funcionaba.

    Manduks me refirió a la función getStaticPaths, esta función te permite consultar una Rest API o CMS para sacar todo el contenido que necesites y generar todas las páginas que requieres. Esto era justo lo que necesitaba, así que en lugar de consultar Notion en cada request, porque es lento hacer todas esas peticiones y descargar las imágenes, además de que mi sitio será estático cuando se vaya a producción, decidí leer el archivo JSON que genere previamente.

    Aquí calcule el número de páginas necesarias y se regresan en los paths, luego Nextjs internamente va a generar todos los HTML necesarios al momento de hacer el build.

    const POST_PER_PAGE: number = parseInt(process.env.POST_PER_PAGE, 10)
    
    type Page = {
      params: {
        slug: string;
      }
    }
    
    export async function getStaticPaths() {
      const paths: Page[] = []
      const allPostsJson = await fs.readFile(`./public/data/posts.json`, 'utf-8');
      const allPosts = JSON.parse(allPostsJson);
      const totalPages: number = Math.ceil(allPosts.length / POST_PER_PAGE)
    
      for (let i = 0; i < totalPages; i++) {
        const page: Page = {
          params: {
            slug: `${i + 1}`,
          },
        }
        paths.push(page)
      }
    
      return {
        paths,
        fallback: false,
      };
    }

    Una vez que NextJS sabe cuántas páginas tiene que generar, lo siguiente es proveer la información que cada página tendrá. Para eso usamos el método getStaticProps de la siguiente manera.

    export async function getStaticProps(context) {
      const page: number = parseInt(context.params.slug, 10) || 1
    
      const allPostsJson = await fs.readFile(`./public/data/posts.json`, 'utf-8');
      const allPosts = JSON.parse(allPostsJson);
    
      const startIndex = (page - 1) * POST_PER_PAGE;
      const endIndex = startIndex + POST_PER_PAGE;
      const posts = allPosts.slice(startIndex, endIndex)
    
      return {
        props: {
          posts,
          params: {
            ...context.params,
            paginator: {
              total: Math.ceil(allPosts.length / POST_PER_PAGE),
              current: parseInt(context.params.slug, 10) || 1,
            }
          },
          tags,
        },
      }
    }

    Aquí lo importante es sacar el parámetro del objeto context, con eso podremos calcular la página actual y enviar los posts necesarios para esa página. Adicionalmente mando información para el pajinador.

    ¡Y listo! Ya con eso podremos usar la información adecuada, lo siguiente es usar react para renderizar le información, pero eso está fuera del scope de este tutorial, ya que hay cientos de miles de tutoriales al respecto.

    Deployar a producción

    Para deployar a producción basta tirar tres comandos en la terminal.

    $ yarn blog
    $ yarn build
    $ yarn export

    El primer comando es el script que hicimos al principio, ese que saca toda la información de Notion y descarga las imágenes a tu local, el segundo y tercero son scripts de NextJs para generar el sitio estático.

    En un futuro tutorial voy a mostrar como automatizar el deployment con Github Actions para deployar a Github Pages. De esa manera bastaría con disparar el deployment de manera automática cuando se haga un push a master, o bien manualmente desde la interfase de Github.

    ¿Que te pareció mi integración? No dudes en mandarme cualquier duda vía twitter.

    Happy coding!

    Te ayudo a mejorar al entrevistar, únete a mi lista de correo.

    Unirse

    Te mando historias y consejos para mejorar tu carrera como Ingeniero de Software, también hablo sobre finanzas personales e inversiones.

    Crysfel's Twitter accountCrysfel's Linkedin accountCrysfel's Youtube channel

    También estoy en Youtube

    Publico videos en Youtube de vez en cuando, suscríbete a mi canal.

    ©2023 ALL RIGHTS RESERVED CRYSFEL'S BLOG