From da3d26d16bc5b7419439f7133319bff67a915a3b Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 21:28:32 +0100 Subject: [PATCH] Render pages statically using next/root-params getLocale() read request headers, which made every route dynamic and the page-level revalidate a no-op. Make [locale]/layout.tsx the root layout so the locale can be read through next/root-params, set , add generateStaticParams for locales and slugs, and pass the locale explicitly to metadata translations. The middleware now matches all non-file paths so URLs without a locale are redirected instead of hitting the root layout's notFound(). Co-Authored-By: Claude Opus 5.5 --- src/app/[locale]/blog/[slug]/page.tsx | 12 ++++--- src/app/[locale]/event/[slug]/page.tsx | 16 +++++---- src/app/[locale]/layout.tsx | 47 +++++++++++++++++++------- src/app/[locale]/page.tsx | 14 ++++++-- src/app/layout.tsx | 26 -------------- src/app/not-found.tsx | 18 ---------- src/components/seo.tsx | 7 ++-- src/i18n.ts | 11 +++--- src/middleware.ts | 5 +-- 9 files changed, 77 insertions(+), 79 deletions(-) delete mode 100644 src/app/layout.tsx delete mode 100644 src/app/not-found.tsx diff --git a/src/app/[locale]/blog/[slug]/page.tsx b/src/app/[locale]/blog/[slug]/page.tsx index 46504a5..998a881 100644 --- a/src/app/[locale]/blog/[slug]/page.tsx +++ b/src/app/[locale]/blog/[slug]/page.tsx @@ -8,7 +8,6 @@ import { getBlogPost } from "@/lib/sanity" import { PortableText, PortableTextComponents } from "@portabletext/react" import { Metadata, Viewport } from "next" import { notFound } from "next/navigation" -import { getLocale } from "next-intl/server" import Image from "next/image" import * as styles from "./page.module.css" @@ -33,15 +32,21 @@ interface LinkValue { href: string } +// Render posts on first visit and cache them, instead of on every request +export async function generateStaticParams() { + return [] +} + export async function generateMetadata({ params, }: BlogPageParams): Promise { - const { slug } = await params + const { slug, locale } = await params const data = await getBlogPost(slug) if (!data) notFound() return generateSeoMetadata({ + locale, title: data.title, description: data.excerpt, imageURL: data.mainImage?.asset?.url, @@ -94,8 +99,7 @@ const components: PortableTextComponents = { } const BlogPost = async ({ params }: BlogPageParams) => { - const { slug }: { slug: string } = await params - const locale = await getLocale() + const { slug, locale } = await params const data = await getBlogPost(slug) diff --git a/src/app/[locale]/event/[slug]/page.tsx b/src/app/[locale]/event/[slug]/page.tsx index 4504171..83cafe6 100644 --- a/src/app/[locale]/event/[slug]/page.tsx +++ b/src/app/[locale]/event/[slug]/page.tsx @@ -5,7 +5,7 @@ import { getEvent } from "@/lib/sanity" import { Metadata, Viewport } from "next" import { notFound } from "next/navigation" -import { getLocale, getTranslations } from "next-intl/server" +import { getTranslations } from "next-intl/server" import { FaCalendarAlt, FaMapMarkerAlt } from "react-icons/fa" import * as styles from "./page.module.css" @@ -17,16 +17,20 @@ interface EventPageParams { }> } +// Render events on first visit and cache them, instead of on every request +export async function generateStaticParams() { + return [] +} + export async function generateMetadata({ params, }: EventPageParams): Promise { - const { slug } = await params + const { slug, locale } = await params const data = await getEvent(slug) if (!data) notFound() - const locale = await getLocale() - const t = await getTranslations("metaTags") + const t = await getTranslations({ locale, namespace: "metaTags" }) const formatDate = (dateString: string) => { const date = new Date(dateString) @@ -43,6 +47,7 @@ export async function generateMetadata({ } return generateSeoMetadata({ + locale, title: data.title, description: `${t("time")}: ${formatDate(data.date)}\n${t("location")}: ${data.location}\n\n${data.description}`, }) @@ -53,8 +58,7 @@ export function generateViewport(): Viewport { } const Event = async ({ params }: EventPageParams) => { - const { slug }: { slug: string } = await params - const locale = await getLocale() + const { slug, locale } = await params const data = await getEvent(slug) diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index ff19902..d514566 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -1,5 +1,7 @@ -// app/[locale]/layout.tsx +import "@/app/globals.css" + import { hasLocale, NextIntlClientProvider } from "next-intl" +import type { Metadata } from "next" import { notFound } from "next/navigation" import LanguageSelector from "@components/ui/languageSelector" @@ -7,7 +9,22 @@ import UmamiAnalytics from "@/components/umamiAnalytics" import { locales } from "@/i18n" import { getAnalyticsConfig } from "@/lib/analyticsConfig" -export default async function LocaleLayout({ +export const metadata: Metadata = { + title: "UiO Gaming", + description: + "En hobbyforening som ønsker skape et sosialt miljø rundt gaming og e-sport på UiO!", + icons: { + icon: "/logo_2024_neon_pink.png", + shortcut: "/logo_2024_neon_pink.png", + apple: "/logo_2024_neon_pink.png", + }, +} + +export function generateStaticParams() { + return locales.map(locale => ({ locale })) +} + +export default async function RootLayout({ children, params, }: { @@ -24,16 +41,20 @@ export default async function LocaleLayout({ const analyticsConfig = getAnalyticsConfig() return ( - - -
{children}
- {analyticsConfig?.enabled && ( - - )} -
+ + + + +
{children}
+ {analyticsConfig?.enabled && ( + + )} +
+ + ) } diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index 58e0702..8c89850 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -11,8 +11,18 @@ import { Metadata, Viewport } from "next" export const revalidate = 3600 // 1 hour -export async function generateMetadata(): Promise { - return generateSeoMetadata({}) +interface HomePageParams { + params: Promise<{ + locale: string + }> +} + +export async function generateMetadata({ + params, +}: HomePageParams): Promise { + const { locale } = await params + + return generateSeoMetadata({ locale }) } export function generateViewport(): Viewport { diff --git a/src/app/layout.tsx b/src/app/layout.tsx deleted file mode 100644 index 0635cba..0000000 --- a/src/app/layout.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import "@/app/globals.css" - -import type { Metadata } from "next" - -export const metadata: Metadata = { - title: "UiO Gaming", - description: - "En hobbyforening som ønsker skape et sosialt miljø rundt gaming og e-sport på UiO!", - icons: { - icon: "/logo_2024_neon_pink.png", - shortcut: "/logo_2024_neon_pink.png", - apple: "/logo_2024_neon_pink.png", - }, -} - -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { - return ( - - {children} - - ) -} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx deleted file mode 100644 index a6b4787..0000000 --- a/src/app/not-found.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import NotFound from "@/components/page-sections/notFound" - -import { defaultLocale } from "@/i18n" - -import { Metadata } from "next" -import { getTranslations } from "next-intl/server" - -export async function generateMetadata(): Promise { - const t = await getTranslations({ locale: defaultLocale, namespace: "404" }) - - return { title: `${t("title")} | UiO Gaming` } -} - -// Rendered when [locale]/layout.tsx rejects an unknown locale (e.g. /de), -// so there is no valid locale to use -export default function RootNotFound() { - return -} diff --git a/src/components/seo.tsx b/src/components/seo.tsx index 44bfb22..c865384 100644 --- a/src/components/seo.tsx +++ b/src/components/seo.tsx @@ -1,7 +1,8 @@ import { Metadata, Viewport } from "next" -import { getLocale, getTranslations } from "next-intl/server" +import { getTranslations } from "next-intl/server" type SeoProps = { + locale: string title?: string description?: string imageURL?: string @@ -9,13 +10,13 @@ type SeoProps = { } export async function generateSeoMetadata({ + locale, title, description, imageURL, author, }: SeoProps): Promise { - const t = await getTranslations("metaTags") - const locale = await getLocale() + const t = await getTranslations({ locale, namespace: "metaTags" }) const defaultTitle = "UiO Gaming" const metaDescription = description || t("description") diff --git a/src/i18n.ts b/src/i18n.ts index f7e2bad..50b056c 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -1,14 +1,15 @@ import { hasLocale } from "next-intl" import { getRequestConfig } from "next-intl/server" +import { locale as rootLocale } from "next/root-params" export const locales = ["no", "en"] as const -export const defaultLocale = "no" +const defaultLocale = "no" -export default getRequestConfig(async ({ locale, requestLocale }) => { +export default getRequestConfig(async ({ locale }) => { // An explicit locale (e.g. getTranslations({ locale })) takes precedence. - // Fall back to the default instead of throwing, so pages rendered outside - // a valid locale (like the root not-found page) can still be translated - const requested = locale ?? (await requestLocale) + // The root param can be any first path segment until the layout rejects it, + // so fall back to the default instead of throwing + const requested = locale ?? (await rootLocale()) const resolved = hasLocale(locales, requested) ? requested : defaultLocale return { diff --git a/src/middleware.ts b/src/middleware.ts index 12e7b68..2ce9fdc 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -7,6 +7,7 @@ export default createMiddleware({ }) export const config = { - // Match only internationalized pathnames - matcher: ["/", "/(no|en)/:path*"], + // Match everything except API routes, Next internals and files with an + // extension, so paths without a locale get redirected to one + matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"], }