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;Hauptbestandteile des Broken Link Checker
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.

Cloudapp-dev und bevor Sie uns verlassen
Danke, dass Sie bis zum Ende gelesen haben. Noch eine Bitte bevor Sie gehen:



