WordPress Headless + Astro – Recursos de la clase 05

WordPress Headless + Astro – Recursos de la clase 05

SofiDev

Angela Sofía Osorio

Tiempo de lectura 10 minutes

Fecha de publicación

Creando menú dinámico

Pasemos a lo realmente interesante de esta clase: ¿Cómo podemos crear nuestro menú desde WordPress?

Bueno, para esto vamos a necesitar una query sencilla la cual vamos a crear dentro de src/services/ y tendre el nombre de getMenu.js : src/services/ getMenu.js

La data que estamos consiguiendo mediante la query es el menú desde su ID, que en el caso wordpress es el nombre que le hemos puesto. En mi caso, yo llame ami menú ,«menu» (Que original🙉). La cosa es que de hecho puedes tener más de un menú en wordpress. lo que nos ayudará más adelante a crear por ejemplo otros navs para footer o sidebars.

Este será un video muy largo o tendrá dos partes, si vienes del futuro, ya lo tendras mas claro. Pero lo que haremos será ir abordando toda la lógica del menú gerárquico y después enfocarnos en los estilos.

Componente Header

El componente Header sera básicamente el componendte estático, el esqueleto que contendrá los componentes dinámicos, así que solo nos centraremos en hacer su caja externa. A pesar de que este proyecto se está realizando con Tailwind, para este componente utilizaré 100% CSS3. Siéntete libre de adaptarlo a tailwind. Pero creo más conveniente hacerlo así para obtener unas tranciciones mas finas y bonitas 🦝.

---

---
<header class="header ">
  <div class="header__container ">
    /*
     Aquí iran los demás compoentes como: 
     Logo, NavMenu, HamburgerButton
    */
  </div>
</header>
Astro

Ahora que tenemos la base de nuestro header vamos a crear dos componentes más, El primero será nuestro componente de LOGO el cual obtiene los datos del sitio de wordpress, el segúndo será nuestro menú de navegación, el cúal obtiene los datos de WordPress de cualquier menu que creemos en nuestro wordpress y el último es un simple botón un simple botón que hara la función de nuestro menú hamburgesa.

Empecemos entonces por crear nuestro logo dinámico, trayendo la data necesaria de nuestro sitio de WordPress. Esto tambien te puede ser útil para otras secciones dónde necesites mostrar el titulo del sitio. Vamos entonces a necesitar la siguiente Query:

//src/services/getSiteInfo
import { wpquery } from "@src/data/wordpress";

export const getLogo = async () => {
    try {
        const data = await wpquery({
            query: `
                query getLogo {
                    allSettings {
                        generalSettingsDescription
                        generalSettingsTitle
                    }
                    siteLogo {
                        altText
                        link
                    }
                    }
            `,
        });
        return data;

    } catch (error) {
        console.log(error);
    }
};
JavaScript

Luego vamos a destructurar sus datos aquí mismo:

//src/services/getSiteInfo

const data = await getLogo();
export const logoItems =  {
    siteLogo: data.siteLogo.link,
    siteTitle: data.allSettings.generalSettingsTitle,
    siteSlogan: data.allSettings.generalSettingsDescription
  }
JavaScript

Nos quedaría de la siguiente manera:

import { wpquery } from "@src/data/wordpress";

export const getLogo = async () => {
    try {
        const data = await wpquery({
            query: `
                query getLogo {
                    allSettings {
                        generalSettingsDescription
                        generalSettingsTitle
                    }
                    siteLogo {
                        altText
                        link
                    }
                    }
            `,
        });
        return data;

    } catch (error) {
        console.log(error);
    }
};

const data = await getLogo();
export const logoItems =  {
    siteLogo: data.siteLogo.link,
    siteTitle: data.allSettings.generalSettingsTitle,
    siteSlogan: data.allSettings.generalSettingsDescription
  }
JavaScript

Vamos a crear el componente del logo como un componente atómico.

Para ello importaremos logoItmes desde nuestra ruta de servicios `//src/services/getSiteInfo`:

---
//src/components/atoms/Logo.astro

import { logoItems} from "@src/services/getLogo";
const {siteLogo, siteTitle, siteSlogan} = logoItems

---
<a href="/" class="logo">
  <img
    class="logo__img"
    src={siteLogo}
    alt={`logo de ${siteTitle}`}
  />
  <div class="logo__container">
    <h5 class="logo__title">{siteTitle}</h5>
    <p class="logo__slogan">{siteSlogan}</p>
  </div>
</a>
Astro

Ya tenemos el logo con toda la info disponible. vamos a reservarlo para después incluirlo en el Header.

Creando El menú de navegación

Este es el componente más complejo de esta clase. Pero no te preocupes porque lo aboradaremos paso a paso, empecemos primero con la query necesaria para obtener la data del menú de WordPress — En el video de youtube te muestro como crear un menú en WordPress, pero es muy sencillo.

import { wpquery } from "@src/data/wordpress";
 
export const fetchMenuData = async (menuID) => {
  const data = await wpquery({
    query: `
            query GET_MENU_BY_NAME {
                menu(id:"${menuID}", idType: NAME) {
                    menuItems {
                    nodes {
                      id
                      parentId
                      path
                      label
                    }
                    }
                }
                }
            `,
  });

  return data;
};
JavaScript

Ahora que tenemos la query necesaria, vamos a crear el componente navMenu. Acá lo haremos paso a paso, en cada bloque xplicando el funcionamiento de cada uno:

---
//vamos a importar la data de nuestra query
import { fetchMenuData } from "@src/services/getMenu.js";


// Necesitaremos obtener la ruta actual para renderizar el Home, condicionalmente
const pagePathname = Astro.url.pathname;


// Ahora vamos a llamar nuestra función pasandole el nombre del menu que le pusimos en wordpress como params, (En mi caso el menú se llama , menu)
const menuData = await fetchMenuData("menu");


//Ahora vamos a validar si existen los items en el menu y los convertimos en Array
//Si los menu Items no existen, usaremos un array vacio como fallback (Tú puedes crear una array de objetos con algún menú estatico como fallback)
const menuItems = Array.isArray(menuData?.menu?.menuItems?.nodes)
  ? [...menuData.menu.menuItems.nodes]
  : [];

//Continuamos con los demás pasos en el siguiente bloque 🦝
---

JavaScript

Con esto ya vamos a mitad del camino. Ahora necesitamos crear una utilidad (Función), que convierta nuestro menu plano en una gerarquía de padres e hijos. Acá tienes la documentación oficial de graphQl por si quieres profundizar más en ello, pero te resumiré lo que haremos: La data contiene los elementos del menú como una lista plana. Pero notarás que cada elemento del menu contine un id y un parenId.

Cuando El elemento del menu sea un elemento hijo, notarás que el parentId contiene un valor tipo string. Este parentId, casualmente corresponde con otro menuItem en su id. es decir que este es hijo del segundo. A su vez puedes notar que el padre, en su campo parentId, el valor es null, lo cual quiere decir que no es hijo de nadie.

Es decir cada elemento del menu contiene id y parentId, la diferencia radica en que los padres tienen valor null en el parent.

Vamos entonces a crear una función dentro de la ruta src/utils/flatListToHierarchical.js

Te dejaré comentarios de javascript en elcódigo para ayudarte a comprender mejor su funcionamiento:

/**
 * Este archivo de utilidades contiene funciones para transformar una lista plana de items de menú
 * en una estructura jerárquica (en árbol) para crear menús anidados
 */

/**
 * Extrae la última parte de una URL para usarla como slug
 * Ejemplo: "https://example.com/blog/post-1" -> "post-1"
 * @param {string} path - La URL completa a procesar
 * @returns {string} El slug extraído
 */
function getSlug(path) {
  // Eliminar la barra diagonal final si está presente
  const urlWithoutTrailingSlash = path.endsWith("/") ? path.slice(0, -1) : path;
  // Dividir la URL en partes y obtener el último segmento
  const parts = urlWithoutTrailingSlash.split("/");
  return parts[parts.length - 1];
}

/**
 * Transforma un array plano de items en una estructura jerárquica de árbol
 * @param {Array} data - Array de items a transformar
 * @param {Object} options - Opciones de configuración
 * @param {string} options.idKey - El nombre de la propiedad para el ID del item (por defecto: "id")
 * @param {string} options.parentKey - El nombre de la propiedad para el ID del padre (por defecto: "parentId")
 * @param {string} options.childrenKey - El nombre de la propiedad para el array de hijos (por defecto: "children")
 * @returns {Array} Estructura de árbol de los datos
 */
 
const flatListToHierarchical = (
  data = [],
  { idKey = "id", parentKey = "parentId", childrenKey = "children" } = {}
) => {
  // Inicializar el array de árbol resultante
  const tree = [];
  // Objeto para almacenar relaciones padre-hijo
  const childrenOf = {};

  // Procesar cada item en los datos de entrada
  data.forEach((item) => {
    // Crear nuevo item con slug de la URL
    const newItem = { ...item, slug: getSlug(item.path) };

    // Extraer ID y parentId (valor por defecto 0 si no hay padre)
    const { [idKey]: id, [parentKey]: parentId = 0 } = newItem;

    // Inicializar array de hijos para este item
    childrenOf[id] = childrenOf[id] || [];
    newItem[childrenKey] = childrenOf[id];

    // Añadir item al array de hijos de su padre o a la raíz si no tiene padre
    parentId
      ? (childrenOf[parentId] = childrenOf[parentId] || []).push(newItem)
      : tree.push(newItem);
  });

  return tree;
};

export {
  flatListToHierarchical
}
JavaScript

Ahora que tenemos nuestra función lista, vamos a importarla dentro de neustro componente NavMenu :

---
import { fetchMenuData } from "@src/services/getMenu.js";
import { flatListToHierarchical } from "@utils/flatListToHierarchical";


const pagePathname = Astro.url.pathname;


const menuData = await fetchMenuData("menu");



const menuItems = Array.isArray(menuData?.menu?.menuItems?.nodes)
  ? [...menuData.menu.menuItems.nodes]
  : [];

// Convert flat menu list into a hierarchical structure (parent-child relationship)
const hierarchicalMenu = flatListToHierarchical(menuItems);

---
Astro

Ahora procedamos con la creación del componente:

---
import { fetchMenuData } from "@src/services/getMenu.js";
import { flatListToHierarchical } from "@utils/flatListToHierarchical";


const pagePathname = Astro.url.pathname;


const menuData = await fetchMenuData("menu");



const menuItems = Array.isArray(menuData?.menu?.menuItems?.nodes)
  ? [...menuData.menu.menuItems.nodes]
  : [];

// Convert flat menu list into a hierarchical structure (parent-child relationship)
const hierarchicalMenu = flatListToHierarchical(menuItems);

---

<!-- Navigation menu component -->
<nav class="header__menu">
  <ul class="header__list">
    {/* Este primer bloque, muestra el enlace de ir al home page, únicamente cuando estás en otra ruta que no sea el mismo home */}
    {
      pagePathname !== "/" && (
        <li class="header__item">
          <a
            class="header__link"
            href="/"
            data-astro-prefetch
            transition:name="home"
          >
            Home
          </a>
        </li>
      )
    }
   	//Resto del código...
</nav>
Astro

Vamos ahora a mapdear sobre hieriarchicalMenu para poder obtener los enlaces padre de neustro menú y luego haciendo uso de otra lista desordenada que estará anidada, vamos a crear el dropdown:

---
import { fetchMenuData } from "@src/services/getMenu.js";
import { flatListToHierarchical } from "@utils/flatListToHierarchical";




const pagePathname = Astro.url.pathname;


const menuData = await fetchMenuData("menu");



const menuItems = Array.isArray(menuData?.menu?.menuItems?.nodes)
  ? [...menuData.menu.menuItems.nodes]
  : [];

// Convert flat menu list into a hierarchical structure (parent-child relationship)
const hierarchicalMenu = flatListToHierarchical(menuItems);

---


<nav class="header__menu">
  <ul class="header__list">
    {/*Muestra el enlace de ir al Home, solamente cuando se está en una ruta diferente de el mismo */}
    {
      pagePathname !== "/" && (
        <li class="header__item">
          <a
            class="header__link"
            href="/"
            data-astro-prefetch
            transition:name="home"
          >
            Home
          </a>
        </li>
      )
    }
    {/* Mapea a través de los menuItems y crea los links de naveagación*/}
    {
      hierarchicalMenu.map((item) => (
        <li class="header__item">
          {/* Acá salen se inyectarán los elementos padre del menú*/}
          <a
            class="header__link"
            href={`/categories/${item.slug}`}
            data-astro-prefetch
          >
            {item.label}
          </a>
          {/* Si el Item tiene un hijo, entonces se crea el dropdown */}
          {item.children.length > 0 && (
            <>
              <iconify-icon class="dropdown__icon" icon="gridicons:dropdown" />
              <ul class="dropdown">
                {/* Mapeamos a través de los items hijos */}
                {item.children.map((child) => (
                  <li class="dropdown__item">
                    <a
                      class="dropdown__link"
                      href={`/categories/${child.slug}`}
                      data-astro-prefetch
                    >
                      {child.label}
                    </a>
                  </li>
                ))}
              </ul>
            </>
          )}
        </li>
      ))
    }
  </ul>
</nav>
Astro

Estilos para nuestro Header

A©a te dejo la hoja de estilos para darle vida y color a tu header. recuerda que es solo la forma en que yo lo quise estilar, síentete libre de modificarlo y personalizarlo a tu gusto.

.header {
  --text-gradient: linear-gradient(to right, #33ff76 0%, #58ecff 100%);
  --ufoGreen: #33ff76;

  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  position: fixed;
  z-index: 1000;
  padding: 1rem;
}

.header__container {
  backdrop-filter: blur(17px);
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  min-height: 8dvh;
  max-width: 1200px;
  background: black;
  background: rgba(0, 0, 0, 0.767);
  border-radius: 16px;
  padding: 1rem 2rem;
  z-index: 50;
}

.header__menu {
  height: 100%;
  display: flex;
  justify-content: flex-start;
  align-items: center;
}

.header__menu .header__list {
  display: flex;
  align-items: center;
}

.header__menu .header__list .header__icons {
  display: none;
}

.header__list > *:not(a) {
  display: flex;
  margin-right: 1.5rem;
  list-style: none;
}

.header__link {
  color: aqua;
  font-weight: bold;
  position: relative;
}

.header__link:hover {
  background: var(--text-gradient);
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.header__link::after {
  position: absolute;
  top: 25px;
  left: 0;
  content: "";
  width: 100%;
  transform: scale(0);
  transform-origin: 1;
  height: 2px;
  transition: 0.35s transform;
  background: var(--ufoGreen);
}

.header__link:hover::after {
  transform: scale(1);
}

.header__item {
  position: relative;
  margin-right: 1rem;
  display: flex;
  align-items: center;
}

.header__item iconify-icon {
  color: aqua;
}

.header__item::after {
  content: "";
  position: absolute;
  display: none;
  width: 100%;
  height: 32px;
  top: 25px;
}

.header__item:hover::after {
  display: block;
}

/* Media queries */
@media (max-width: 768px) {
  .header__menu .header__list .header__icons {
    display: block;
  }

  .header__list > *:not(a) {
    margin-bottom: 2rem;
  }

  .header__list > *:not(a) .header__link {
    font-size: 2rem;
  }

  .header__container {
    justify-content: space-between;
    align-self: center;
    height: auto;
    min-width: 24rem;
    padding: 0.8rem 1rem;
  }

  .header__menu {
    transition: transform 0.6s ease;
    position: absolute;
    transform: translateX(100%);
    right: -14px;
    top: -10px;
    width: 103vw;
    height: 100dvh;
    padding-left: 3rem;
    background: black;
  }

  .header__list {
    width: 100%;
    height: 100dvh;
    flex-direction: column;
    justify-content: flex-start;
    align-items: flex-start;
    padding-top: 14rem;
  }

  .header__menu.is-active {
    transform: translateX(0%);
    border-radius: 14px 0 0 14px;
  }

  .dropdown__icon {
    font-size: 3.4rem;
    margin-left: 1rem;
  }
}

/* Dropdown styles */
.header__menu .header__list .dropdown {
  position: absolute;
  left: 0;
  width: 100%;
  background-color: #030405;
  z-index: 50;
  padding: 10px;
  border-radius: 8px;
  z-index: -1;
  opacity: 0;
  color: white;
}

.dropdown__item {
  width: 100%;
  margin: 0;
  padding: 0;
}

.dropdown__link {
  text-transform: capitalize;
  text-align: center;
  padding: 5px 0px;
  width: 100%;
  transition: background-color 0.3s ease-in-out;
  color: white;
  border-radius: 4px;
}

.dropdown__link:hover {
  background-color: #383838;
}

.dropdown__icon {
  font-size: 2.5rem;
  transform: rotate(-90deg);
  transition: transform 0.4s ease-in-out;
}

.header__item:hover .dropdown {
  transition: all 0.6s ease;
  opacity: 1;
  display: block;
  transform: translateY(30px);
}

@starting-style {
  .header__item:hover .dropdown {
    opacity: 0;
    display: none;
    transform: translateY(0px);
  }
}

.header__item:hover .dropdown__icon {
  transform: rotate(0deg);
}

/* Dark theme */
.dark .header__link {
  color: aqua;
}

.dark .header__menu .header__list .dropdown a {
  color: aqua;
}

/* Additional dropdown styles */
.dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  display: none;
  background-color: #fff;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  list-style: none;
  padding: 0;
  margin: 0;
  min-width: 150px;
  z-index: 10;
  margin-top: 0.3rem;
}

.dropdown__item {
  padding: 0.5rem 1rem;
  white-space: nowrap;
}

.dropdown__link {
  text-decoration: none;
  color: inherit;
  display: block;
  width: 100%;
}

.header__item:hover .dropdown {
  display: block;
}
CSS

Creando menú hamburguesa

Ahora que esta lista la funcionalidad, vamos a agregar un menú hamburguesa para nuestra version mobile.

Para ello simplemente devemos de crear un componente atómico como este:

---
//src/components/atoms/hamburgerBtn/HamburgerBtn.astro

---
<button class="hamburger hamburger--collapse" type="button">
  <span class="hamburger-box">
    <span class="hamburger-inner"></span>
  </span>
</button>
Astro

Eso es todo. Ya sé, ya sé, es demasiado pequeño. Si quieres pónlo dentro del mismo header, pero yo prefiero no tener tanto código en un mismo componente como pergamino.

Vamos a crear su hoja de estilos en la misma carpeta del componente:

.hamburger {
  display: none;
}

@media screen (max-width:768px) {
  .hamburger {
    font: inherit;
    display: inline-block;
    overflow: visible;
    margin: 0;
    cursor: pointer;
    transition-timing-function: linear;
    transition-duration: 0.15s;
    transition-property: opacity, filter;
    text-transform: none;
    color: inherit;
    border: 0;
    background-color: transparent;
  }
  .hamburger-box {
    position: relative;
    display: inline-block;
    width: 40px;
    height: 24px;
  }
  .hamburger-inner {
    top: 50%;
    display: block;
    margin-top: -2px;
  }
  .hamburger-inner:after,
  .hamburger-inner:before {
    display: block;
    content: "";
  }

  .hamburger-inner,
  .hamburger-inner::before,
  .hamburger-inner::after {
    background-color: aqua;
  }
  .hamburger-inner:before {
    top: -10px;
  }
  .hamburger-inner:after {
    bottom: -10px;
  }

  .hamburger-inner,
  .hamburger-inner:after,
  .hamburger-inner:before {
    position: absolute;
    width: 40px;
    height: 4px;
    transition-timing-function: ease;
    transition-duration: 0.15s;
    transition-property: transform;
    border-radius: 4px;
  }

  .hamburger--collapse .hamburger-inner {
    top: auto;
    bottom: 0;
    transition-delay: 0.13s;
    transition-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
    transition-duration: 0.13s;
  }

  .hamburger--collapse .hamburger-inner:before {
    transition: top 0.12s cubic-bezier(0.33333, 0.66667, 0.66667, 1) 0.2s,
      transform 0.13s cubic-bezier(0.55, 0.055, 0.675, 0.19);
  }
  .hamburger--collapse .hamburger-inner:after {
    top: -20px;
    transition: top 0.2s cubic-bezier(0.33333, 0.66667, 0.66667, 1) 0.2s,
      opacity 0.1s linear;
  }
  /*animation*/

  .hamburger--collapse.is-active .hamburger-inner {
    transition-delay: 0.22s;
    transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
    transform: translate3d(0, -10px, 0) rotate(-45deg);
  }

  .hamburger--collapse.is-active .hamburger-inner:before {
    top: 0;
    transition: top 0.1s cubic-bezier(0.33333, 0, 0.66667, 0.33333) 0.16s,
      transform 0.13s cubic-bezier(0.215, 0.61, 0.355, 1) 0.25s;
    transform: rotate(-90deg);
  }

  .hamburger--collapse.is-active .hamburger-inner:after {
    top: 0;
    transition: top 0.2s cubic-bezier(0.33333, 0, 0.66667, 0.33333),
      opacity 0.1s linear 0.22s;
    opacity: 0;
  }
}
CSS

Entonces vamos a importarlo en nuestro componente Header

---
import Logo from "@atoms/Logo/Logo.astro";
import HamburgerButton from "@atoms/hamburgerBtn/HamburgerBtn.astro";
import NavMenu from "@molecules/Header/NavMenu/NavMenu.astro";
---

<script>
  import "@controllers/hamburger.controller";
</script>
<header class="header">
  <div class="header__container">
    <Logo />
    <NavMenu />
    <HamburgerButton />
  </div>
</header>
Astro

Con esto Ya tenemos nuestro header responsive y recibiendo el menú desde WordPress 🦝.

Recuerda que puedes ver el tutorial en Youtube, uniéndote como miembro del canal con nivel: Sofier(Videos VIP).


Nos vemos en la siguiente clase.