From 2e54e32ea64d618e060309fc7af776c5f87f8c77 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 22:59:56 +0100 Subject: [PATCH] Add canonical URLs, hreflang alternates and article metadata Set metadataBase so relative metadata URLs resolve to the real domain, link the Norwegian and English versions of each page, and mark blog posts as articles with their publish date and author. Co-Authored-By: Claude Opus 5.5 --- src/app/[locale]/blog/[slug]/page.tsx | 2 ++ src/app/[locale]/event/[slug]/page.tsx | 1 + src/app/[locale]/layout.tsx | 4 ++- src/app/[locale]/page.tsx | 2 +- src/components/seo.tsx | 35 +++++++++++++++++++++++--- 5 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/app/[locale]/blog/[slug]/page.tsx b/src/app/[locale]/blog/[slug]/page.tsx index 7df83f1..80024bd 100644 --- a/src/app/[locale]/blog/[slug]/page.tsx +++ b/src/app/[locale]/blog/[slug]/page.tsx @@ -64,10 +64,12 @@ export async function generateMetadata({ return generateSeoMetadata({ locale, + path: `/blog/${slug}`, title: data.title, description: data.excerpt, imageURL: data.mainImage?.asset?.url, author: data.author?.name, + publishedTime: data._createdAt, }) } diff --git a/src/app/[locale]/event/[slug]/page.tsx b/src/app/[locale]/event/[slug]/page.tsx index a2ad221..e23e035 100644 --- a/src/app/[locale]/event/[slug]/page.tsx +++ b/src/app/[locale]/event/[slug]/page.tsx @@ -36,6 +36,7 @@ export async function generateMetadata({ return generateSeoMetadata({ locale, + path: `/event/${slug}`, title: data.title, description: [ `${t("time")}: ${formatEventDate(data.date, locale)}`, diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 7ea79f5..8dc77f8 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -6,7 +6,7 @@ import { getMessages } from "next-intl/server" import { notFound } from "next/navigation" import LanguageSelector from "@/components/ui/languageSelector" -import { THEME_COLOR } from "@/components/seo" +import { SITE_URL, THEME_COLOR } from "@/components/seo" import UmamiAnalytics from "@/components/umamiAnalytics" import { routing } from "@/i18n/routing" @@ -16,6 +16,8 @@ import { routing } from "@/i18n/routing" const CLIENT_NAMESPACES = ["about", "buttons", "common", "office"] export const metadata: Metadata = { + // Resolves relative URLs in metadata, like Open Graph images and alternates + metadataBase: new URL(SITE_URL), title: "UiO Gaming", description: "En hobbyforening som ønsker skape et sosialt miljø rundt gaming og e-sport på UiO!", diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index 19b8930..d7ab8b9 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -23,7 +23,7 @@ export async function generateMetadata({ }: HomePageParams): Promise { const { locale } = await params - return generateSeoMetadata({ locale }) + return generateSeoMetadata({ locale, path: "/" }) } export default function Home() { diff --git a/src/components/seo.tsx b/src/components/seo.tsx index 63476ab..65f94fd 100644 --- a/src/components/seo.tsx +++ b/src/components/seo.tsx @@ -2,38 +2,65 @@ import { Metadata } from "next" import type { Locale } from "next-intl" import { getTranslations } from "next-intl/server" +import { routing } from "@/i18n/routing" + +export const SITE_URL = "https://uiogaming.no" export const THEME_COLOR = "#7d36e7" type SeoProps = { locale: Locale + /** Path without the locale prefix, e.g. "/" or "/event/some-slug" */ + path: string title?: string description?: string imageURL?: string author?: string + /** Marks the page as an article, e.g. a blog post */ + publishedTime?: string } export async function generateSeoMetadata({ locale, + path, title, description, imageURL, author, + publishedTime, }: SeoProps): Promise { const t = await getTranslations({ locale, namespace: "metaTags" }) const defaultTitle = "UiO Gaming" const metaDescription = description || t("description") - const siteUrl = "https://uiogaming.no" + const localizedPath = (pageLocale: Locale) => + path === "/" ? `/${pageLocale}` : `/${pageLocale}${path}` return { title: title ? `${title} | ${defaultTitle}` : defaultTitle, description: metaDescription, authors: author ? [{ name: author }] : [{ name: "UiO Gaming" }], + // 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), + languages: { + ...Object.fromEntries( + routing.locales.map(pageLocale => [ + pageLocale, + localizedPath(pageLocale), + ]) + ), + "x-default": path, + }, + }, openGraph: { title: title || defaultTitle, description: metaDescription, - type: "website", + ...(publishedTime + ? { type: "article", publishedTime, authors: author && [author] } + : { type: "website" }), locale: locale, + url: localizedPath(locale), siteName: defaultTitle, images: imageURL ? [ @@ -46,7 +73,7 @@ export async function generateSeoMetadata({ ] : [ { - url: `${siteUrl}/logo_2024_neon_trans.png`, + url: "/logo_2024_neon_trans.png", alt: defaultTitle, }, ], @@ -55,7 +82,7 @@ export async function generateSeoMetadata({ card: imageURL ? "summary_large_image" : "summary", title: title || defaultTitle, description: metaDescription, - images: imageURL ? [imageURL] : [`${siteUrl}/logo_2024_neon_trans.png`], + images: imageURL ? [imageURL] : ["/logo_2024_neon_trans.png"], }, other: { "msapplication-TileColor": THEME_COLOR,