From b60f8a92e126a20549067307cbf94dfce1374342 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 23:01:49 +0100 Subject: [PATCH] Add sitemap.xml and robots.txt List the home page, blog posts and events in both languages with hreflang alternates, and point crawlers to the sitemap. Co-Authored-By: Claude Opus 5.5 --- src/app/robots.ts | 14 ++++++++++++++ src/app/sitemap.ts | 39 +++++++++++++++++++++++++++++++++++++++ src/components/seo.tsx | 12 +++++++----- src/lib/sanity.ts | 30 ++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 src/app/robots.ts create mode 100644 src/app/sitemap.ts diff --git a/src/app/robots.ts b/src/app/robots.ts new file mode 100644 index 0000000..e7fff1c --- /dev/null +++ b/src/app/robots.ts @@ -0,0 +1,14 @@ +import { SITE_URL } from "@/components/seo" + +import type { MetadataRoute } from "next" + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: "*", + allow: "/", + disallow: "/api/", + }, + sitemap: `${SITE_URL}/sitemap.xml`, + } +} diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts new file mode 100644 index 0000000..c1f4874 --- /dev/null +++ b/src/app/sitemap.ts @@ -0,0 +1,39 @@ +import { getLocalizedPath, SITE_URL } from "@/components/seo" +import { routing } from "@/i18n/routing" +import { getSitemapDocuments } from "@/lib/sanity" + +import type { MetadataRoute } from "next" + +export const revalidate = 3600 // 1 hour + +export default async function sitemap(): Promise { + const { posts, events } = await getSitemapDocuments() + + const pages: { path: string; lastModified?: string }[] = [ + { path: "/" }, + ...posts.map(post => ({ + path: `/blog/${post.slug}`, + lastModified: post._updatedAt, + })), + ...events.map(event => ({ + path: `/event/${event.slug}`, + lastModified: event._updatedAt, + })), + ] + + // One entry per language version, each linking to the other versions + return pages.flatMap(({ path, lastModified }) => { + const languages = Object.fromEntries( + routing.locales.map(locale => [ + locale, + `${SITE_URL}${getLocalizedPath(locale, path)}`, + ]) + ) + + return routing.locales.map(locale => ({ + url: languages[locale], + lastModified, + alternates: { languages }, + })) + }) +} diff --git a/src/components/seo.tsx b/src/components/seo.tsx index 65f94fd..09e5a89 100644 --- a/src/components/seo.tsx +++ b/src/components/seo.tsx @@ -7,6 +7,10 @@ import { routing } from "@/i18n/routing" export const SITE_URL = "https://uiogaming.no" export const THEME_COLOR = "#7d36e7" +/** Adds the locale prefix to a path, e.g. ("en", "/event/x") -> "/en/event/x" */ +export const getLocalizedPath = (locale: Locale, path: string) => + path === "/" ? `/${locale}` : `/${locale}${path}` + type SeoProps = { locale: Locale /** Path without the locale prefix, e.g. "/" or "/event/some-slug" */ @@ -32,8 +36,6 @@ export async function generateSeoMetadata({ const defaultTitle = "UiO Gaming" const metaDescription = description || t("description") - const localizedPath = (pageLocale: Locale) => - path === "/" ? `/${pageLocale}` : `/${pageLocale}${path}` return { title: title ? `${title} | ${defaultTitle}` : defaultTitle, @@ -42,12 +44,12 @@ export async function generateSeoMetadata({ // Tells search engines that the Norwegian and English versions are the // same page. The unprefixed path redirects based on the browser language alternates: { - canonical: localizedPath(locale), + canonical: getLocalizedPath(locale, path), languages: { ...Object.fromEntries( routing.locales.map(pageLocale => [ pageLocale, - localizedPath(pageLocale), + getLocalizedPath(pageLocale, path), ]) ), "x-default": path, @@ -60,7 +62,7 @@ export async function generateSeoMetadata({ ? { type: "article", publishedTime, authors: author && [author] } : { type: "website" }), locale: locale, - url: localizedPath(locale), + url: getLocalizedPath(locale, path), siteName: defaultTitle, images: imageURL ? [ diff --git a/src/lib/sanity.ts b/src/lib/sanity.ts index 222b932..06429be 100644 --- a/src/lib/sanity.ts +++ b/src/lib/sanity.ts @@ -144,3 +144,33 @@ export async function getBoardMembers() { } ) } + +export async function getSitemapDocuments(): Promise<{ + posts: { slug: string; _updatedAt: string }[] + events: { slug: string; _updatedAt: string }[] +}> { + const query = ` + { + "posts": *[_type == "post" && defined(slug.current)] { + "slug": slug.current, + _updatedAt + }, + "events": *[_type == "event" && defined(slug.current)] { + "slug": slug.current, + _updatedAt + } + } + ` + + return sanityClient.fetch( + query, + {}, + { + cache: "force-cache", + next: { + tags: ["blog-posts", "events"], + revalidate: 3600, // 1 hour + }, + } + ) +}