Add auto-updating sitemap.xml and robots.txt
Sitemap combines hardcoded static routes with Payload's Pages and published Posts collections (hourly ISR revalidation), so new CMS pages/blog posts appear without a code change. Excludes noindex transactional routes (checkout, cart, bestellbestaetigung, newsletter-confirmed, konto/*) to avoid Search Console warnings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Eg6h91yngXmSnM51wxXM8
This commit is contained in:
@@ -1518,3 +1518,46 @@ export async function getPageBySlug(slug: string, options?: { draft?: boolean })
|
||||
if (!doc) return null;
|
||||
return mapPayloadPage(doc);
|
||||
}
|
||||
|
||||
export type SitemapEntry = { slug: string; updatedAt: string };
|
||||
|
||||
// Backs app/sitemap.ts. Pages has no status/draft field (see Pages.ts) —
|
||||
// every doc that exists is live — so unlike getBlogPosts there's no
|
||||
// `where[status]` filter needed here.
|
||||
export async function getAllPageSlugs(): Promise<SitemapEntry[]> {
|
||||
const params = new URLSearchParams({
|
||||
"where[tenant.slug][equals]": TENANT_SLUG,
|
||||
depth: "0",
|
||||
limit: "100",
|
||||
});
|
||||
|
||||
const res = await fetch(`${PAYLOAD_URL}/api/pages?${params}`, { next: { revalidate: 3600 } });
|
||||
if (!res.ok) {
|
||||
console.error(`getAllPageSlugs: Payload returned ${res.status} ${res.statusText}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const data: { docs?: { slug: string; updatedAt: string }[] } = await res.json();
|
||||
return (data.docs ?? []).map((doc) => ({ slug: doc.slug, updatedAt: doc.updatedAt }));
|
||||
}
|
||||
|
||||
// Same shape as getAllPageSlugs but for Posts, which — unlike Pages — does
|
||||
// have a draft/scheduled state, so this filters to `published` the same
|
||||
// way getBlogPosts does (the sitemap must never list a post that 404s).
|
||||
export async function getAllPostSlugs(): Promise<SitemapEntry[]> {
|
||||
const params = new URLSearchParams({
|
||||
"where[tenant.slug][equals]": TENANT_SLUG,
|
||||
"where[status][equals]": "published",
|
||||
depth: "0",
|
||||
limit: "100",
|
||||
});
|
||||
|
||||
const res = await fetch(`${PAYLOAD_URL}/api/posts?${params}`, { next: { revalidate: 3600 } });
|
||||
if (!res.ok) {
|
||||
console.error(`getAllPostSlugs: Payload returned ${res.status} ${res.statusText}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const data: { docs?: { slug: string; updatedAt: string }[] } = await res.json();
|
||||
return (data.docs ?? []).map((doc) => ({ slug: doc.slug, updatedAt: doc.updatedAt }));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
const SITE_URL = "https://einfach-produktiv.mk360.de";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
disallow: ["/checkout", "/cart", "/konto", "/bestellbestaetigung", "/newsletter-confirmed", "/email-preview", "/company-settings-preview", "/api"],
|
||||
},
|
||||
],
|
||||
sitemap: `${SITE_URL}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
import { getAllPageSlugs, getAllPostSlugs } from "./lib/payload";
|
||||
|
||||
const SITE_URL = "https://einfach-produktiv.mk360.de";
|
||||
|
||||
// Every indexable static route in this repo. Deliberately excludes:
|
||||
// - transactional/session pages already marked `robots: noindex` in their
|
||||
// own page.tsx (checkout, checkout/verarbeitung, cart, bestellbestaetigung,
|
||||
// newsletter-confirmed, company-settings-preview, /konto/*) — Google
|
||||
// explicitly advises against listing noindex URLs in a sitemap.
|
||||
// - /sticker/[code] and /r/[code]: redirect-only routes with no content of
|
||||
// their own (see resolveAndTrackRedirect), not pages to index.
|
||||
// - /email-preview/[type]: internal template-preview tool, not customer-facing.
|
||||
// - /blog/[slug] and /[slug] (Payload Pages): appended dynamically below.
|
||||
const STATIC_ROUTES = [
|
||||
"",
|
||||
"/blog",
|
||||
"/shop",
|
||||
"/newsletter",
|
||||
"/todo-cards",
|
||||
"/der-eine",
|
||||
"/tasse-die-pause",
|
||||
"/die-sieben",
|
||||
"/einfach-anfangen",
|
||||
"/7-tage-klarheits-check",
|
||||
"/sticker",
|
||||
"/agb",
|
||||
"/datenschutz",
|
||||
"/impressum",
|
||||
"/versand",
|
||||
"/widerruf",
|
||||
];
|
||||
|
||||
// New routes only ever need adding in one of two places from now on: a
|
||||
// plain static page.tsx goes in STATIC_ROUTES above; anything authored
|
||||
// through Payload (a Pages doc or a blog Post) shows up here automatically
|
||||
// the next time this regenerates — no code change needed for those.
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const [pages, posts] = await Promise.all([getAllPageSlugs(), getAllPostSlugs()]);
|
||||
|
||||
const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((path) => ({
|
||||
url: `${SITE_URL}${path}`,
|
||||
changeFrequency: path === "" ? "daily" : "weekly",
|
||||
priority: path === "" ? 1 : 0.7,
|
||||
}));
|
||||
|
||||
const pageEntries: MetadataRoute.Sitemap = pages.map(({ slug, updatedAt }) => ({
|
||||
url: `${SITE_URL}/${slug}`,
|
||||
lastModified: updatedAt,
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.6,
|
||||
}));
|
||||
|
||||
const postEntries: MetadataRoute.Sitemap = posts.map(({ slug, updatedAt }) => ({
|
||||
url: `${SITE_URL}/blog/${slug}`,
|
||||
lastModified: updatedAt,
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.5,
|
||||
}));
|
||||
|
||||
return [...staticEntries, ...pageEntries, ...postEntries];
|
||||
}
|
||||
Reference in New Issue
Block a user