Imagen destacada del post -Wordpress Headless: Autenticación con Next.js-
Última actualización:

Wordpress Headless: Autenticación con Next.js


¿Hola qué tal? En este post te mostraré como aprovechar el sistema de autenticación de Wordpress en nuestra aplicación de Next.js de una manera rápida y robusta. Este tutorial va dirigido ha desarrolladores con algo de experiencia o estudiantes que quieran ampliar sus conocimientos, por eso durante el transcurso del mismo supondré que sabes un poco o algo de estas tecnologías: Javascript, NodeJS y ReactJS.

¡Muy bien pongámonos por faena! 💪

Lo primero será instalar Wordpress en local, aquí te dejo tres opciones para hacerlo de más sencillo y gráfico a más bajo nivel con docker:

Una vez instalado Wordpress vamos a instalar y configurar los plugins necesarios para poder completar la autenticación:

El primero es JWT Authentication for WP-API que nos abilita dos endpoints en la api de wordpress. El primero y más importante es wp-json/jwt-auth/v1/token al que vamos a atacar para hacer la autenticación y que nos retorne el token de acceso o autorización para los endpoints protegidos. Y el segundo es /wp-json/jwt-auth/v1/token/validate donde podremos validar si los tokens están firmados correctamente o no han expirado. Para configurarlo correctamente hay que asegurarse de que el servidor acepta autorización en las cabeceras y si no pegar el siguiente código en el .htaccess:

RewriteEngine on
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]

También tenemos que definir una constante global en el archivo wp-config.php con la clave secreta para firmar nuestros tokens:

define('JWT_AUTH_SECRET_KEY', 'your-top-secret-key');

El segundo es Disable REST API que nos permitirá desde la configuración una vez activado proteger los endpoints y así comprobar nuestra autenticación con los tokens. Eso si acuerdate de habilitar los endpoints de autenticación del plugin anterior:

Configuracion endpoints API Rest


Muy bien una vez terminada la parte de Wordpress toca instalar Next.js y las librerías que vamos a utilizar. Pero antes de esto y a modo de recordatorio voy a suponer que tienes instalado node.js en tu sistema operativo. Si no es el caso te recomiendo que uses nvm para sistemas operativos tipo *NIX y nvm-windows ya lo puedes adivinar. Esto te va permitir instalar varias versiones en el sistema aparte de la que vamos a utilizar en este tutorial que es la 18 LTS.

Para empezar con la instalación vete a la consola y ejecuta el siguiente comando:

npx create-next-app@latest

A continuación vamos a responder a las preguntas de la siguiente manera:

What is your project named? wp-headless-nextjs
Would you like to use TypeScript? No
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like to use `src/` directory? No
Would you like to use App Router? (recommended) No
Would you like to customize the default import alias (@/*)? No

En el momento de realizar este tutorial con Next.js en la versión 13.* se puede elegir el tipo de declaración de rutas. La forma recomendada por la documentación son las rutas de aplicación o “App Router”, pero como esta forma es relativamente nueva y todavía la mayoría de los ejemplos y tutoriales están hechos con las rutas de páginas elegiremos esta opción lo que no quita que extendamos o modifiquemos esto en el futuro. Dicho esto procedamos a instalar las dependencias con este comando:

npm install @headlessui/react @heroicons/react @tailwindcss/forms next-auth

Las primeras tres librerías son componentes, plugins e iconos que utilizaremos con Taildwind, la última nos permitirá hacer la autenticación de manera rápida.

En el archivo de configuración de Tailwind tailwind.config.js añadimos la carga del plugin de formularios así:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      backgroundImage: {
        'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
        'gradient-conic':
          'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
      },
    },
  },
  plugins: [
    require('@tailwindcss/forms')
  ],
}

En el archivo de configuración de eslint .eslintrc.json añadimos las reglas de next/babel así:

{
  "extends": ["next/core-web-vitals", "next/babel"]
}

Añade el archivo .env.local y rellena las siguientes variables de entorno:

NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=mi-clave-secreta
WP_BACK_URL=http://localhost:8000

Luego algo no obligatorio pero sí recomendable es añadir a tu editor, en mi caso Visual Studio Code, soporte para formateo del código y vigía al tiempo de desarrollar. Para esto ya estamos utilizando eslint pero asegúrate de tener instalada su extensión para tu editor. Además yo utilizo dos herramientas más:

Prettier añadiendo el archivo prettier.config.js

// prettier.config.js, .prettierrc.js, prettier.config.cjs, or .prettierrc.cjs

/** @type {import("prettier").Config} */
const config = {
  tabWidth: 2,
  semi: true,
  singleQuote: true,
};

module.exports = config;

Y EditorConfig añadiendo el archivo .editorconfig

# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

[*.{js,jsx}]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

Como con eslint no te olvides de instalar sus extensiones oficiales para tu editor. Puedes seguir la evolución en el repositorio de los tutoriales.

Lo siguiente será añadir las vistas y páginas básicas para el tutorial las cuales serán auth/signin, dashboard y home que ya viene por defecto pero nosotros vamos a modificarla. Para esto estaremos utilizando los templates de la capa gratuita de TailwindUI. Para la home renombraremos el archivo index.js dentro de la carpeta pages a index.jsx y añadimos el código que encontramos aquí. Para el dashboard añadimos un archivo en la carpeta pages llamado dashboard.jsx y le añadimos el siguiente código. Por último creamos una carpeta dentro de la carpeta pages llamada auth y luego añadimos dentro un archivo llamado signin.jsx, luego vamos a encontrar el código aquí.

Ya estamos listos para levantar nuestra aplicación en desarrollo ejecuta el siguiente comando:

npm run dev

Si todo ha salido bien y puedes acceder a todas las páginas lo siguiente será hacer funcionar nuestra autenticación para proteger el acceso a la página dashboard si el usuario no está autenticado o lo que es lo mismo convertiremos esta ruta en privada. Para conseguir esto tenemos que configurar NextAuth e implementar los servicios que ataquen a nuestro wordpress. ¡Empecemos! 👨‍🏭

Crea un archivo en la siguiente ruta pages/api/auth llamado de la siguiente manera [...nextauth].js. Esta forma de nombrar archivos es la manera de decirle a next.js que vamos a hacer una declaración de rutas dinámicas, de las cuales se encargará de implementar NextAuth con el siguente código:

import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";

export const authOptions = {
  providers: [
    CredentialsProvider({
      name: "Credentials",
      async authorize(credentials, req) {
        // Añadiremos la lógica después

        console.log('credentials:server', credentials);
        return null;
      }
    })
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.accessToken = user.accessToken;
      }
      return token;
    },
  },
  jwt: {
    maxAge: 60 * 60 * 24 * 7,
  },
  pages: {
    signIn: '/auth/signin',
    error: '/auth/signin',
  },
  secret: process.env.NEXTAUTH_SECRET,
}
export default NextAuth(authOptions);

Para poder acceder desde nuestras vistas a los hooks que nos provee NextAuth tenemos acondicionar nuestra aplicación desde la raíz que se encuentra en el archivo _app.js y añadiendo algunos cambios quedaría así:

import '@/styles/globals.css';

import { SessionProvider } from 'next-auth/react';

export default function App({
  Component,
  pageProps: { session, ...pageProps },
}) {
  return (
    <SessionProvider session={session}>
      <Component {...pageProps} />
    </SessionProvider>
  );
}

Para comprobar si hemos configurado bien nuestras rutas para hacer autenticación vamos al archivo pages/auth/signin.jsx y hacemos algunas modificaciones quedando así:

import { authOptions } from 'pages/api/auth/[...nextauth]';
import { getServerSession } from 'next-auth/next';
import { signIn, getCsrfToken } from 'next-auth/react';

export async function getServerSideProps(context) {
  const session = await getServerSession(context.req, context.res, authOptions);

  if (session) {
    return {
      redirect: {
        destination: '/dashboard',
        permanent: false,
      },
    };
  }

  return {
    props: {
      csrfToken: await getCsrfToken(context),
    },
  };
}

export default function Signin({ csrfToken }) {
  const handleSingIn = (event) => {
    event.preventDefault();
    const { email, password } = event.target;
    signIn('credentials', {
      email: email.value,
      password: password.value,
      callbackUrl: '/dashboard',
      redirect: false
    })
      .then((res) => {
        console.log('credentials:client', res);
      });
  };

  return (
    <>
      <div className="flex min-h-full flex-1 flex-col justify-center px-6 py-12 lg:px-8">
        <div className="sm:mx-auto sm:w-full sm:max-w-sm">
          <img
            className="mx-auto h-10 w-auto"
            src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600"
            alt="Your Company"
          />
          <h2 className="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
            Sign in to your account
          </h2>
        </div>

        <div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
          <form onSubmit={handleSingIn} className="space-y-6" method="POST">
            <input name="csrfToken" type="hidden" defaultValue={csrfToken} />

            <div>
              <label
                htmlFor="email"
                className="block text-sm font-medium leading-6 text-gray-900"
              >
                Email address
              </label>
              <div className="mt-2">
                <input
                  id="email"
                  name="email"
                  type="email"
                  autoComplete="email"
                  required
                  className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
                />
              </div>
            </div>

            <div>
              <div className="flex items-center justify-between">
                <label
                  htmlFor="password"
                  className="block text-sm font-medium leading-6 text-gray-900"
                >
                  Password
                </label>
                <div className="text-sm">
                  <a
                    href="#"
                    className="font-semibold text-indigo-600 hover:text-indigo-500"
                  >
                    Forgot password?
                  </a>
                </div>
              </div>
              <div className="mt-2">
                <input
                  id="password"
                  name="password"
                  type="password"
                  autoComplete="current-password"
                  required
                  className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
                />
              </div>
            </div>

            <div>
              <button
                type="submit"
                className="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
              >
                Sign in
              </button>
            </div>
          </form>
        </div>
      </div>
    </>
  );
}

Si todo sale bien e intentamos autenticarnos veremos que en la consola de el servidor donde hemos levantado nuestra app de next.js tenemos algo como esto:

credentials:server [Object: null prototype] {
  redirect: 'false',
  email: 'albertodsosa@gmail.com',
  password: 'yalosabes',
  callbackUrl: '/dashboard',
  csrfToken: '06a8f16e86344e7028d1e615a2d533e4d5501914f4d3d37a912f5a28b0cae5aa',
  json: 'true'
}

Y en la del navegador un error de respuesta 401 (Unauthorized) con el log:

credentials:client {
    "error": "CredentialsSignin",
    "status": 401,
    "ok": false,
    "url": null
}

Ahora ya sabiendo que nuestras rutas están haciendo su trabajo vamos a proceder a crear nuestra sesión en la aplicación ahora si preguntándole a wordpress, para ello vamos a crear un par de archivos más let’s go! 👷‍♂️. Lo primero y como buena práctica es crearnos un archivo donde definir nuestras constantes globales, yo suelo hacerlo siempre desde la raíz del proyecto en una carpeta que se llame utils añadir un archivo constants.js y declaramos ahí nuestras variables quedando así:

export const WP_BACK_URL = process.env.WP_BACK_URL;
export const AUTH_SECRET = process.env.NEXTAUTH_SECRET;

Ahora vamos a crear nuestro servicio para comunicarse con wordpress, creamos una carpeta desde la raíz llamada services y creamos dentro un archivo llamado wp-auth.js al que le añadiremos el siguiente código:

import { WP_BACK_URL, AUTH_SECRET } from '@/utils/constants';
import { getToken } from 'next-auth/jwt';

export const getAccessToken = async ({ req }) => {
  const token = await getToken({ req, secret: AUTH_SECRET });

  if (!token) {
    return null;
  }

  return token.accessToken;
};

export const wpAuth = async ({ email, password }) => {
  const respUser = await fetch(`${WP_BACK_URL}/wp-json/jwt-auth/v1/token`, {
    method: 'post',
    body: JSON.stringify({ username: email, password }),
    headers: { 'Content-Type': 'application/json' },
  });

  const user = await respUser.json();

  if (user.token) {
    const respProfile = await fetch(`${WP_BACK_URL}/wp-json/wp/v2/users/me`, {
      headers: {
        Authorization: `Bearer ${user.token}`,
      },
    });

    const profile = await respProfile.json();

    return {
      accessToken: user.token,
      name: user.user_display_name,
      email: user.user_email,
      image: profile.avatar_urls,
      nick: user.user_nicename,
    };
  } else {
    return null;
  }
};

export default wpAuth;

Una vez creado el servicio vamos a probarlo en nuestro archivo de configuración en pages/api/auth/[...nextauth].js que tendría que quedar así:

import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { wpAuth } from '@/services/wp-auth';

export const authOptions = {
  providers: [
    CredentialsProvider({
      name: 'Credentials',

      async authorize(credentials, req) {
        // console.log('credentials:server', credentials);
        return wpAuth(credentials);
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.accessToken = user.accessToken;
      }
      return token;
    },
  },
  jwt: {
    maxAge: 60 * 60 * 24 * 7,
  },
  pages: {
    signIn: '/auth/signin',
    error: '/auth/signin',
  },
  secret: process.env.NEXTAUTH_SECRET,
};

export default NextAuth(authOptions);

Probemos la autenticación entonces introduciendo las credenciales de tu usuario de wordpress, esta vez si todo está bien en la consola del navegador la respuesta tiene que ser un status 200 y el objeto del log tiene que ser algo así:

credentials:client -> {
    "error": null,
    "status": 200,
    "ok": true,
    "url": "http://localhost:3000/dashboard"
}

¡Enhorabuena! Ya has realizado tu primera autenticación con wordpress desde una aplicación de next.js, ahora solo nos queda que la aplicación redireccione a la página dashboard cuando exista sesión. Esto lo conseguimos con un hook que nos suministra next.js llamado useRouter que para resumirlo y que lo veas mejor finalmente el archivo signin.jsx quedaría así:

import { authOptions } from 'pages/api/auth/[...nextauth]';
import { getServerSession } from 'next-auth/next';
import { signIn, getCsrfToken } from 'next-auth/react';

import { useRouter } from 'next/router';

export async function getServerSideProps(context) {
  const session = await getServerSession(context.req, context.res, authOptions);
  
  if (session) {
    return {
      redirect: {
        destination: '/dashboard',
        permanent: false,
      },
    };
  }

  return {
    props: {
      csrfToken: await getCsrfToken(context),
    },
  };
}

export default function Signin({ csrfToken }) {
  const router = useRouter();

  const handleSingIn = (event) => {
    event.preventDefault();
    const { email, password } = event.target;
    signIn('credentials', {
      email: email.value,
      password: password.value,
      callbackUrl: '/dashboard',
      redirect: false,
    }).then((res) => {
      // console.log('credentials:client', res);
      if (res.ok) {
        router.push('/dashboard');
      }
    });
  };

  return (
    <>
      <div className="flex min-h-full flex-1 flex-col justify-center px-6 py-12 lg:px-8">
        <div className="sm:mx-auto sm:w-full sm:max-w-sm">
          <img
            className="mx-auto h-10 w-auto"
            src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600"
            alt="Your Company"
          />
          <h2 className="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
            Sign in to your account
          </h2>
        </div>

        <div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
          <form onSubmit={handleSingIn} className="space-y-6" method="POST">
            <input name="csrfToken" type="hidden" defaultValue={csrfToken} />

            <div>
              <label
                htmlFor="email"
                className="block text-sm font-medium leading-6 text-gray-900"
              >
                Email address
              </label>
              <div className="mt-2">
                <input
                  id="email"
                  name="email"
                  type="email"
                  autoComplete="email"
                  required
                  className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
                />
              </div>
            </div>

            <div>
              <div className="flex items-center justify-between">
                <label
                  htmlFor="password"
                  className="block text-sm font-medium leading-6 text-gray-900"
                >
                  Password
                </label>
                <div className="text-sm">
                  <a
                    href="#"
                    className="font-semibold text-indigo-600 hover:text-indigo-500"
                  >
                    Forgot password?
                  </a>
                </div>
              </div>
              <div className="mt-2">
                <input
                  id="password"
                  name="password"
                  type="password"
                  autoComplete="current-password"
                  required
                  className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
                />
              </div>
            </div>

            <div>
              <button
                type="submit"
                className="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
              >
                Sign in
              </button>
            </div>
          </form>
        </div>
      </div>
    </>
  );
}

Perfecto vamos a proteger la ruta dashboard para que no entren si no está la sesión abierta. Hay una forma muy rápida de hacerlo y es con un middelware, solo tendríamos que crear el archivo middleware.js en la raíz del proyecto con este código:

export { default } from 'next-auth/middleware';

export const config = { matcher: ['/dashboard'] };

Si quieres comprobarlo por las dudas estaría bien e incluso seguir esta guía más avanzada, pero para tener un poco más de control lo vamos ha hacer desde la función getServerSideProps de la propia página, obsérvalo aquí:

import { Fragment } from 'react';

import { authOptions } from 'pages/api/auth/[...nextauth]';
import { getServerSession } from 'next-auth/next';
import { useSession, signOut, getCsrfToken } from 'next-auth/react';

import { Disclosure, Menu, Transition } from '@headlessui/react';
import { Bars3Icon, BellIcon, XMarkIcon } from '@heroicons/react/24/outline';

const navigation = [
  { name: 'Dashboard', href: '/dashboard', current: true },
  { name: 'Home', href: '/', current: false },
  { name: 'Projects', href: '#', current: false },
  { name: 'Calendar', href: '#', current: false },
  { name: 'Reports', href: '#', current: false },
];
const userNavigation = [
  { name: 'Your Profile', href: '#' },
  { name: 'Settings', href: '#' },
];

function classNames(...classes) {
  return classes.filter(Boolean).join(' ');
}

export async function getServerSideProps(context) {
  const session = await getServerSession(context.req, context.res, authOptions);

  if (!session) {
    return {
      redirect: {
        destination: '/auth/signin',
        permanent: false,
      },
    };
  }

  return {
    props: {
      session,
      csrfToken: await getCsrfToken(context),
    },
  };
}

export default function Dashboard() {
  const session = useSession();

  const { email, image, name } = session.data.user;

  const user = {
    imageUrl: image['48'],
    name,
    email,
  };

  return (
    <>
      <div className="min-h-full">
        <Disclosure as="nav" className="bg-gray-800">
          {({ open }) => (
            <>
              <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
                <div className="flex h-16 items-center justify-between">
                  <div className="flex items-center">
                    <div className="flex-shrink-0">
                      <img
                        className="h-8 w-8"
                        src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=500"
                        alt="Your Company"
                      />
                    </div>
                    <div className="hidden md:block">
                      <div className="ml-10 flex items-baseline space-x-4">
                        {navigation.map((item) => (
                          <a
                            key={item.name}
                            href={item.href}
                            className={classNames(
                              item.current
                                ? 'bg-gray-900 text-white'
                                : 'text-gray-300 hover:bg-gray-700 hover:text-white',
                              'rounded-md px-3 py-2 text-sm font-medium'
                            )}
                            aria-current={item.current ? 'page' : undefined}
                          >
                            {item.name}
                          </a>
                        ))}
                      </div>
                    </div>
                  </div>
                  <div className="hidden md:block">
                    <div className="ml-4 flex items-center md:ml-6">
                      <button
                        type="button"
                        className="relative rounded-full bg-gray-800 p-1 text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800"
                      >
                        <span className="absolute -inset-1.5" />
                        <span className="sr-only">View notifications</span>
                        <BellIcon className="h-6 w-6" aria-hidden="true" />
                      </button>

                      {/* Profile dropdown */}
                      <Menu as="div" className="relative ml-3">
                        <div>
                          <Menu.Button className="relative flex max-w-xs items-center rounded-full bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800">
                            <span className="absolute -inset-1.5" />
                            <span className="sr-only">Open user menu</span>
                            <img
                              className="h-8 w-8 rounded-full"
                              src={user.imageUrl}
                              alt=""
                            />
                          </Menu.Button>
                        </div>
                        <Transition
                          as={Fragment}
                          enter="transition ease-out duration-100"
                          enterFrom="transform opacity-0 scale-95"
                          enterTo="transform opacity-100 scale-100"
                          leave="transition ease-in duration-75"
                          leaveFrom="transform opacity-100 scale-100"
                          leaveTo="transform opacity-0 scale-95"
                        >
                          <Menu.Items className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
                            {userNavigation.map((item) => (
                              <Menu.Item key={item.name}>
                                {({ active }) => (
                                  <a
                                    href={item.href}
                                    className={classNames(
                                      active ? 'bg-gray-100' : '',
                                      'block px-4 py-2 text-sm text-gray-700'
                                    )}
                                  >
                                    {item.name}
                                  </a>
                                )}
                              </Menu.Item>
                            ))}
                            <Menu.Item className="cursor-pointer">
                              {({ active }) => (
                                <span
                                  onClick={() => {
                                    signOut();
                                  }}
                                  className={classNames(
                                    active ? 'bg-gray-100' : '',
                                    'block px-4 py-2 text-sm text-gray-700'
                                  )}
                                >
                                  Log out
                                </span>
                              )}
                            </Menu.Item>
                          </Menu.Items>
                        </Transition>
                      </Menu>
                    </div>
                  </div>
                  <div className="-mr-2 flex md:hidden">
                    {/* Mobile menu button */}
                    <Disclosure.Button className="relative inline-flex items-center justify-center rounded-md bg-gray-800 p-2 text-gray-400 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800">
                      <span className="absolute -inset-0.5" />
                      <span className="sr-only">Open main menu</span>
                      {open ? (
                        <XMarkIcon
                          className="block h-6 w-6"
                          aria-hidden="true"
                        />
                      ) : (
                        <Bars3Icon
                          className="block h-6 w-6"
                          aria-hidden="true"
                        />
                      )}
                    </Disclosure.Button>
                  </div>
                </div>
              </div>

              <Disclosure.Panel className="md:hidden">
                <div className="space-y-1 px-2 pb-3 pt-2 sm:px-3">
                  {navigation.map((item) => (
                    <Disclosure.Button
                      key={item.name}
                      as="a"
                      href={item.href}
                      className={classNames(
                        item.current
                          ? 'bg-gray-900 text-white'
                          : 'text-gray-300 hover:bg-gray-700 hover:text-white',
                        'block rounded-md px-3 py-2 text-base font-medium'
                      )}
                      aria-current={item.current ? 'page' : undefined}
                    >
                      {item.name}
                    </Disclosure.Button>
                  ))}
                </div>
                <div className="border-t border-gray-700 pb-3 pt-4">
                  <div className="flex items-center px-5">
                    <div className="flex-shrink-0">
                      <img
                        className="h-10 w-10 rounded-full"
                        src={user.imageUrl}
                        alt=""
                      />
                    </div>
                    <div className="ml-3">
                      <div className="text-base font-medium leading-none text-white">
                        {user.name}
                      </div>
                      <div className="text-sm font-medium leading-none text-gray-400">
                        {user.email}
                      </div>
                    </div>
                    <button
                      type="button"
                      className="relative ml-auto flex-shrink-0 rounded-full bg-gray-800 p-1 text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800"
                    >
                      <span className="absolute -inset-1.5" />
                      <span className="sr-only">View notifications</span>
                      <BellIcon className="h-6 w-6" aria-hidden="true" />
                    </button>
                  </div>
                  <div className="mt-3 space-y-1 px-2">
                    {userNavigation.map((item) => (
                      <Disclosure.Button
                        key={item.name}
                        as="a"
                        href={item.href}
                        className="block rounded-md px-3 py-2 text-base font-medium text-gray-400 hover:bg-gray-700 hover:text-white"
                      >
                        {item.name}
                      </Disclosure.Button>
                    ))}
                    <Disclosure.Button
                      className="block rounded-md px-3 py-2 text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white"
                      onClick={() => {
                        signOut();
                      }}
                    >
                      Log out
                    </Disclosure.Button>
                  </div>
                </div>
              </Disclosure.Panel>
            </>
          )}
        </Disclosure>

        <header className="bg-white shadow">
          <div className="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
            <h1 className="text-3xl font-bold tracking-tight text-gray-900">
              Dashboard
            </h1>
          </div>
        </header>
        <main>
          <div className="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8">
            {/* Your content */}
          </div>
        </main>
      </div>
    </>
  );
}

Para rematar la faena mira los cambios que le he hecho a la home según exista o no sesión:

import { useState } from 'react';
import { useSession, signOut } from 'next-auth/react';
import Link from 'next/link';

import { Dialog } from '@headlessui/react';
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline';

const navigation = [
  { name: 'Product', href: '#' },
  { name: 'Features', href: '#' },
  { name: 'Marketplace', href: '#' },
  { name: 'Company', href: '#' },
];

export default function HomePage() {
  const session = useSession();
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);

  return (
    <div className="bg-white">
      <header className="absolute inset-x-0 top-0 z-50">
        <nav
          className="flex items-center justify-between p-6 lg:px-8"
          aria-label="Global"
        >
          <div className="flex lg:flex-1">
            <a href="#" className="-m-1.5 p-1.5">
              <span className="sr-only">Your Company</span>
              <img
                className="h-8 w-auto"
                src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600"
                alt=""
              />
            </a>
          </div>
          <div className="flex lg:hidden">
            <button
              type="button"
              className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-700"
              onClick={() => setMobileMenuOpen(true)}
            >
              <span className="sr-only">Open main menu</span>
              <Bars3Icon className="h-6 w-6" aria-hidden="true" />
            </button>
          </div>
          <div className="hidden lg:flex lg:gap-x-12">
            {navigation.map((item) => (
              <Link
                key={item.name}
                href={item.href}
                className="text-sm font-semibold leading-6 text-gray-900"
              >
                {item.name}
              </Link>
            ))}
          </div>
          <div className="hidden lg:flex lg:flex-1 lg:justify-end">
            {session.status === 'authenticated' ? (
              <span
                onClick={() => {signOut()}}
                className="text-sm font-semibold leading-6 text-gray-900 cursor-pointer"
              >
                Log out <span aria-hidden="true">&rarr;</span>
              </span>
            ) : (
              <Link
                href="/auth/signin"
                className="text-sm font-semibold leading-6 text-gray-900"
              >
                Log in <span aria-hidden="true">&rarr;</span>
              </Link>
            )}
          </div>
        </nav>
        <Dialog
          as="div"
          className="lg:hidden"
          open={mobileMenuOpen}
          onClose={setMobileMenuOpen}
        >
          <div className="fixed inset-0 z-50" />
          <Dialog.Panel className="fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-white px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10">
            <div className="flex items-center justify-between">
              <a href="#" className="-m-1.5 p-1.5">
                <span className="sr-only">Your Company</span>
                <img
                  className="h-8 w-auto"
                  src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600"
                  alt=""
                />
              </a>
              <button
                type="button"
                className="-m-2.5 rounded-md p-2.5 text-gray-700"
                onClick={() => setMobileMenuOpen(false)}
              >
                <span className="sr-only">Close menu</span>
                <XMarkIcon className="h-6 w-6" aria-hidden="true" />
              </button>
            </div>
            <div className="mt-6 flow-root">
              <div className="-my-6 divide-y divide-gray-500/10">
                <div className="space-y-2 py-6">
                  {navigation.map((item) => (
                    <Link
                      key={item.name}
                      href={item.href}
                      className="-mx-3 block rounded-lg px-3 py-2 text-base font-semibold leading-7 text-gray-900 hover:bg-gray-50"
                    >
                      {item.name}
                    </Link>
                  ))}
                </div>
                <div className="py-6">
                  {session.status === 'authenticated' ? (
                    <span
                      onClick={() => {signOut()}}
                      className="text-sm font-semibold leading-6 text-gray-900 cursor-pointer"
                    >
                      Log out <span aria-hidden="true">&rarr;</span>
                    </span>
                  ) : (
                    <Link
                      href="/auth/signin"
                      className="text-sm font-semibold leading-6 text-gray-900"
                    >
                      Log in <span aria-hidden="true">&rarr;</span>
                    </Link>
                  )}
                </div>
              </div>
            </div>
          </Dialog.Panel>
        </Dialog>
      </header>

      <div className="relative isolate px-6 pt-14 lg:px-8">
        <div
          className="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80"
          aria-hidden="true"
        >
          <div
            className="relative left-[calc(50%-11rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 rotate-[30deg] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-30 sm:left-[calc(50%-30rem)] sm:w-[72.1875rem]"
            style={{
              clipPath:
                'polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)',
            }}
          />
        </div>
        <div className="mx-auto max-w-2xl py-32 sm:py-48 lg:py-56">
          <div className="hidden sm:mb-8 sm:flex sm:justify-center">
            <div className="relative rounded-full px-3 py-1 text-sm leading-6 text-gray-600 ring-1 ring-gray-900/10 hover:ring-gray-900/20">
              Announcing our next round of funding.{' '}
              <a href="#" className="font-semibold text-indigo-600">
                <span className="absolute inset-0" aria-hidden="true" />
                Read more <span aria-hidden="true">&rarr;</span>
              </a>
            </div>
          </div>
          <div className="text-center">
            <h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
              Data to enrich your online business
            </h1>
            <p className="mt-6 text-lg leading-8 text-gray-600">
              Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui
              lorem cupidatat commodo. Elit sunt amet fugiat veniam occaecat
              fugiat aliqua.
            </p>
            <div className="mt-10 flex items-center justify-center gap-x-6">
              <a
                href="#"
                className="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
              >
                Get started
              </a>
              <a
                href="#"
                className="text-sm font-semibold leading-6 text-gray-900"
              >
                Learn more <span aria-hidden="true">→</span>
              </a>
            </div>
          </div>
        </div>
        <div
          className="absolute inset-x-0 top-[calc(100%-13rem)] -z-10 transform-gpu overflow-hidden blur-3xl sm:top-[calc(100%-30rem)]"
          aria-hidden="true"
        >
          <div
            className="relative left-[calc(50%+3rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-30 sm:left-[calc(50%+36rem)] sm:w-[72.1875rem]"
            style={{
              clipPath:
                'polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)',
            }}
          />
        </div>
      </div>
    </div>
  );
}

¡Y esto sería todo! 🥳 Gracias por llegar hasta aquí y espero que sigas el próximo tutorial donde vamos a traernos contenido de diversas formas de Wordpress y manejarlo con ReactQuery ¡Nos vemos allá! 🙋‍♂️