Loading...
Sitemap-counter
Author Cloudapp
E.G.

Next.js 14 / SEO - Erstellen Sie einen Sitemap-Zähler in 5 Minuten

18. Juni 2024
Inhaltsverzeichnis

Die richtige SEO-Strategie ist für jeden Blog/jede Website entscheidend, und daher bildet eine aktuelle sitemap.xml die Grundlage für den Google Crawler. Wir werden eine einfache Funktion erstellen, um die sitemap.xml zu überprüfen und ihre Einträge zu zählen. Dies wird uns helfen zu überprüfen, ob alle Seiten vorhanden sind.

Verwendeter Stack

Ich werde mit meinem Standard-Stack beginnen:

  • Contentful als CMS

  • Next.js 14 als das Web-Framework

  • Vercel für das Hosting

Hier ist das GitHub repo mit dem vollständigen Code.

Beispielseite gehostet auf Vercel -> https://nextjs14-azureb2c-prisma.vercel.app/

Zwei neue XML NPM Pakete

Ich muss das Paket xml2js und die entsprechenden Typen @types/xml2js installieren, damit der Sitemap-Zähler funktioniert.

npm i @types/xml2js xml2js

Neue Lib Datei (Sitemapcounter.ts)

//src/lib/sitemapcounter.ts
import { parseStringPromise } from "xml2js";

export async function getSitemapEntries(url: string): Promise<number | null> {
  try {
    const response = await fetch(url);
    const xmlData = await response.text();
    const result = await parseStringPromise(xmlData);
    const entries = result.urlset.url.length;

    return entries;
  } catch (error) {
    console.error("Error fetching or parsing sitemap:", error);
    return null;
  }
}

Sobald die Lib-Datei vorhanden ist, erstelle ich eine neue API-Route für die Hauptlogik.

Neue API Route für das Zahlen der URL's

//src/app/api/sitemapcounter
import { NextRequest, NextResponse } from "next/server";
import { getSitemapEntries } from "@/lib/sitemapcounter";

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const url = searchParams.get("url");

  if (!url) {
    return NextResponse.json({ error: "URL is required" }, { status: 400 });
  }

  const entries = await getSitemapEntries(url);

  if (entries !== null) {
    return NextResponse.json({ entries: entries || 0 });
  } else {
    return NextResponse.json(
      { error: "Failed to fetch or parse sitemap" },
      { status: 500 }
    );
  }

  // Alternatively, you could return a 202 Accepted status code

  return new NextResponse(null, { status: 202 });
}

Neue Komponente zum Abrufen der Sitemap-URL

Da ich Internationalisierung verwende, muss ich die common.json-Dateien unter src/app/i18n/locales/de-DE - en-US anpassen, damit alle übersetzten Labels wie erwartet funktionieren.

//src/components/tools/counter.component.tsx
"use client";
import { useState } from "react";
import type { LocaleTypes } from "@/app/i18n/settings";
import { useTranslation } from "@/app/i18n/client";
import { useParams } from "next/navigation";
import { Button } from "@tremor/react";

export default function Sitemapcounter() {
  const locale = useParams()?.locale as LocaleTypes;
  const { t } = useTranslation(locale, "common");
  const [url, setUrl] = useState<string>("");
  const [entries, setEntries] = useState<number | null>(null);
  const [error, setError] = useState<string | null>(null);

  const fetchEntries = async () => {
    try {
      const res = await fetch(
        `/api/sitemapcounter?url=${encodeURIComponent(url)}`
      );
      const data = await res.json();
      if (res.ok) {
        setEntries(data.entries);
        setError(null);
      } else {
        setError(data.error);
        setEntries(null);
      }
    } catch (err) {
      setError("An unexpected error occurred");
      setEntries(null);
    }
  };

  return (
    <div>
      <h2 className="mb-4">Sitemap Entry Counter</h2>
      <input
        type="text"
        name="search"
        id="search"
        className="block w-full h-10 max-w-md border border-gray-200 rounded-md pl-2 focus:border-indigo-500 focus:ring-indigo-500 sm:text-base"
        placeholder={t("sitemapcounter.placeholder")}
        spellCheck={false}
        onChange={(e) => setUrl(e.target.value)}
      />
      <Button
        size="lg"
        variant="primary"
        className="mt-4 sm:flex sm:max-w-md"
        onClick={fetchEntries}
      >
        Count Entries
      </Button>

      {entries !== null && <p>Number of entries: {entries}</p>}
      {error && <p style={{ color: "red" }}>{error}</p>}
    </div>
  );
}

Komponente in Page.tsx integrieren

Als letzten Schritt füge ich die neue Komponente zu meiner page.tsx unter src/app/[locale] hinzu

import { client } from "@/lib/client";
import { notFound } from "next/navigation";
import { ArticleHero } from "@/components/contentful/ArticleHero";
import { ArticleTileGrid } from "@/components/contentful/ArticleTileGrid";
import { Container } from "@/components/contentful/container/Container";
import { draftMode } from "next/headers";
// Internationalization
import { locales, LocaleTypes } from "@/app/i18n/settings";
import { createTranslation } from "@/app/i18n/server";
//SEO - JSON-LD
import { Article, WithContext } from "schema-dts";
import path from "path";
import Script from "next/script";
import { Metadata, ResolvingMetadata } from "next";
// New Fields Part 9 of tutorial
import { PageBlogPostOrder } from "@/lib/__generated/sdk";
import { TextHighLight } from "@/components/contentful/TextHighLight";
import { revalidateDuration } from "@/utils/constants";
import { TagCloudSimpleHome } from "@/components/search/tagcloudsimpleHome.component";
import Link from "next/link";
import { LandingContent } from "@/components/contentful/ArticleContentLanding";

import Sitemapcounter from "@/components/tools/sitemapcounter/counter.component";

export const revalidate = revalidateDuration; // revalidate at most every hour

interface PageParams {
  slug: string;
  locale: string;
}

interface PageProps {
  params: PageParams;
}

const generateUrl = (locale: string, slug: string) => {
  if (locale === "en-US") {
    return new URL(slug, process.env.NEXT_PUBLIC_BASE_URL!).toString();
  } else {
    return new URL(locale, process.env.NEXT_PUBLIC_BASE_URL!).toString();
  }
};

const WebUrl = process.env.NEXT_PUBLIC_BASE_URL as string;

export async function generateMetadata(
  { params }: PageProps,
  parent: ResolvingMetadata
): Promise<Metadata> {
  const [PagedataSeo] = await Promise.all([
    client.pageLanding({
      slug: "/",
      locale: params.locale.toString(),
      preview: draftMode().isEnabled,
    }),
  ]);

  const landingPage = PagedataSeo.pageLandingCollection?.items[0];

  if (!landingPage) {
    return notFound();
  }

  const url = generateUrl(params.locale || "", "");

  return {
    title: landingPage.seoFields?.pageTitle,
    description: landingPage.seoFields?.pageDescription,
    metadataBase: new URL(WebUrl),
    alternates: {
      canonical: url,
      languages: {
        "en-US": "/",
        "de-DE": "/de-DE",
        "x-default": "/",
      },
    },
    openGraph: {
      type: "website",
      siteName: "Example.dev - Free Tutorials and Resources for Developers",
      locale: params.locale,
      url: url || "",

      title: landingPage.seoFields?.pageTitle || undefined,
      description: landingPage.seoFields?.pageDescription || undefined,
      images: landingPage.seoFields?.shareImagesCollection?.items.map(
        (item) => ({
          url: item?.url || "",
          width: item?.width || 0,
          height: item?.height || 0,
          alt: item?.description || "",
          type: item?.contentType || "",
        })
      ),
    },
    robots: {
      follow: landingPage.seoFields?.follow || false,
      index: landingPage.seoFields?.index || false,
      googleBot: {
        index: true,
        follow: true,
        "max-image-preview": "large",
      },
    },
  };
}

async function Home({ params }: PageProps) {
  const { isEnabled } = draftMode();
  //declare JSON-LD schema
  let jsonLd: WithContext<Article> = {} as WithContext<Article>;
  const [landingPageData] = await Promise.all([
    client.pageLanding({
      slug: "/",
      locale: params.locale.toString(),
      preview: isEnabled,
    }),
  ]);

  const page = landingPageData.pageLandingCollection?.items[0];

  if (!page) {
    // If a blog post can't be found,
    // tell Next.js to render a 404 page.
    return notFound();
  }

  // TagCloud
  const showTagCloud = page.showTagCloud === "Yes";

  let { datanew, minSize, maxSize } = {
    datanew: [],
    minSize: 0,
    maxSize: 0,
  };

  if (showTagCloud) {
    const searchFacets = await fetch(
      `${process.env.NEXT_PUBLIC_BASE_URL}/api/search/facets`,
      {}
    )
      .then((res) => res.json())
      .catch((error) => {
        console.log("No data found");
      });

    if (searchFacets) {
      maxSize = searchFacets.maxSize;
      minSize = searchFacets.minSize;
      datanew = searchFacets.datanew;
    }
  }

  // Getting BlogPosts
  const blogPostsData = await client.pageBlogPostCollection({
    limit: 10,
    locale: params.locale.toString(),
    skip: 0,
    preview: isEnabled,
    order: PageBlogPostOrder.PublishedDateDesc,
    where: {
      slug_not: page?.featuredBlogPost?.slug,
    },
  });
  const posts = blogPostsData.pageBlogPostCollection?.items;

  // Create JSON-LD schema only if blogPost is available
  if (page) {
    jsonLd = {
      "@context": "https://schema.org",
      "@type": "Article",
      headline: page?.seoFields?.pageTitle || undefined,
      author: {
        "@type": "Person",
        name: page?.featuredBlogPost?.author?.name || undefined,
        // The full URL must be provided, including the website's domain.
        url: new URL(
          path.join(
            params.locale.toString() || "",
            params.slug?.toString() || ""
          ),
          process.env.NEXT_PUBLIC_BASE_URL!
        ).toString(),
      },
      publisher: {
        "@type": "Organization",
        name: "Example.dev - Free Tutorials and Resources for Developers",
        logo: {
          "@type": "ImageObject",
          url: "https://www.example.dev/favicons/icon-192x192.png",
        },
      },
      image: page?.featuredBlogPost?.featuredImage?.url || undefined,
      datePublished: page.sys.firstPublishedAt,
      dateModified: page.sys.publishedAt,
    };
  }

  // Internationalization, get the translation function
  const { t } = await createTranslation(params.locale as LocaleTypes, "common");

  const highLightHeadings: any = page.textHighlightCollection?.items[0];

  if (!page?.featuredBlogPost || !posts) return;

  return (
    <>
      {page && (
        <Script
          id="article-schema"
          type="application/ld+json"
          dangerouslySetInnerHTML={{
            __html: JSON.stringify(jsonLd),
          }}
        />
      )}
      <Container className="mt-5">
        {highLightHeadings && <TextHighLight headings={highLightHeadings} />}

        {showTagCloud && datanew.length > 0 && (
          <TagCloudSimpleHome
            datanew={datanew}
            minSize={minSize * 10}
            maxSize={maxSize * 5}
            locale={params.locale.toString()}
            source={"homepage"}
          />
        )}

        <Link
          href={`/${params.locale.toString()}/${page.featuredBlogPost.slug}`}
        >
          <ArticleHero article={page.featuredBlogPost} isHomePage={true} />
        </Link>
        <div className="md:mx-24 md:my-24 sm:mx-16 sm:my-16">
          <LandingContent landing={page} />
        </div>
      </Container>

      <Container className="my-8 md:mb-10 lg:mb-16">
        {posts.length > 0 && (
          <h2 className="mb-4 md:mb-6">{t("landingPage.latestArticles")}</h2>
        )}
        <ArticleTileGrid
          className="grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
          articles={posts}
          slug={page.featuredBlogPost.slug}
          source="loadmore"
          locale={params.locale.toString()}
        />
         {/* SitemapCounter */}
        <Sitemapcounter />
      </Container>
    </>
  );
}

export default Home;

Endergebnis

Und los geht's. Unten können Sie den neuen Sitemap-Zähler auf der Homepage sehen.

Sitemap-Counter-Frontend
Sitemap-Counter-Frontend

Cloudapp-dev und bevor Sie uns verlassen

Danke, dass Sie bis zum Ende gelesen haben. Noch eine Bitte bevor Sie gehen:

Wenn Ihnen gefallen hat was Sie gelesen haben oder wenn es Ihnen sogar geholfen hat, dann würden wir uns über einen "Clap" 👏 oder einen neuen Follower auf unseren Medium Account sehr freuen.

Oder folgen Sie uns auf Twitter -> Cloudapp.dev

Verwandte Artikel