Guía para Mostrar Contenido de Hashnode en tu Página Web a través de su API

Introducción

Hashnode es una plataforma de blogging técnica popular entre desarrolladores, que ofrece una API GraphQL pública para acceder a sus datos. En este tutorial, aprenderás cómo consumir esta API para mostrar los posts de un usuario en tu propio sitio web, con un diseño atractivo y paginación funcional.

Estructura de proyecto

Para este proyecto creamos la siguiente estructura de ficheros:

/sitio-hashnode
├── index.html
├── /js/main.js

Tenemos un directorio sitio-hashnode y dentro sólo necesitamos un fichero index.html para la estructura de la página y un fichero /js/main.js desde el cuál haremos la llamada a la api de hashnode e inyectaremos los posts al sitio web.

Diseñar la interfaz con HTML y TailwindCSS

Abre el fichero index.html y añade el siguiente código:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hashnode API</title>
    <script src="/js/main.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body>
    <div class="container mx-auto max-w-[960px] py-8">
        <h1 class="text-center text-zinc-500 text-3xl font-bold">Hashnode API</h1>
        <div id="grid_articles" class="grid grid-cols-1 md:grid-cols-3 gap-4 p-4">
            <!-- Card 1 -->
            <div class="bg-white rounded-lg shadow-md overflow-hidden">
                <img src="https://fakeimg.pl/380x200" alt="Cover Image 1" class="w-full h-auto object-cover">
                <div class="p-4">
                    <h2 class="text-xl font-bold mb-2">Article Title 1</h2>
                    <p class="text-gray-600 text-sm mb-2">By John Doe</p>
                    <p class="text-gray-700">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
                </div>
            </div>

            <!-- Card 2 -->
            <div class="bg-white rounded-lg shadow-md overflow-hidden">
                <img src="https://fakeimg.pl/380x200" alt="Cover Image 2" class="w-full h-auto object-cover">
                <div class="p-4">
                    <h2 class="text-xl font-bold mb-2">Article Title 2</h2>
                    <p class="text-gray-600 text-sm mb-2">By Jane Smith</p>
                    <p class="text-gray-700">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
                </div>
            </div>

            <!-- Card 3 -->
            <div class="bg-white rounded-lg shadow-md overflow-hidden">
                <img src="https://fakeimg.pl/380x200" alt="Cover Image 3" class="w-full h-auto object-cover">
                <div class="p-4">
                    <h2 class="text-xl font-bold mb-2">Article Title 3</h2>
                    <p class="text-gray-600 text-sm mb-2">By Mike Johnson</p>
                    <p class="text-gray-700">Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
                </div>
            </div>
            <!-- Pagination -->
            <div class="col-span-full flex justify-center items-center gap-2 mt-8">
                <button class="px-4 py-2 bg-gray-200 rounded-lg hover:bg-gray-300 disabled:opacity-50" disabled>Previous</button>
                <span class="px-4 py-2 bg-blue-500 text-white rounded-lg">1</span>
                <button class="px-4 py-2 bg-gray-200 rounded-lg hover:bg-gray-300">2</button>
                <button class="px-4 py-2 bg-gray-200 rounded-lg hover:bg-gray-300">3</button>
                <button class="px-4 py-2 bg-gray-200 rounded-lg hover:bg-gray-300">Next</button>
            </div>
        </div>
    </div>
</body>
</html>

Estamos usando tailwindcss para los estilos gráficos. Este código crea un interfaz en el cuál añadimos un título, un grid de artículos y un componente paginador.

Lo que sigue es crear el script para inyectar de forma dinámica los artículos que obtendremos por medio de la API de hashnode y crear la funcionalidad para el paginador.

API Hashnode

En el Hashnode API Playground podemos probar cómo funciona la API de Hashnode:

La API utiliza GraphQL y estamos realizando la siguiente consulta:

query {
    user(username: "alegrecode") {
      posts(pageSize:3, page:1) {
              pageInfo {
            hasPreviousPage
            hasNextPage
            previousPage
            nextPage
          }
              totalDocuments
          nodes {
            author {
              username
            }
            title
            brief
            url
            coverImage {
              url
            }
          }
        }
      }
    }

A continuación vamos a realizar un desglose detallado de cada parte de la consulta:

  1. Selección del Usuario user(username: "alegrecode"): Selecciona el perfil de usuario, en este caso, con el nombre “alegrecode”, puedes reemplazar este nombre con tu perfil de usuario.

  2. Obtención de Publicaciones posts(pageSize:3, page:1): Recupera las publicaciones del usuario. Limita la consulta a 3 publicaciones por página (pageSize:3). Solicita la primer página de los resultados (page:1).

  3. Información de Paginación pageInfo { hasPreviousPage hasNextPage previousPage nextPage }: Esta información es importante para la correcta funcionalidad del componente paginador. hasPreviousPage es un booleano que indica si existe una página anterior. hasNextPage es un booleano que indica si hay más páginas disponibles. previousPage número de la página anterior. nextPage número de la página siguiente.

  4. Detalles de los documentos: totalDocuments número total de documentos, lo vamos a usar para saber cuantas páginas debe tener nuestro paginador. nodes arreglo que contiene los detalles de cada publicación.

  5. Campos de cada publicación: author { username } nombre de usuario del autor de la publicación. title título de la publicación. brief breve descripción o extracto del post. url enlace directo a la publicación. coverImage { url } URL de la imagen de portada.

Con la información que recuperamos con esta consulta vamos a crear la grilla de artículos y crear la navegación entre páginas.

A continuación vamos a ver como combinar la función fetch con esta consulta GraphQL.

Consumiendo la API de Hashnode

En el fichero main.js, implementamos la función principal que realiza la consulta a la API:

document.addEventListener("DOMContentLoaded", function () {
    let currentPage = 1;

    function fetchPosts(page) {
        const query = `query {
            user(username: "alegrecode") {
                posts(pageSize:3, page:${page}) {
                    pageInfo {
                        hasPreviousPage
                        hasNextPage
                        previousPage
                        nextPage
                    }
                    totalDocuments
                    nodes {
                        title
                        brief
                        url
                        coverImage {
                            url
                        }
                        author {
                            username 
                        }
                    }
                }
            }
        }`;

Hasta aquí, definimos una variable let currentPage = 1 con el cuál vamos a inicializar la carga de posts. Luego creamos la función fetchPosts(page) que se va a encargar de realizar la petición y de renderizar la respuesta en la página. La función recibe el parámetro page el cuál usamos en la consulta GraphQL para que la carga de la página sea dinámica. La consulta GraphQL lo guardamos en la variable const query .

A continuación, justo debajo de la consulta, definimos la función fetch que se encarga de realizar la llamada a la API:

fetch("https://gql.hashnode.com/", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Accept": "application/json",
            },
            body: JSON.stringify({
                query: query,
            })
        })

Este código realiza una solicitud POST a la API de Hashnode utilizando GraphQL. Mediante fetch realizamos una petición HTTP a la API de Hashnode. Especifica que la solicitud será de tipo POST, lo cual es necesario para enviar consultas GraphQL. Define el tipo de contenido de la solicitud como JSON, lo que indica que los datos enviados estarán en formato JSON. body contiene los datos que enviamos en la solicitud. JSON.stringify({ query: query }) convierte el objeto { query: query } en una cadena JSON. query es la consulta GraphQL que define qué datos queremos obtener de la API.

Continuamos con el procesamiento de la respuesta y el renderizado de los posts:

.then(response => response.json())
            .then(data => {
                const posts = data.data.user.posts.nodes;
                const pageInfo = data.data.user.posts.pageInfo;
                const totalDocs = data.data.user.posts.totalDocuments;
                const totalPages = Math.ceil(totalDocs / 3); // 3 is the pageSize
                const postContainer = document.getElementById("grid_articles");

                postContainer.innerHTML = posts.map(post => `
                    <a href="${post.url}" target="_blank">
                        <div class="bg-white rounded-lg shadow-md overflow-hidden">
                            <img src="${post.coverImage.url}" alt="Cover Image 1" class="w-full h-auto object-cover aspect-[40/21]">
                            <div class="p-4">
                                <h2 class="text-xl font-bold mb-2">${post.title}</h2>
                                <p class="text-gray-600 text-sm mb-2">${post.author.username}</p>
                                <p class="text-gray-700">${post.brief}</p>
                            </div>
                        </div>
                    </a>`
                ).join("");
    });
}

const posts contiene los datos de cada publicación, const pageInfo contiene los metadatos que vamos a utilizar para la paginación, const totalDocs contiene el número total de documentos, const totalPages calcula el número total de páginas, const postContainer accede al elemento contenedor de artículos.

Por medio de postContainer.innerHTML renderizamos los artículos, para esto utilizamos posts.map para recorrer cada documento y vamos creando cada tarjeta con los estilos.

Realizamos la llamada a la función fetchPosts(currentPage) pasándole como parámetro la primer página.

Hasta aquí el código que tenemos es el siguiente:

document.addEventListener("DOMContentLoaded", function () {
    let currentPage = 1;

    function fetchPosts(page) {
        const query = `query {
            user(username: "alegrecode") {
                posts(pageSize:3, page:${page}) {
                    pageInfo {
                        hasPreviousPage
                        hasNextPage
                        previousPage
                        nextPage
                    }
                    totalDocuments
                    nodes {
                        title
                        brief
                        url
                        coverImage {
                            url
                        }
                        author {
                            username 
                        }
                    }
                }
            }
        }`;

        fetch("https://gql.hashnode.com/", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Accept": "application/json",
            },
            body: JSON.stringify({
                query: query,
            })
        }).then(response => response.json())
            .then(data => {
                const posts = data.data.user.posts.nodes;
                const pageInfo = data.data.user.posts.pageInfo;
                const totalDocs = data.data.user.posts.totalDocuments;
                const totalPages = Math.ceil(totalDocs / 3); // 3 is the pageSize
                const postContainer = document.getElementById("grid_articles");

                postContainer.innerHTML = posts.map(post => `
                    <a href="${post.url}" target="_blank">
                        <div class="bg-white rounded-lg shadow-md overflow-hidden">
                            <img src="${post.coverImage.url}" alt="Cover Image 1" class="w-full h-auto object-cover aspect-[40/21]">
                            <div class="p-4">
                                <h2 class="text-xl font-bold mb-2">${post.title}</h2>
                                <p class="text-gray-600 text-sm mb-2">${post.author.username}</p>
                                <p class="text-gray-700">${post.brief}</p>
                            </div>
                        </div>
                    </a>`
                ).join("");
            });
    }

    // Initial load
    fetchPosts(currentPage);
});

Con este código alcanzamos la primer parte del objetivo del proyecto, que es la llamada a la API de Hashnode y renderizar el resultado:

A continuación debemos añadir la funcionalidad para la navegación entre páginas.

Añadir paginación

Justo después del renderizado de los posts, añadimos los controles de navegación como sigue:

const paginator = `
                    <div class="col-span-full flex justify-center items-center mt-6 gap-4">
                        <button 
                            class="px-4 py-2 bg-blue-500 text-white rounded ${!pageInfo.hasPreviousPage ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-blue-600'}"
                            ${!pageInfo.hasPreviousPage ? 'disabled' : ''}
                            onclick="changePage(${pageInfo.previousPage})">
                            Previous
                        </button>
                        ${Array.from({ length: totalPages }, (_, i) => i + 1)
                            .map(pageNum => `
                                <button 
                                    class="px-4 py-2 ${currentPage === pageNum ? 'bg-blue-500 text-white' : 'bg-gray-200'} rounded-lg hover:bg-gray-300 cursor-pointer"
                                    onclick="changePage(${pageNum})">
                                    ${pageNum}
                                </button>
                            `).join('')}
                        <button 
                            class="px-4 py-2 bg-blue-500 text-white rounded ${!pageInfo.hasNextPage ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-blue-600'}"
                            ${!pageInfo.hasNextPage ? 'disabled' : ''}
                            onclick="changePage(${pageInfo.nextPage})">
                            Next
                        </button>
                    </div>`;
                postContainer.insertAdjacentHTML('beforeend', paginator);

Este código crea el componente paginador y asocia al evento onclick de cada botón a una función que se encargará de actualizar el renderizado de artículos.

Al hace clic en el botón “Previous”, se ejecuta la función changePage y se le pasa como parámetro pageInfo.previousPage. Además tiene un atributo condicional ${!pageInfo.hasPreviousPage ? 'disabled' : ''} , que indica que si no existe página anterior, el botón se desactiva. El botón “Next” sigue la misma lógica, al hacer clic en el botón, se ejecuta la función changePage y pasa el parámetro pageInfo.nextPage. De forma similar al botón “Previous”, tiene un atributo condicional, ${!pageInfo.hasNextPage ? 'disabled' : ''} que indica que si no existe página siguiente, el botón se desactiva.

Los botones de páginas numeradas se genera por medio de un array. Array.from({ length: totalPages }, (_, i) => i + 1) crea un array con números secuenciales desde 1 hasta totalPages. Luego realiza un mapeo del array y transforma cada número de página (pageNum) en un elemento HTML (un botón). Finalmente asocia al evento clic de cada botón a la función changePage y pasa pageNum como parámetro.

A continuación el código postContainer.insertAdjacentHTML('beforeend', paginator); inserta el HTML generado para la paginación (paginator) dentro del contenedor de posts (postContainer), específicamente al final de su contenido existente. 'beforeend' indica antes de cerrar el elemento (como último hijo).

Ahora debemos crear la función changePage. Fuera de la función fetchPosts añade el siguiente código:

window.changePage = function(page) {
        currentPage = page;
        fetchPosts(page);
    }

Este código actualiza el número de página actual (currentPage), esto permite rastrear en qué página está el usuario, y vuelve a cargar los posts correspondientes a la nueva página mediante fetchPosts(page).

El código completo de main.js queda como sigue:

document.addEventListener("DOMContentLoaded", function () {
    let currentPage = 1;

    function fetchPosts(page) {
        const query = `query {
            user(username: "alegrecode") {
                posts(pageSize:3, page:${page}) {
                    pageInfo {
                        hasPreviousPage
                        hasNextPage
                        previousPage
                        nextPage
                    }
                    totalDocuments
                    nodes {
                        title
                        brief
                        url
                        coverImage {
                            url
                        }
                        author {
                            username 
                        }
                    }
                }
            }
        }`;

        fetch("https://gql.hashnode.com/", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Accept": "application/json",
            },
            body: JSON.stringify({
                query: query,
            })
        }).then(response => response.json())
            .then(data => {
                const posts = data.data.user.posts.nodes;
                const pageInfo = data.data.user.posts.pageInfo;
                const totalDocs = data.data.user.posts.totalDocuments;
                const totalPages = Math.ceil(totalDocs / 3); // 3 is the pageSize
                const postContainer = document.getElementById("grid_articles");

                postContainer.innerHTML = posts.map(post => `
                    <a href="${post.url}" target="_blank">
                        <div class="bg-white rounded-lg shadow-md overflow-hidden">
                            <img src="${post.coverImage.url}" alt="Cover Image 1" class="w-full h-auto object-cover aspect-[40/21]">
                            <div class="p-4">
                                <h2 class="text-xl font-bold mb-2">${post.title}</h2>
                                <p class="text-gray-600 text-sm mb-2">${post.author.username}</p>
                                <p class="text-gray-700">${post.brief}</p>
                            </div>
                        </div>
                    </a>`
                ).join("");

                // Add paginator
                const paginator = `
                    <div class="col-span-full flex justify-center items-center mt-6 gap-4">
                        <button 
                            class="px-4 py-2 bg-blue-500 text-white rounded ${!pageInfo.hasPreviousPage ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-blue-600'}"
                            ${!pageInfo.hasPreviousPage ? 'disabled' : ''}
                            onclick="changePage(${pageInfo.previousPage})">
                            Previous
                        </button>
                        ${Array.from({ length: totalPages }, (_, i) => i + 1)
                            .map(pageNum => `
                                <button 
                                    class="px-4 py-2 ${currentPage === pageNum ? 'bg-blue-500 text-white' : 'bg-gray-200'} rounded-lg hover:bg-gray-300 cursor-pointer"
                                    onclick="changePage(${pageNum})">
                                    ${pageNum}
                                </button>
                            `).join('')}
                        <button 
                            class="px-4 py-2 bg-blue-500 text-white rounded ${!pageInfo.hasNextPage ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-blue-600'}"
                            ${!pageInfo.hasNextPage ? 'disabled' : ''}
                            onclick="changePage(${pageInfo.nextPage})">
                            Next
                        </button>
                    </div>`;
                postContainer.insertAdjacentHTML('beforeend', paginator);
            });
    }

    // Global function to change page
    window.changePage = function(page) {
        currentPage = page;
        fetchPosts(page);
    }

    // Initial load
    fetchPosts(currentPage);
});

Ahora podemos renderizar los artículos y navegar entre las distintas páginas:

Conclusión

En este tutorial, aprendimos cómo utilizar la API de Hashnode para obtener y mostrar publicaciones de un usuario en nuestro sitio web. Cada paso nos permitió entender cómo interactuar con una API GraphQL y visualizar los datos de forma atractiva y funcional. Además vimos como crea una paginación para navegar entre páginas los resultados de la consulta.

Puedes encontrar el proyecto completo en mi repositorio de GitHub haciendo clic AQUÍ.

Espero que lo hayas encontrado entretenido, instructivo y claro. Si tienes alguna duda, puedes hacérmelo saber en los comentarios. Pronto estaré subiendo más tutoriales.

Nos vemos en la próxima. Saludos!👋😊

2
Subscribe to my newsletter

Read articles from Carlos Alberto Alegre directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Carlos Alberto Alegre
Carlos Alberto Alegre

Soy Carlos Alberto Alegre, analista programador y desarrollador web front-end y back-end. Me considero un autodidacta entusiasta y disfruto explorando y aprendiendo nuevas tecnologías para mantenerme al tanto de las últimas tendencias en diseño y desarrollo web.