Jak vygenerovat dynamický soubor Sitemap pomocí Next.js

Jak dynamicky vygenerovat mapu webu pro váš web nebo aplikaci založenou na Next.js, abyste zlepšili viditelnost vašeho webu pro vyhledávače, jako je Google a DuckDuckGo.

Pokud vytváříte web nebo aplikaci pomocí Next.js, která musí být viditelná pro vyhledávače, jako je Google, je nezbytné mít k dispozici mapu webu. Sitemap je mapa adres URL na vašem webu a usnadňuje vyhledávačům indexování vašeho obsahu, čímž se zvyšuje pravděpodobnost umístění ve výsledcích vyhledávání.

Protože v Next.js spoléháme na vestavěný směrovač při zpřístupňování tras veřejnosti, nejjednodušší způsob, jak nastavit mapu webu, je vytvořit speciální komponentu stránky, která upraví hlavičky svých odpovědí, aby signalizovala prohlížečům, že obsah se vrací je text/xml data (prohlížeče a vyhledávače předpokládají, že se náš soubor Sitemap vrátí jako soubor XML).

Tímto způsobem můžeme využít obvyklé vymoženosti načítání a vykreslování dat React a Next.js a současně vracet data ve formátu, který prohlížeč očekává.

Abychom demonstrovali, jak to funguje, použijeme jako výchozí bod CheatCode Next.js Boilerplate. Chcete-li začít, naklonujte kopii z Github:

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

Dále cd do klonovaného adresáře a nainstalujte závislosti standardu pomocí NPM:

cd nextjs-boilerplate && npm install

Nakonec spusťte standard s (z kořenového adresáře projektu):

npm run dev

Jakmile bude toto vše dokončeno, jsme připraveni začít budovat naši komponentu sitemap.

Vytvoření komponenty stránky mapy webu

Nejprve v /pages adresář v kořenovém adresáři projektu, vytvořte nový soubor (soubor, nikoli složku) s názvem sitemap.xml.js . Důvod, proč jsme zvolili tento název, je ten, že Next.js automaticky vytvoří trasu v naší aplikaci na /sitemap.xml což je místo, kde prohlížeče a prohledávače vyhledávačů očekávají existenci našeho souboru Sitemap.

Dále, uvnitř souboru, začněme sestavovat komponentu:

/pages/sitemap.xml.js

import React from "react";

const Sitemap = () => {};

export default Sitemap;

První věc, které si všimnete, je, že tato komponenta je pouze prázdná funkční komponenta (to znamená, že nevykreslujeme žádné značky, když je komponenta vykreslena Reactem). Je to proto, že technicky vzato nechceme vykreslovat komponentu na této adrese URL. Místo toho chceme unést getServerSideProps metodu (tu volá Next.js, když obdrží příchozí požadavek na server), aby řekla „místo načítání dat a jejich mapování na rekvizity pro naši komponentu přepište res objekt (naše odpověď) a místo toho vrátí obsah naší mapy webu."

To je pravděpodobně matoucí. Abychom to trochu více rozvedli, přidejte hrubou verzi res přepsání, které musíme provést:

/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;

Tím by měl být koncept „přepisu“ konkrétnější. Nyní vidíme, že místo vracení objektu rekvizit z getServerSideProps , ručně voláme, abychom nastavili Content-Type záhlaví odpovědi, napište tělo odpovědi a ukončete požadavek (signalizuje, že odpověď by měla být odeslána zpět na původní požadavek).

Zde jsme specifikovali základní šablonu pro soubor Sitemap. Jak jsme naznačili výše, očekává se, že soubor Sitemap bude ve formátu dat XML (nebo text/xml typ MIME). Dále, když načteme naše data, vyplníme <urlset></urlset> tag s <url></url> značky. Každá značka bude představovat jednu ze stránek na našem webu a poskytne adresu URL této stránky.

V dolní části getInitialProps zpracujeme naši odpověď na příchozí požadavek.

Nejprve nastavíme Content-Type hlavička v odpovědi, která signalizuje zpět prohlížeči, že vracíme .xml soubor. Funguje to, protože Content-Type nastavuje očekávání, co prohlížeč potřebuje vykreslit, a sitemap.xml součástí našeho sitemap.xml.js název souboru je to, co Next.js používá pro URL stránky. Pokud bychom tedy naši stránku nazvali pizza.json.js , bude adresa URL vygenerovaná Next.js něco jako http://mydomain.com/pizza.json (v tomto případě dostaneme http://mydomain.com/sitemap.xml ).

Dále zavoláme res.write() , předáním vygenerovaného sitemap tětiva. To bude představovat tělo odpovědi, kterou obdrží prohlížeč (nebo prohledávač vyhledávače). Poté na požadavek pomocí res.end() upozorníme, že „odeslali jsme vše, co můžeme odeslat“ .

Aby byly splněny požadavky getServerSideProps funkce (podle pravidel Next.js), vrátíme prázdný objekt s props vlastnost nastavena na prázdný objekt – aby bylo jasno, pokud to neuděláme, Next.js vyvolá chybu.

Načítání dat pro váš soubor Sitemap

Nyní k té zábavnější části. Dále musíme na naše stránky dostat veškerý obsah, který chceme reprezentovat v našem souboru Sitemap. Obvykle je to vše , ale můžete mít určité stránky, které chcete vyloučit.

Když dojde na co obsah, který načítáme, aby se vrátil do našeho souboru Sitemap, existují dva typy:

  1. Statické stránky – Stránky, které jsou umístěny na pevné adrese URL na vašem webu/aplikaci. Například http://mydomain.com/about .
  2. Dynamické stránky – Stránky, které se nacházejí na proměnlivé adrese URL na vašem webu/aplikaci, jako je příspěvek na blogu nebo jiný dynamický obsah. Například http://mydomain.com/posts/slug-of-my-post .

Získávání těchto dat se provádí několika způsoby. Za prvé, u statických stránek můžeme vypsat obsah našeho /pages adresář (odfiltrování položek, které chceme ignorovat). U dynamických stránek lze použít podobný přístup, načítání dat z REST API nebo GraphQL API.

Pro začátek se podívejme na načtení seznamu statických stránky v naší aplikaci a jak přidat nějaké filtrování, abychom zkrátili to, co chceme:

/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;

Zde jsme přidali tři velké věci:

Nejprve jsme přidali nový baseUrl hodnotu v horní části našeho getServerSideProps funkce, která nám umožní nastavit základ každé adresy URL, kterou vykreslíme v našem souboru Sitemap. To je nezbytné, protože náš soubor Sitemap musí obsahovat absolutní cesty.

Za druhé jsme přidali volání fs.readdirSync() funkce (s fs importováno v horní části souboru), což je metoda synchronního čtení adresáře, která je zabudována do Node.js. To nám umožňuje získat seznam souborů adresáře na cestě, kterou předáme (zde zadáváme pages adresář, protože chceme získat všechny naše statické stránky).

Po načtení provedeme volání .filter() na poli, které očekáváme, že se vrátíme, odfiltrováním stránek nástrojů na našem webu (včetně sitemap.xml.js sám), které neděláme chcete být zahrnuti v naší mapě webu. Poté zmapujeme každou z platných stránek a zřetězíme jejich cestu s baseUrl jsme určili na základě našeho aktuálního NODE_ENV nahoru.

Pokud bychom měli console.log(staticPages) , konečný výsledek by měl vypadat nějak takto:

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

Za třetí, zaměříme se zpět na naše sitemap proměnná, kde ukládáme naši mapu webu jako řetězec (před předáním na res.write() ), vidíme, že jsme to upravili tak, aby provádělo .map() přes naše staticPages pole, vracející řetězec obsahující potřebné označení pro přidání adresy URL do našeho souboru Sitemap:

/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("")}

Z hlediska čeho vracíme, zde vracíme obsah XML očekávaný webovým prohlížečem (nebo prohledávačem vyhledávače) při čtení mapy webu. Ke každé adrese URL na našem webu, kterou chceme přidat do naší mapy, přidáme <url></url> tag umístěním <loc></loc> tag uvnitř, který určuje umístění naší adresy URL, <lastmod></lastmod> tag, který určuje, kdy byl obsah na adrese URL naposledy aktualizován, <changefreq></changefreq> tag, který určuje jak obsah na adrese URL je často aktualizován a <priority></priority> určete důležitost adresy URL (což znamená, jak často by měl prohledávač procházet danou stránku).

Zde předáme naše url na <loc></loc> a poté nastavte naše <lastmod></lastmod> na aktuální datum jako řetězec ISO-8601 (standardní typ počítačem/člověkem čitelného formátu data). Pokud máte k dispozici datum poslední aktualizace těchto stránek, je nejlepší uvést toto datum co nejpřesněji a uvést toto konkrétní datum zde.

Pro <changefreq></changefreq> , nastavujeme rozumnou výchozí hodnotu monthly , ale může to být některý z následujících:

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

Podobné jako <lastmod></lastmod> , budete chtít, aby to bylo co nejpřesnější, abyste se vyhnuli problémům s pravidly vyhledávačů.

Nakonec pro <priority></priority> , nastavíme základ 1.0 (maximální úroveň důležitosti). Pokud to chcete změnit, aby bylo konkrétnější, toto číslo může být cokoli mezi 0.0 a 1.0 s 0.0 je nedůležité, 1.0 je nejdůležitější.

I když to teď technicky nevypadá, když navštívíme http://localhost:5000/sitemap.xml v našem prohlížeči (za předpokladu, že pracujete s CheatCode Next.js Boilerplate a spustili jste dev server dříve), měli bychom vidět mapu webu obsahující naše statické stránky!

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

Dále se podívejme na rozšíření našeho souboru Sitemap načtením našich dynamických stránek pomocí GraphQL.

Generování dynamických dat pro naši mapu stránek

Protože pro náš příklad používáme CheatCode Next.js Boilerplate, již máme kabeláž potřebnou pro klienta GraphQL. Abychom naši práci uvedli do kontextu, použijeme tuto funkci ve spojení s CheatCode Node.js Boilerplate, která obsahuje ukázkovou databázi využívající MongoDB, plně implementovaný server GraphQL a ukázkovou kolekci dokumentů, kterou můžeme použít k získání testovacích dat. od.

Nejprve naklonujme kopii souboru Node.js Boilerplate a nastavíme jej:

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

A pak cd do klonovaného projektu a nainstalujte všechny závislosti:

cd nodejs-server-boilerplate && npm install

Nakonec pokračujte a spusťte server pomocí (z kořenového adresáře projektu):

npm run dev

Pokud budete pokračovat a otevřete projekt, přidáme trochu kódu, abychom do databáze přidali nějaké dokumenty, abychom měli skutečně co načíst pro naši mapu webu:

/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;
    }
  }
};

Nejprve musíme vytvořit soubor, do kterého bude uloženo zařízení (přezdívka pro kód, který pro nás generuje testovací data), který nám vygeneruje naše testovací dokumenty. Za tímto účelem exportujeme funkci, která dělá několik věcí:

  1. Načte testovacího uživatele (vytvořeného přiloženým /api/fixtures/users.js příslušenství, které je součástí základní desky).
  2. Načte existující .count() dokumentů v databázi.
  3. Spustí while smyčka říct "zatímco číslo existingDocuments v databázi je menší než 100 , vložte dokument."

Pro obsah dokumentu vygenerujeme název, který využívá aktuální i iterace smyčky plus jedna pro vygenerování jiného názvu pro každý vygenerovaný dokument. Dále zavoláme Documents.insertOne() funkce, kterou poskytuje náš import Documents kolekce (již implementováno v základním kódu) na .insertOne() dokument.

Tento dokument obsahuje _id nastavit na hex řetězec pomocí přiloženého generateId() funkce v kotli. Dále nastavíme title , následovaný userId nastavte na _id z testUser jsme načetli a pak jsme spolu s createdAt nastavili nějaký fiktivní obsah a updatedAt časové razítko pro dobré měřítko (ty vstoupíme do hry v naší mapě webu příště).

/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();
};

Aby vše fungovalo, musíme stáhnout přiložený users příslušenství a náš nový documents funkce do /api/index.js soubor (tento soubor se nám automaticky načte při startu serveru). Protože naše svítidla jsou po importu exportována jako funkce ve funkci exportované z /api/index.js , voláme tyto funkce a ujistíme se, že await volání, abychom se vyhnuli závodům s našimi daty (nezapomeňte, že náš uživatel musí existovat, než se pokusíme vytvořit dokumenty).

Než budeme pokračovat, musíme provést ještě jednu drobnou změnu, abychom zajistili, že budeme moci načíst dokumenty pro náš test:

/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();
  },
  [...]
};

Ve výchozím nastavení příklad documents resolvler v Node.js Boilerplate předá dotaz do Documents.find() metoda vyžadující zpět dokumenty pouze pro _id přihlášeného uživatele . Zde můžeme tento dotaz odstranit a pouze požádat o zpět všechny dokumenty, protože to právě testujeme.

To je vše na straně serveru. Vraťme se ke klientovi a připojte jej k našemu souboru Sitemap.

Načítání dat z našeho GraphQL API

Jak jsme viděli v minulé části, Node.js Boilerplate také obsahuje plně nakonfigurovaný server GraphQL a existující resolvery pro načítání dokumentů. Zpět v našem /pages/sitemap.xml.js soubor, stáhneme zahrnutého klienta GraphQL v Next.js Boilerplate a načteme některá data ze stávajícího documents resolver v GraphQL API:

/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;

Nahoře v souboru jsme importovali vzorový soubor dotazu GraphQL z /graphql/queries/Documents.gql soubor zahrnutý v CheatCode Next.js Boilerplate. Níže také importujeme zahrnutého klienta GraphQL z /graphql/client.js .

Zpět v našem getServerSideProps funkce, přidáme volání client.query() k provedení dotazu GraphQL pro naše dokumenty těsně pod naším dřívějším voláním, abychom získali naše staticPages . S naším seznamem v závěsu opakujeme stejný vzor, ​​který jsme viděli dříve, .map() přes documents našli jsme a použili stejnou strukturu XML, jako jsme použili u našich statických stránek.

Velký rozdíl je v tom, že pro naše <loc></loc> , vytváříme naši adresu URL ručně uvnitř .map() , s využitím našeho stávajícího baseUrl hodnotu a připojení /documents/${_id} na něj, kde _id je jedinečné ID aktuálního dokumentu, na který mapujeme. Také jsme zaměnili vložené volání na new Date().toISOString() předán <lastmod></lastmod> s updatedAt časové razítko, které nastavíme v databázi.

A je to! Pokud navštívíte http://localhost:5000/sitemap.xml v prohlížeči byste měli vidět naše stávající statické stránky spolu s našimi dynamicky generovanými adresami URL dokumentů:

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

Odtud, jakmile bude váš web nasazen online, můžete odeslat svůj soubor Sitemap do vyhledávačů, jako je Google, abyste zajistili správné indexování a hodnocení vašeho webu.

Řešení problémů sestavení Next.js na Vercel

Pro vývojáře, kteří se pokoušejí zprovoznit výše uvedený kód na Vercelu, je třeba provést malou změnu ve volání fs.readdirSync() výše. Místo použití fs.readdirSync("pages") jak ukazujeme výše, budete muset upravit svůj kód, aby vypadal takto:

/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}`;
  });

Zde je změna to, co předáme fs.readdirSync() . V aplikaci Next.js nasazené Vercelem se změní cesta k adresáři vašich stránek. Přidáním podmíněné cesty, jak vidíme výše, zajistíte, že při spuštění kódu souboru Sitemap převede stránky na správnou cestu (v tomto případě na /build/server/pages adresář vygenerovaný, když Vercel vytváří vaši aplikaci).

Zabalení

V tomto tutoriálu jsme se naučili, jak dynamicky generovat mapu webu pomocí Next.js. Naučili jsme se používat getServerSideProps funkce v Next.js k únosu odpovědi na požadavky odeslané na /sitemap.xml stránku v naší aplikaci a vrátí řetězec XML, čímž vynutí Content-Type záhlaví bude text/xml simulovat vrácení .xml soubor.

Podívali jsme se také na generování některých testovacích dat v MongoDB pomocí Node.js a načítání těchto dat pro zahrnutí do našeho souboru Sitemap pomocí dotazu GraphQL.