# 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:

```plaintext
/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:

```xml
<!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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1743108647235/8a79ad07-d02e-4da6-b74f-1d8592b99a75.png align="center")

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](https://gql.hashnode.com/?source=hashnode-footer) podemos probar cómo funciona la API de Hashnode:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1743125703169/804fd237-4746-4b4f-8057-815d17f4b9a4.gif align="center")

La API utiliza GraphQL y estamos realizando la siguiente consulta:

```graphql
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:

```javascript
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:

```javascript
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:

```javascript
.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:

```javascript
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:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1743212298823/0ce11912-eb1e-46a7-8afe-3188dce584c7.png align="center")

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:

```javascript
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:

```javascript
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:

```javascript
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:  

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1743215415774/781ce750-8a69-481d-b757-3fee9b62cadb.gif align="center")

## 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Í**](https://github.com/AlegreCode/sitio-hashnode/tree/main).

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!👋😊
