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 <html lang>, 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 <noreply@anthropic.com>
This commit is contained in:
leanderandClaude Opus 5.5 committed 2026-10-30 21:28:32 +01:00
1 parent a8b31a5379
commit da3d26d16b
9 files changed
+77 -79

No files matched your search

+8 -4
View File
@@ -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<Metadata> {
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)
+10 -6
View File
@@ -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<Metadata> {
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)
+34 -13
View File
@@ -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 (
<NextIntlClientProvider locale={locale} messages={messages}>
<LanguageSelector />
<main>{children}</main>
{analyticsConfig?.enabled && (
<UmamiAnalytics
websiteId={analyticsConfig.websiteId}
src={analyticsConfig.src}
domains={analyticsConfig.domains}
/>
)}
</NextIntlClientProvider>
<html lang={locale}>
<body>
<NextIntlClientProvider locale={locale} messages={messages}>
<LanguageSelector />
<main>{children}</main>
{analyticsConfig?.enabled && (
<UmamiAnalytics
websiteId={analyticsConfig.websiteId}
src={analyticsConfig.src}
domains={analyticsConfig.domains}
/>
)}
</NextIntlClientProvider>
</body>
</html>
)
}
+12 -2
View File
@@ -11,8 +11,18 @@ import { Metadata, Viewport } from "next"
export const revalidate = 3600 // 1 hour
export async function generateMetadata(): Promise<Metadata> {
return generateSeoMetadata({})
interface HomePageParams {
params: Promise<{
locale: string
}>
}
export async function generateMetadata({
params,
}: HomePageParams): Promise<Metadata> {
const { locale } = await params
return generateSeoMetadata({ locale })
}
export function generateViewport(): Viewport {
-26
View File
@@ -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 (
<html>
<body>{children}</body>
</html>
)
}
-18
View File
@@ -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<Metadata> {
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 <NotFound locale={defaultLocale} />
}
+4 -3
View File
@@ -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<Metadata> {
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")
+6 -5
View File
@@ -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 {
+3 -2
View File
@@ -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|.*\\..*).*)"],
}