Cómo generar un mapa de sitio dinámico con Next.js

Cómo generar dinámicamente un mapa del sitio para su sitio o aplicación basada en Next.js para mejorar la visibilidad de su sitio para motores de búsqueda como Google y DuckDuckGo.

Si está creando un sitio o una aplicación con Next.js que debe ser visible para los motores de búsqueda como Google, es esencial tener un mapa del sitio disponible. Un mapa del sitio es un mapa de las URL de su sitio y facilita que los motores de búsqueda indexen su contenido, lo que aumenta la probabilidad de que se clasifique en los resultados de búsqueda.

En Next.js, debido a que confiamos en el enrutador incorporado para exponer las rutas al público, la forma más fácil de configurar un mapa del sitio es crear un componente de página especial que modifique sus encabezados de respuesta para señalar a los navegadores que el contenido se está devolviendo. es text/xml datos (los navegadores y los motores de búsqueda anticipan que nuestro mapa del sitio se devuelve como un archivo XML).

Al hacer esto, podemos aprovechar las ventajas habituales de obtención y representación de datos de React y Next.js y, al mismo tiempo, devolver datos en un formato que el navegador espera.

Para demostrar cómo funciona esto, vamos a utilizar CheatCode Next.js Boilerplate como punto de partida. Para comenzar, clona una copia de Github:

git clone https://github.com/cheatcode/nextjs-boilerplate.git

A continuación, cd en el directorio clonado e instale las dependencias de la Boilerplate a través de NPM:

cd nextjs-boilerplate && npm install

Finalmente, comience la caldera con (desde el directorio raíz del proyecto):

npm run dev

Una vez que todo esto está completo, estamos listos para comenzar a construir nuestro componente de mapa del sitio.

Creación de un componente de página de mapa del sitio

Primero, en el /pages Directorio en la raíz del proyecto, cree un nuevo archivo (archivo, no una carpeta) llamado sitemap.xml.js . La razón por la que estamos eligiendo este nombre es que Next.js creará automáticamente una ruta en nuestra aplicación en /sitemap.xml cuál es la ubicación donde los navegadores y los rastreadores de motores de búsqueda esperan que viviera nuestro mapa del sitio.

A continuación, dentro del archivo, comencemos a construir el componente:

/pages/sitemap.xml.js

import React from "react";

const Sitemap = () => {};

export default Sitemap;

Lo primero que notará es que este componente es solo un componente de función vacío (lo que significa que no estamos procesando ningún marcado cuando React procesa el componente). Esto se debe a que, técnicamente hablando, no queremos representar un componente en esta URL. En su lugar, queremos secuestrar el getServerSideProps (Next.js lo llama cuando recibe una solicitud entrante en el servidor) para decir "en lugar de obtener algunos datos y asignarlos a los accesorios de nuestro componente, anule el res objeto (nuestra respuesta) y en su lugar devuelve el contenido de nuestro mapa del sitio ".

Eso es probablemente confuso. Fuyendo esto un poco más, agregemos una versión aproximada del res anulaciones que necesitamos hacer:

/pages/sitemap.xml.js

import React from "react";

const Sitemap = () => {};

export const getServerSideProps = ({ res }) => {
  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
      <!-- We'll render the URLs for our sitemap here. -->
    </urlset>
  `;

  res.setHeader("Content-Type", "text/xml");
  res.write(sitemap);
  res.end();

  return {
    props: {},
  };
};

export default Sitemap;

Esto debería hacer que el concepto de "anulación" sea más concreto. Ahora, podemos ver que en lugar de devolver un objeto de accesorios de getServerSideProps , estamos llamando manualmente para configurar el Content-Type encabezado de la respuesta, escriba el cuerpo de la respuesta y finalice la solicitud (lo que indica que la respuesta debe enviarse de vuelta a la solicitud original).

Aquí, hemos especificado la plantilla básica para un mapa del sitio. Como insinuamos anteriormente, se espera que un mapa del sitio esté en un formato de datos XML (o, text/xml Tipo de Mimica). Luego, cuando obtengamos nuestros datos, completaremos el <urlset></urlset> etiqueta con <url></url> etiquetas Cada etiqueta representará una de las páginas de nuestro sitio y proporcionará la URL de esa página.

En la parte inferior del getInitialProps función, manejamos nuestra respuesta a la solicitud entrante.

Primero, configuramos el Content-Type encabezado en la respuesta para indicarle al navegador que estamos devolviendo un .xml expediente. Esto funciona porque el Content-Type establece las expectativas de lo que el navegador necesita representar y el sitemap.xml parte de nuestro sitemap.xml.js el nombre del archivo es lo que utiliza Next.js para la URL de la página. Entonces, si llamamos a nuestra página pizza.json.js , la URL generada por Next.js sería algo así como http://mydomain.com/pizza.json (en este caso, obtendremos http://mydomain.com/sitemap.xml ).

A continuación, llamamos a res.write() , pasando el sitemap generado cuerda. Esto representará el cuerpo de la respuesta que recibe el navegador (o el rastreador del motor de búsqueda). Después, devolvemos la señal de que "hemos enviado todo lo que podemos enviar" a la solicitud con res.end() .

Para cumplir con los requisitos del getServerSideProps función (según las reglas de Next.js), devolvemos un objeto vacío con un props propiedad establecida en un objeto vacío; para ser claros, si no hacemos esto, Next.js arrojará un error.

Obteniendo datos para su mapa del sitio

Ahora viene la parte divertida. A continuación, debemos obtener todo el contenido de nuestro sitio que queremos representar en nuestro mapa del sitio. Por lo general, esto es todo , pero es posible que tenga ciertas páginas que desee excluir.

Cuando se trata de qué contenido que buscamos para devolverlo en nuestro mapa del sitio, hay dos tipos:

  1. Páginas estáticas - Páginas que se encuentran en una URL fija en su sitio/aplicación. Por ejemplo, http://mydomain.com/about .
  2. Páginas dinámicas - Páginas que se encuentran en una URL variable en su sitio/aplicación, como una publicación de blog o algún otro contenido dinámico. Por ejemplo, http://mydomain.com/posts/slug-of-my-post .

La recuperación de estos datos se realiza de un par de maneras. Primero, para páginas estáticas, podemos enumerar el contenido de nuestro /pages directorio (filtrando los elementos que queremos ignorar). Para las páginas dinámicas, se puede adoptar un enfoque similar, obteniendo datos de una API REST o una API GraphQL.

Para comenzar, veamos cómo obtener una lista de elementos estáticos. páginas en nuestra aplicación y cómo agregar algunos filtros para recortar lo que queremos:

/pages/sitemap.xml.js

import React from "react";
import fs from "fs";

const Sitemap = () => {};

export const getServerSideProps = ({ res }) => {
  const baseUrl = {
    development: "http://localhost:5000",
    production: "https://mydomain.com",
  }[process.env.NODE_ENV];

  const staticPages = fs
    .readdirSync("pages")
    .filter((staticPage) => {
      return ![
        "_app.js",
        "_document.js",
        "_error.js",
        "sitemap.xml.js",
      ].includes(staticPage);
    })
    .map((staticPagePath) => {
      return `${baseUrl}/${staticPagePath}`;
    });

  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
      ${staticPages
        .map((url) => {
          return `
            <url>
              <loc>${url}</loc>
              <lastmod>${new Date().toISOString()}</lastmod>
              <changefreq>monthly</changefreq>
              <priority>1.0</priority>
            </url>
          `;
        })
        .join("")}
    </urlset>
  `;

  res.setHeader("Content-Type", "text/xml");
  res.write(sitemap);
  res.end();

  return {
    props: {},
  };
};

export default Sitemap;

Hemos agregado tres cosas importantes aquí:

Primero, hemos agregado un nuevo baseUrl valor en la parte superior de nuestro getServerSideProps función que nos permitirá establecer la base de cada URL que representamos en nuestro mapa del sitio. Esto es necesario porque nuestro mapa del sitio debe incluir absoluto caminos.

En segundo lugar, hemos agregado una llamada al fs.readdirSync() función (con fs importado en la parte superior del archivo), que es el método de directorio de lectura síncrono integrado en Node.js. Esto nos permite obtener la lista de archivos de un directorio en la ruta que pasamos (aquí, especificamos el pages porque queremos obtener todas nuestras páginas estáticas).

Una vez obtenido, hacemos un punto para llamar a .filter() en la matriz que esperamos recuperar, filtrando las páginas de utilidad en nuestro sitio (incluyendo sitemap.xml.js mismo) que no queremos presente en nuestro mapa del sitio. Después de esto mapeamos cada una de las páginas válidas y concatenamos su ruta con el baseUrl determinamos en base a nuestro NODE_ENV actual arriba.

Si fuéramos a console.log(staticPages) , el resultado final de esto debería verse así:

[
  'http://localhost:5000/documents',
  'http://localhost:5000/login',
  'http://localhost:5000/recover-password',
  'http://localhost:5000/reset-password',
  'http://localhost:5000/signup'
]

En tercer lugar, volver a centrarnos en nuestro sitemap variable donde almacenamos nuestro mapa del sitio como una cadena (antes de pasar a res.write() ), podemos ver que hemos modificado esto para realizar un .map() sobre nuestro staticPages matriz, devolviendo una cadena que contiene el marcado necesario para agregar una URL a nuestro mapa del sitio:

/pages/sitemap.xml.js

${staticPages
  .map((url) => {
    return `
      <url>
        <loc>${url}</loc>
        <lastmod>${new Date().toISOString()}</lastmod>
        <changefreq>monthly</changefreq>
        <priority>1.0</priority>
      </url>
    `;
  })
  .join("")}

En términos de qué estamos devolviendo, aquí devolvemos el contenido XML esperado por un navegador web (o rastreador de motor de búsqueda) al leer un mapa del sitio. Para cada URL en nuestro sitio que queremos agregar a nuestro mapa, agregamos el <url></url> etiqueta, colocando un <loc></loc> etiqueta interior que especifica la ubicación de nuestra URL, el <lastmod></lastmod> etiqueta que especifica cuándo se actualizó por última vez el contenido de la URL, el <changefreq></changefreq> etiqueta que especifica cómo con frecuencia se actualiza el contenido de la URL y un <priority></priority> etiqueta para especificar la importancia de la URL (que se traduce en la frecuencia con la que un rastreador debe rastrear esa página).

Aquí, pasamos nuestro url a <loc></loc> y luego configure nuestro <lastmod></lastmod> a la fecha actual como una cadena ISO-8601 (un tipo estándar de formato de fecha legible por computadora/humano). Si tiene una fecha disponible para la última actualización de estas páginas, es mejor ser lo más preciso posible con esta fecha y pasar esa fecha específica aquí.

Para <changefreq></changefreq> , estamos configurando un valor predeterminado razonable de monthly , pero puede ser cualquiera de los siguientes:

  • never
  • yearly ,
  • monthly
  • weekly
  • daily
  • hourly
  • always

Similar al <lastmod></lastmod> etiqueta, querrá que sea lo más precisa posible para evitar cualquier problema con las reglas de los motores de búsqueda.

Finalmente, para <priority></priority> , establecemos una base de 1.0 (el nivel máximo de importancia). Si desea cambiar esto para que sea más específico, este número puede ser cualquier valor entre 0.0 y 1.0 con 0.0 siendo poco importante, 1.0 siendo lo más importante.

Aunque puede que no parezca mucho ahora, técnicamente, si visitamos http://localhost:5000/sitemap.xml en nuestro navegador (asumiendo que está trabajando con CheatCode Next.js Boilerplate e inició el servidor de desarrollo antes), deberíamos ver un mapa del sitio que contiene nuestras páginas estáticas.

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>http://localhost:5000/documents</loc>
    <lastmod>2021-04-14T01:36:47.469Z</lastmod>
    <changefreq>monthly</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>http://localhost:5000/login</loc>
    <lastmod>2021-04-14T01:36:47.469Z</lastmod>
    <changefreq>monthly</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>http://localhost:5000/recover-password</loc>
    <lastmod>2021-04-14T01:36:47.469Z</lastmod>
    <changefreq>monthly</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>http://localhost:5000/reset-password</loc>
    <lastmod>2021-04-14T01:36:47.469Z</lastmod>
    <changefreq>monthly</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>http://localhost:5000/signup</loc>
    <lastmod>2021-04-14T01:36:47.469Z</lastmod>
    <changefreq>monthly</changefreq>
    <priority>1.0</priority>
  </url>
</urlset>

A continuación, echemos un vistazo a la expansión de nuestro mapa del sitio obteniendo nuestras páginas dinámicas usando GraphQL.

Generando datos dinámicos para nuestro sitemap

Debido a que estamos usando CheatCode Next.js Boilerplate para nuestro ejemplo, ya tenemos el cableado necesario para un cliente GraphQL. Para contextualizar nuestro trabajo, vamos a usar esta función junto con CheatCode Node.js Boilerplate, que incluye una base de datos de ejemplo que usa MongoDB, un servidor GraphQL completamente implementado y una colección de Documentos de ejemplo que podemos usar para extraer datos de prueba. de.

Primero, clonemos una copia de Node.js Boilerplate y configurémoslo:

git clone https://github.com/cheatcode/nodejs-server-boilerplate.git

Y luego cd en el proyecto clonado e instale todas las dependencias:

cd nodejs-server-boilerplate && npm install

Finalmente, continúe y ejecute el servidor con (desde la raíz del proyecto):

npm run dev

Si sigue adelante y abre el proyecto, agregaremos un poco de código para sembrar la base de datos con algunos documentos, de modo que tengamos algo que buscar para nuestro mapa del sitio:

/api/fixtures/documents.js

import _ from "lodash";
import generateId from "../../lib/generateId";
import Documents from "../documents";
import Users from "../users";

export default async () => {
  let i = 0;

  const testUser = await Users.findOne();
  const existingDocuments = await Documents.find().count();

  if (existingDocuments < 100) {
    while (i < 100) {
      const title = `Document #${i + 1}`;

      await Documents.insertOne({
        _id: generateId(),
        title,
        userId: testUser?._id,
        content: "Test content.",
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      });

      i += 1;
    }
  }
};

Primero, necesitamos crear un archivo para contener un accesorio (un apodo para el código que genera datos de prueba para nosotros) que generará nuestros documentos de prueba para nosotros. Para hacerlo, exportamos una función que hace algunas cosas:

  1. Recupera un usuario de prueba (creado por el /api/fixtures/users.js incluido accesorio incluido con el repetitivo).
  2. Recupera el .count() existente de documentos en la base de datos.
  3. Ejecuta un while bucle para decir "mientras que el número de existingDocuments en la base de datos es menor que 100 , inserte un documento."

Para el contenido del documento, generamos un título que utiliza el i actual iteración del ciclo más uno para generar un título diferente para cada documento generado. A continuación, llamamos al Documents.insertOne() función, proporcionada por nuestra importación del Documents colección (ya implementada en el repetitivo) a .insertOne() documento.

Ese documento incluye un _id establecido en una cadena hexadecimal usando el generateId() incluido función en el repetitivo. A continuación, configuramos el title , seguido del userId establecido en el _id del testUser recuperamos y luego configuramos contenido ficticio junto con un createdAt y updatedAt marca de tiempo por si acaso (estas entrarán en juego en nuestro mapa del sitio a continuación).

/api/index.js

import graphql from "./graphql/server";
import usersFixture from "./fixtures/users";
import documentsFixture from "./fixtures/documents";

export default async (app) => {
  graphql(app);
  await usersFixture();
  await documentsFixture();
};

Para que todo esto funcione, debemos extraer el users incluido accesorio y nuestro nuevo documents función en el /api/index.js (este archivo se carga automáticamente para nosotros al iniciar el servidor). Debido a que nuestros aparatos se exportan como funciones, después de importarlos, en la función exportada desde /api/index.js , llamamos a esas funciones, asegurándonos de await las llamadas para evitar condiciones de carrera con nuestros datos (recuerde, nuestro usuario debe existir antes de que intentemos crear documentos).

Antes de continuar, debemos hacer un pequeño cambio más para asegurarnos de que podamos obtener documentos para nuestra prueba:

/api/documents/graphql/queries.js

import isDocumentOwner from "../../../lib/isDocumentOwner";
import Documents from "../index";

export default {
  documents: async (parent, args, context) => {
    return Documents.find().toArray();
  },
  [...]
};

Por defecto, el ejemplo documents resolvler en Node.js Boilerplate pasa una consulta al Documents.find() método que solicita documentos atrasados ​​solo para el _id del usuario que ha iniciado sesión . Aquí, podemos eliminar esta consulta y simplemente pedir que nos devuelvan todos los documentos, ya que solo estamos probando esto.

Eso es todo en el lado del servidor. Volvamos al cliente y conectemos esto a nuestro mapa del sitio.

Obtener datos de nuestra API GraphQL

Como vimos en la última sección, el Boilerplate de Node.js también incluye un servidor GraphQL completamente configurado y resolutores existentes para obtener documentos. De vuelta en nuestro /pages/sitemap.xml.js archivo, extraigamos el cliente GraphQL incluido en el Boilerplate de Next.js y obtengamos algunos datos del documents existente resolver en la API de GraphQL:

/pages/sitemap.xml.js

import React from "react";
import fs from "fs";
import { documents as documentsQuery } from "../graphql/queries/Documents.gql";
import client from "../graphql/client";

const Sitemap = () => {};

export const getServerSideProps = async ({ res }) => {
  const baseUrl = {
    development: "http://localhost:5000",
    production: "https://mydomain.com",
  }[process.env.NODE_ENV];

  const staticPages = fs
    .readdirSync("pages")
    .filter((staticPage) => {
      return ![
        "_app.js",
        "_document.js",
        "_error.js",
        "sitemap.xml.js",
      ].includes(staticPage);
    })
    .map((staticPagePath) => {
      return `${baseUrl}/${staticPagePath}`;
    });

  const { data } = await client.query({ query: documentsQuery });
  const documents = data?.documents || [];

  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
      ${staticPages
        .map((url) => {
          return `
            <url>
              <loc>${url}</loc>
              <lastmod>${new Date().toISOString()}</lastmod>
              <changefreq>monthly</changefreq>
              <priority>1.0</priority>
            </url>
          `;
        })
        .join("")}
      ${documents
        .map(({ _id, updatedAt }) => {
          return `
              <url>
                <loc>${baseUrl}/documents/${_id}</loc>
                <lastmod>${updatedAt}</lastmod>
                <changefreq>monthly</changefreq>
                <priority>1.0</priority>
              </url>
            `;
        })
        .join("")}
    </urlset>
  `;

  res.setHeader("Content-Type", "text/xml");
  res.write(sitemap);
  res.end();

  return {
    props: {},
  };
};

export default Sitemap;

En la parte superior del archivo, hemos importado el archivo de consulta GraphQL de ejemplo del /graphql/queries/Documents.gql archivo incluido en CheatCode Next.js Boilerplate. Debajo de eso, también importamos el cliente GraphQL incluido desde /graphql/client.js .

De vuelta en nuestro getServerSideProps función, agregamos una llamada a client.query() para ejecutar una consulta GraphQL para nuestros documentos justo debajo de nuestra llamada anterior para obtener nuestro staticPages . Con nuestra lista a cuestas, repetimos el mismo patrón que vimos antes, .map() sobre el documents encontramos y usando la misma estructura XML que usamos con nuestras páginas estáticas.

La gran diferencia aquí es que para nuestro <loc></loc> , estamos construyendo nuestra URL a mano dentro del .map() , utilizando nuestro baseUrl existente valor y agregando /documents/${_id} a ella, donde _id es el ID único del documento actual sobre el que estamos mapeando. También hemos cambiado la llamada en línea a new Date().toISOString() pasado a <lastmod></lastmod> con el updatedAt marca de tiempo que establecemos en la base de datos.

¡Eso es todo! Si visitas http://localhost:5000/sitemap.xml en el navegador, debería ver nuestras páginas estáticas existentes, junto con nuestras URL de documentos generadas dinámicamente:

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>http://localhost:5000/documents</loc>
    <lastmod>2021-04-14T03:06:24.018Z</lastmod>
    <changefreq>monthly</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>http://localhost:5000/login</loc>
    <lastmod>2021-04-14T03:06:24.018Z</lastmod>
    <changefreq>monthly</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>http://localhost:5000/recover-password</loc>
    <lastmod>2021-04-14T03:06:24.018Z</lastmod>
    <changefreq>monthly</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>http://localhost:5000/reset-password</loc>
    <lastmod>2021-04-14T03:06:24.018Z</lastmod>
    <changefreq>monthly</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>http://localhost:5000/signup</loc>
    <lastmod>2021-04-14T03:06:24.018Z</lastmod>
    <changefreq>monthly</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>http://localhost:5000/documents/y9QSUXFlSqzl3ZzN</loc>
    <lastmod>2021-04-14T02:27:06.747Z</lastmod>
    <changefreq>monthly</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>http://localhost:5000/documents/6okKJ3vHX5K0F4A1</loc>
    <lastmod>2021-04-14T02:27:06.749Z</lastmod>
    <changefreq>monthly</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>http://localhost:5000/documents/CdyxBJnVk70vpeSX</loc>
    <lastmod>2021-04-14T02:27:06.750Z</lastmod>
    <changefreq>monthly</changefreq>
    <priority>1.0</priority>
  </url>
  [...]
</urlset>

Desde aquí, una vez que su sitio esté implementado en línea, puede enviar su mapa del sitio a motores de búsqueda como Google para asegurarse de que su sitio esté correctamente indexado y clasificado.

Manejo de problemas de compilación de Next.js en Vercel

Para los desarrolladores que intentan hacer que el código anterior funcione en Vercel, es necesario realizar un pequeño cambio en la llamada a fs.readdirSync() arriba. En lugar de usar fs.readdirSync("pages") como mostramos arriba, deberá modificar su código para que se vea así:

/pages/sitemap.xml.js

const staticPages = fs
  .readdirSync({
    development: 'pages',
    production: './',
  }[process.env.NODE_ENV])
  .filter((staticPage) => {
    return ![
      "_app.js",
      "_document.js",
      "_error.js",
      "sitemap.xml.js",
    ].includes(staticPage);
  })
  .map((staticPagePath) => {
    return `${baseUrl}/${staticPagePath}`;
  });

El cambio aquí es lo que le pasamos a fs.readdirSync() . En una aplicación Next.js implementada por Vercel, la ruta al directorio de sus páginas cambia. Agregar una ruta condicional como la que vemos arriba asegura que cuando se ejecute el código de su mapa del sitio, resuelva las páginas en la ruta correcta (en este caso, en el /build/server/pages directorio generado cuando Vercel construye su aplicación).

Terminando

En este tutorial, aprendimos cómo generar dinámicamente un mapa del sitio con Next.js. Aprendimos a utilizar el getServerSideProps función en Next.js para secuestrar la respuesta a las solicitudes realizadas al /sitemap.xml página en nuestra aplicación y devolver una cadena XML, forzando el Content-Type el encabezado debe ser text/xml para simular devolver un .xml archivo.

También buscamos generar algunos datos de prueba en MongoDB usando Node.js y recuperar esos datos para incluirlos en nuestro mapa del sitio a través de una consulta GraphQL.