Inhaltsverzeichnis
- Warum ist das XML-Sitemap nicht ausreichend?
- Verwendeter Stack (Next.js 14, Contentful, Tailwind)
- Contentful Projekt — Inhaltstypen
- GraphQL-Abfrage
- Neue Page Route — Sitemap-html
- Neue Typescript Typen
- Anpassung Common.json für Mehrsprachigkeit
- Hier ist das Endergebnis
- Der Footer Link
- Zusammenfassung
- Cloudapp-dev und bevor Sie uns verlassen
Ein HTML-Sitemap ist eine Webseite, die alle anderen Seiten einer Website auflistet und verlinkt, wodurch eine klare und organisierte Übersicht über deren Struktur geboten wird. Es verbessert die Benutzererfahrung, indem es Besuchern hilft, schnell die gesuchten Informationen zu finden, insbesondere auf großen Websites mit komplexer Navigation. Suchmaschinen profitieren von HTML-Sitemaps, da sie sicherstellen, dass alle Seiten effizient entdeckt und indexiert werden.
Warum ist das XML-Sitemap nicht ausreichend?
Im Gegensatz zu XML-Sitemaps, die hauptsächlich für Suchmaschinen entwickelt wurden, sind HTML-Sitemaps für menschliche Benutzer konzipiert. Sie können die SEO einer Website verbessern, indem sie die Seitenautorität durch interne Links verteilen. Darüber hinaus unterstützen HTML-Sitemaps die Barrierefreiheit, indem sie Benutzern mit Behinderungen helfen, die Website einfacher zu navigieren.
Hier ist das GitHub repo mit dem vollständigen Code.
Beispielseite -> https://nextjs14-azureb2c-prisma.vercel.app/
Wenn Sie meinen vorherigen Beitrag zu Next.js 14 und SEO gelesen haben, kennen Sie vielleicht bereits die Grundlagen der wichtigsten SEO-Themen, um mit Ihrem Next.js 14-Projekt erfolgreich zu sein.
Verwendeter Stack (Next.js 14, Contentful, Tailwind)
In all meinen Beispielen habe ich Contentful als CMS, Next.js 14 als Framework und Tailwind CSS für das Styling verwendet. Das Deployment erfolgte auf Vercel. Nun werde ich eine HTML-Sitemap erstellen, was ziemlich einfach ist. Wir haben bereits das Fundament gebaut, und mit der großartigen GraphQL-API von Contentful macht es wirklich Spaß, dies zu implementieren.
Contentful Projekt — Inhaltstypen
Ich konzentriere mich auf die drei Hauptinhaltstypen, die ich in meinem Contentful-Bereich habe.
Landing Pages bzw. Hauptseiten (pageLandingCollection)
Tag Seiten bzw. Kategorie Seiten (tagPageCollection)
Blog Post Seiten (pageBlogPostCollection)
GraphQL-Abfrage
Ich starte mit der Erstellung der entsprechenden GraphQL Abfrage
fragment sitemapPagesFields on Query {
pageBlogPostCollection(limit: 100, locale: $locale) {
items {
internalName
slug
sys {
publishedAt
}
}
}
pageLandingCollection(limit: 50, locale: $locale) {
items {
internalName
slug
sys {
publishedAt
}
}
}
tagPageCollection(limit: 50, locale: $locale) {
items {
internalName
tag
sys {
publishedAt
}
}
}
}
query sitemapPages($locale: String!) {
...sitemapPagesFields
}Nach der Änderung müssen wir den folgenden Befehl ausführen, um die Schemas und Typen zu aktualisieren (die Datei src/lib/__generated/sdk.ts wird aktualisiert).
npm run graphql-codegen:generateNeue Page Route — Sitemap-html
Zur Ausgabe im Frontend, erstellen wir eine neue page.tsx unter src/app/[locale]/sitemap-html.
//src/app/[locale]/sitemap-html
import { client } from "@/lib/client";
import { draftMode } from "next/headers";
import { Metadata, ResolvingMetadata } from "next";
import { LandingContent } from "@/components/contentful/ArticleContentLanding";
import { notFound } from "next/navigation";
import { Container } from "@/components/contentful/container/Container";
import { TextHighLight } from "@/components/contentful/TextHighLight";
// Internationalization
import { LocaleTypes } from "@/app/i18n/settings";
import { createTranslation } from "@/app/i18n/server";
//SEO - JSON-LD
import { Article, WithContext } from "schema-dts";
import Script from "next/script";
import path from "path";
import Link from "next/link";
//GraphQL Types
import { PageLandingFieldsFragment } from "@/lib/__generated/sdk";
import { PageBlogPostFieldsFragment } from "@/lib/__generated/sdk";
import { TagPageFieldsFragment } from "@/lib/__generated/sdk";
import { TextHighlightFieldsFragment } from "@/lib/__generated/sdk";
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 + "/" + slug,
process.env.NEXT_PUBLIC_BASE_URL!
).toString();
}
};
export async function generateMetadata(
{ params }: PageProps,
parent: ResolvingMetadata
): Promise<Metadata> {
const [PagedataSeo] = await Promise.all([
client.pageLanding({
slug: "sitemap-html",
locale: params.locale.toString(),
preview: draftMode().isEnabled,
}),
]);
const landingPage = PagedataSeo.pageLandingCollection?.items[0];
if (!landingPage) {
return notFound();
}
const url = generateUrl(params.locale || "", params.slug || "");
const WebUrl = process.env.NEXT_PUBLIC_BASE_URL as string;
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 Sitemap_html({ params }: PageProps) {
const { isEnabled } = draftMode();
//declare JSON-LD schema
let jsonLd: WithContext<Article> = {} as WithContext<Article>;
const [landingPageData, PagesSitemapHtml] = await Promise.all([
client.pageLanding({
slug: "sitemap-html",
locale: params.locale.toString(),
preview: isEnabled,
}),
client.sitemapPages({
locale: params.locale.toString(),
}),
]);
const page = landingPageData.pageLandingCollection?.items[0];
const seoItem = page?.seoFields?.shareImagesCollection?.items[0];
if (!page) {
// If a blog post can't be found,
// tell Next.js to render a 404 page.
return notFound();
}
// 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: seoItem?.url || undefined,
datePublished: page.sys.firstPublishedAt,
dateModified: page.sys.publishedAt,
};
}
const highLightHeadings: TextHighlightFieldsFragment | undefined | null =
page.textHighlightCollection?.items[0];
const sitemapPageLandingUrls: PageLandingFieldsFragment | any =
PagesSitemapHtml?.pageLandingCollection?.items;
const sitemapPageTagpageUrls: TagPageFieldsFragment | any =
PagesSitemapHtml?.tagPageCollection?.items;
const sitemapPageBlogPostUrls: PageBlogPostFieldsFragment | any =
PagesSitemapHtml?.pageBlogPostCollection?.items;
// Internationalization, get the translation function
const { t } = await createTranslation(params.locale as LocaleTypes, "common");
return (
<>
{page && (
<Script
id="article-schema"
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd),
}}
/>
)}
<Container className="mt-5">
{highLightHeadings && <TextHighLight headings={highLightHeadings} />}
<LandingContent landing={page} />
<div className="mx-auto max-w-8xl mt-5 text-base">
<div className="text-2xl font-bold mb-2">
{" "}
{t("sitemaphtml.landingpages")}
</div>
{sitemapPageLandingUrls.map((field: any, index: number) => {
return field ? (
<div key={index}>
<Link href={field.slug}>{field.internalName}</Link>
</div>
) : null;
})}
<div className="text-2xl font-bold mb-2">
{t("sitemaphtml.tagpages")}
</div>
{sitemapPageTagpageUrls.map((field: any, index: number) => {
return field ? (
<div key={index}>
<Link href={field.tag}>{field.internalName}</Link>
</div>
) : null;
})}
<div className="text-2xl font-bold mb-2">
{" "}
{t("sitemaphtml.blogpostpages")}
</div>
{sitemapPageBlogPostUrls.map((field: any, index: number) => {
return field ? (
<div key={index}>
<Link href={field.slug}>{field.internalName}</Link>
</div>
) : null;
})}
</div>
</Container>
</>
);
}
export default Sitemap_html;Neue Typescript Typen
Am Anfang der Datei werden die entsprechenden Typen importiert.
//GraphQL Types
import { PageLandingFieldsFragment } from "@/lib/__generated/sdk";
import { PageBlogPostFieldsFragment } from "@/lib/__generated/sdk";
import { TagPageFieldsFragment } from "@/lib/__generated/sdk";
import { TextHighlightFieldsFragment } from "@/lib/__generated/sdk";Hier wird die Abfrage ausgeführt
const [landingPageData, PagesSitemapHtml] = await Promise.all([
client.pageLanding({
slug: "sitemap-html",
locale: params.locale.toString(),
preview: isEnabled,
}),
client.sitemapPages({
locale: params.locale.toString(),
}),
]);Hier werden die Variablen erstellt bzw. deklariert.
const sitemapPageLandingUrls: PageLandingFieldsFragment | any =
PagesSitemapHtml?.pageLandingCollection?.items;
const sitemapPageTagpageUrls: TagPageFieldsFragment | any =
PagesSitemapHtml?.tagPageCollection?.items;
const sitemapPageBlogPostUrls: PageBlogPostFieldsFragment | any =
PagesSitemapHtml?.pageBlogPostCollection?.items;Ausgabe der Links im Frontend der Seite.
<div className="mx-auto max-w-8xl mt-5 text-base">
<div className="text-2xl font-bold mb-2">
{" "}
{t("sitemaphtml.landingpages")}
</div>
{sitemapPageLandingUrls.map((field: any, index: number) => {
return field ? (
<div key={index}>
<Link href={field.slug}>{field.internalName}</Link>
</div>
) : null;
})}
<div className="text-2xl font-bold mb-2">
{t("sitemaphtml.tagpages")}
</div>
{sitemapPageTagpageUrls.map((field: any, index: number) => {
return field ? (
<div key={index}>
<Link href={field.tag}>{field.internalName}</Link>
</div>
) : null;
})}
<div className="text-2xl font-bold mb-2">
{" "}
{t("sitemaphtml.blogpostpages")}
</div>
{sitemapPageBlogPostUrls.map((field: any, index: number) => {
return field ? (
<div key={index}>
<Link href={field.slug}>{field.internalName}</Link>
</div>
) : null;
})}
</div>Anpassung Common.json für Mehrsprachigkeit
Um die Mehrsprachigkeit innerhalb des Nextjs 14 Projekts zu nutzen, passe ich meine common.json-Dateien für DE und US unter src/app/[locale]/de-DE und en-US an.
US
"sitemaphtml": {
"landingpages": "Landing pages",
"blogpostpages": "Blog Post Pages",
"tagpages": "Tag - Category Pages"
},DE
"sitemaphtml": {
"landingpages": "Hauptseiten",
"blogpostpages": "Blogseiten",
"tagpages": "Kategorieseiten"
},Hier ist das Endergebnis

Der Footer Link

Zusammenfassung
Jede Seite sollte aus mehreren Gründen eine HTML-Sitemap haben:
1. Verbesserte Navigation für Benutzer: Eine HTML-Sitemap bietet einen klaren Überblick über die gesamte Website und ermöglicht es den Benutzern, den gesuchten Inhalt leicht zu finden. Dies kann die Benutzererfahrung verbessern, insbesondere auf großen Websites mit komplexen Strukturen.
2. Besseres SEO: Suchmaschinen verwenden Sitemaps, um die Struktur einer Website zu verstehen und deren Seiten effektiver zu indexieren. Eine HTML-Sitemap kann Suchmaschinen helfen, alle Seiten einer Website zu entdecken, was die Sichtbarkeit und das Ranking der Website in den Suchergebnissen verbessern kann.
3. Schneller Zugriff auf alle Seiten: Benutzer und Suchmaschinen können über die HTML-Sitemap schnell auf jede Seite der Website zugreifen, was besonders nützlich ist, um tief verlinkte oder weniger häufig aufgerufene Seiten zu finden.
4. Verbesserte interne Verlinkung: Eine HTML-Sitemap erstellt zusätzliche interne Links zu allen Seiten der Website. Dies kann die allgemeine Linkstruktur verbessern, die Link-Energie gleichmäßiger verteilen und den SEO-Wert der Website steigern.
5. Barrierefreiheit: Eine HTML-Sitemap kann die Zugänglichkeit der Website verbessern, indem sie eine einfache, textbasierte Navigationsoption bietet. Dies kann besonders nützlich für Benutzer mit Behinderungen sein, die auf Bildschirmlesegeräte angewiesen sind.
6. Inhaltserkennung: Besucher können leicht die gesamte Bandbreite an Inhalten auf der Website entdecken, was sie dazu ermutigen kann, mehr Seiten zu erkunden und mehr Zeit auf der Website zu verbringen.
7. Fehlererkennung: Durch die regelmäßige Überprüfung der HTML-Sitemap können Website-Administratoren schnell fehlerhafte Links oder Seiten identifizieren und korrigieren, die möglicherweise versehentlich aus der Hauptnavigation der Website ausgelassen wurden.
Das war's, nun haben wir eine HTML Sitemap erstellt, welche direkt im Footer verlinkt ist. Wie man gesehen hat, ist dies mit der Kombi aus Contentful und Nextjs 14 sehr einfach.
Cloudapp-dev und bevor Sie uns verlassen
Danke, dass Sie bis zum Ende gelesen haben. Noch eine Bitte bevor Sie gehen:



