Loading...
Broken-link-check
Author Cloudapp
E.G.

Next.js 14 - Erstellen eines Broken Link Checker basierend auf der sitemap.xml

20. Juni 2024
Inhaltsverzeichnis

Ein Prüfung für defekte Links ist entscheidend für die Benutzerfreundlichkeit und das Ranking der Seite. Es hilft dabei, tote Links zu identifizieren und zu beheben. Es hilft dabei, tote Links zu identifizieren und zu beheben, die Nutzer frustrieren und sich negativ auf das SEO-Ranking auswirken können. Die regelmäßige Überprüfung auf defekte Links stellt sicher, dass alle Seiten zugänglich sind und die Glaubwürdigkeit und Funktionalität der Website erhalten bleibt. Außerdem hilft es, veraltete oder verschobene Inhalte zu identifizieren, die aktualisiert oder umgeleitet werden müssen. Letztendlich unterstützt es die Gesamtleistung der Website und die Zufriedenheit der Nutzer.

Verwendeter Stack

Ich werde mit meinem Standard-Stack beginnen:

  • Contentful als CMS als Grundlage für die Erstellung der sitemap.xml

  • Next.js 14 als das Web-Framework (Ich verwende die API Routes für die Logic)

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

Keine neuen NPM-Pakete erforderlich

Da wir uns auf die vorherigen Post stützen, in der ich einen Sitemap-Zähler implementiert habe, ist es nicht notwendig, neue Pakete zu installieren, damit der "Broken Link Checker" wie erwartet funktioniert.

Neue API-Route für das Auslesen der Sitemap.xml

Beginnen wir mit der neuen API-Route zum Lesen der sitemap.xml und testen dann jede URL.

//src/app/api/sitemaplinkcheck/route.ts
import { NextRequest, NextResponse } from "next/server";
import { parseStringPromise } from "xml2js";

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 broken: string[] = [];

  try {
    const response = await fetch(url as string);
    const xmlData = await response.text();
    const result = await parseStringPromise(xmlData);
    const urls = result.urlset.url.map((url: any) => url.loc[0]);

    let data: Response;

    for (const url of urls) {
      try {
        data = await fetch(url);
        // If needed you can check the status code of the response

        // if (data.status !== 200) {
        //   broken.push(url as string);
        // }
      } catch (error) {
        console.error("Error fetching the sitemap:", error);
        broken.push(url as string);
      }
    }
  } catch (error) {
    console.error("Error fetching the sitemap:", error);
  } finally {
  }

  if (broken !== null) {
    return NextResponse.json({ entries: broken || 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 SitemapChecker-Komponente

Dieses Mal ist die gesamte Logik in einer neuen Komponente untergebracht.

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

const SitemapChecker: React.FC = () => {
  const [brokenLinks, setBrokenLinks] = useState<string[]>([]);
  const [loading, setLoading] = useState(false);
  const [url, setUrl] = useState<string>("");

  const locale = useParams()?.locale as LocaleTypes;
  const { t } = useTranslation(locale, "common");

  const checkLinks = async () => {
    setLoading(true);

    try {
      const res = await fetch(
        `/api/sitemaplinkcheck?url=${encodeURIComponent(url)}`
      );
      const data = await res.json();
      if (res.ok) {
        setBrokenLinks(data.entries);
      } else {
        setBrokenLinks(data.error);
      }
    } catch (err) {
      console.error("An unexpected error occurred", err);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="rounded-md mt-4">
      <h2 className="text-2xl font-bold mb-4">Broken Link Checker</h2>
      <input
        type="text"
        value={url}
        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"
        className="mt-4 sm:flex sm:max-w-md"
        onClick={checkLinks}
        disabled={!url}
      >
        {loading ? "Checking..." : "Check Links"}
      </Button>
      {brokenLinks.length > 0 && (
        <div className="mt-4">
          <h3 className="text-xl font-semibold mb-2">Broken Links:</h3>
          <ul className="list-disc list-inside">
            {brokenLinks.map((link, index) => (
              <li key={index} className="text-red-500">
                {link}
              </li>
            ))}
          </ul>
        </div>
      )}

      {brokenLinks.length === 0 && url !== "" && !loading && (
        <p className="mt-4 text-green-500">No broken links found!</p>
      )}
    </div>
  );
};

export default SitemapChecker;

Wir haben ein einfaches Eingabefeld für die Sitemap-URL und einen Button zum Auslösen der Prüfung. Unten sehen Sie die Logik zum Abrufen der sitemap.xml, zum Überprüfen der Einträge und zum Übertragen von Fehlern in die entsprechende Variable.

const checkLinks = async () => {
    setLoading(true);

    try {
      const response = await fetch(url);
      const xmlData = await response.text();
      const result = await parseStringPromise(xmlData);
      const urls = result.urlset.url.map((url: any) => url.loc[0]);

      const broken: string[] = [];

      for (const url of urls) {
        try {
          await fetch(url as string);
        } catch (error) {
          console.error("Error fetching the sitemap:", error);
          broken.push(url as string);
        }
      }

      setBrokenLinks(broken);
    } catch (error) {
      console.error("Error fetching the sitemap:", error);
    } finally {
      setLoading(false);
    }
  };

Die Prüfung ist recht einfach, aber sie ist eine gute Grundlage für weitere Anpassungen (Status, Zeitangaben usw.).

Integration der Komponente in Page.tsx

Als letzten Schritt habe ich die neue Komponente zur page.tsx unter src/app/[locale] hinzugefügt

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";
import SitemapChecker from "@/components/tools/sitemapchecker/sitemapchecker.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 />
        {/* SitemapChecker */}
        <SitemapChecker />      
      </Container>
    </>
  );
}

export default Home;

Endergebnis

Und los geht's. Unten sehen Sie den neuen Sitemap-Zähler aus dem vorigen Beitrag und darunter die neue Komponente für den Broken Link Check auf der Homepage.

Component-broken-link
Component-broken-link

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