Bug /en/about: Halaman EN Merender Konten ID di Next.js
Halaman /en/about tetap bahasa Indonesia padahal build hijau: komponen page sinkron yang mengabaikan params. Perbaikannya begitu sederhana setelah kelihatan.
Ringkasan
Build hijau bukan jaminan konten bener—empat page di app/[locale] masih return konten id hardcoded soalnya nggak baca params. Fix-nya: page jadi async, await params, map konten per locale, plus guard hasLocale biar locale aneh dapat 404, bukan fallback senyap. generateStaticParams cukup sekali di root layout biar rute EN ke-prerender pas build.
Saya buka /en/about siang itu, refresh sekali lagi, teksnya tetap bahasa Indonesia. Padahal file aboutEn udah ada di repo, diterjemahin rapi. Ganti ke /en/client, sama. /en/diklat juga nggak gerak. Nggak ada error di browser, build juga hijau semua. Rasanya kayak konten EN itu cuma pajangan.
Asumsi saya awalnya simpel. App Router kan udah paham locale dari folder [locale], jadi page di dalamnya otomatis ngerender konten yang bener. Kalo file EN ada dan build sukses, berarti paritas konten aman. Ternyata dua-duanya salah.
Saya ngecek app/[locale]/about/page.tsx. Komponennya masih sinkron.
// app/[locale]/about/page.tsx - sebelum fix
export default function AboutPage() {
return <About content={about} />
}Nggak ada params sama sekali. Empat page kena pola yang sama: about, client, diklat, service. Semuanya return objek id hardcoded. Jadi mau buka /en atau /id, hasilnya ya itu-itu aja. Build sukses emang nggak ngecek logika ini. Dia cuma mastiin komponen bisa dirender, bukan mastiin kontennya bener per locale.
Params sekarang Promise, bukan objek biasa
Sejak Next.js 15, params di page dan layout itu Promise dan wajib di-await [2]. Bukan cuma params, searchParams, cookies(), headers(), draftMode() juga ikut jadi async. Ada codemod buat migrasi, plus mode kompatibilitas sinkron sementara yang cuma ngasih warning di dev [2].
Ini breaking change yang gampang kelewat kalo komponen kamu nggak pernah nyentuh params dari awal. TypeScript sebenernya udah bisa nandain komponen sinkron yang akses params sebagai error. Tapi kalo kamu nggak akses params sama sekali kayak kasus saya, ya nggak ada yang komplain. Silent bug.
Pola resmi buat i18n malah jelas: nesting di app/[lang], terus di page ambil { lang } = await params, lalu guard pakai hasLocale(lang) dan notFound() buat locale asing [1]. Dictionary-nya simpel, map dari locale ke objek konten [1]. Nggak ada magic.
Fix yang saya pakai: async + map + guard
Biar tiap page baca locale dari route, saya ubah ke async dan bikin map konten.
// app/[locale]/about/page.tsx - sesudah fix (disederhanakan)
import { hasLocale } from '@/lib/i18n'
import { notFound } from 'next/navigation'
import { about, aboutEn } from '@/content/about'
const contentByLocale = { id: about, en: aboutEn }
export async function generateMetadata({ params }) {
const { locale } = await params
// pakai locale buat title/description per bahasa
}
export default async function AboutPage({ params }) {
const { locale } = await params
if (!hasLocale(locale)) notFound()
return <About content={contentByLocale[locale]} />
}Pola ini saya ulang buat keempat page di commit d1bfef9. generateMetadata juga ikut await params biar metadata nggak ketuker bahasa.
Guard hasLocale penting banget. Tanpa itu, locale aneh kayak /xx/about bakal tetep kerender pakai fallback id dan keliatan normal, padahal harusnya 404. Saya milih bikin 404 yang eksplisit daripada fallback diam-diam. Fallback itu yang bikin bug kayak kemarin nggak keliatan dari build.
Ada nuansa yang saya baru tau pas baca doc: guard ini nggak harus di page. Bisa dipindah ke util dictionary pakai getter next/root-params [1]. Dan contoh resmi malah naruh generateStaticParams di app/[lang]/layout.tsx, bukan di tiap page [1].
generateStaticParams bukan hiasan
Satu lagi yang saya salah kira: App Router itu selalu SSG. Nggak.
generateStaticParams bisa ditaruh di page, layout, atau route handler buat nge-prerender rute locale pas build, dan dia jalan sebelum Layout atau Pages di-generate [3]. Tapi sifatnya kondisional. Kalo kamu nggak define fungsi itu, atau return array kosong tanpa dynamic = 'force-static', rutenya jadi render on-demand [3]. Jadi tanpa generateStaticParams, /en/about tetep bisa kebuka, tapi nggak di-prerender.
Di project ini, opsi paling rapi ya taruh generateStaticParams sekali di root layout app/[locale]/layout.tsx yang return [{ locale: 'id' }, { locale: 'en' }]. Nggak perlu diulang di tiap page. Itu yang doc contohin [1][3]. Prinsipnya: layout yang tau daftar locale yang valid, page tinggal konsumsi.
Opini saya tegas di sini. Jangan andalin fallback default locale di komponen. Biarin TypeScript teriak kalo komponen masih sinkron dan coba akses params tanpa await. Mending build fail di lokal gara-gara type error daripada prod keliatan sukses tapi /en isinya tetap Indonesia. Bug konten lokal emang paling susah ketahuan kalo kamu cuma liat status build.
Kalo kamu lagi migrasi ke Next.js 15, coba cek dulu semua page di bawah [locale]. Jalankan tsc tanpa mode kompatibilitas sinkron, liat mana yang masih sinkron. Terus buka /en/about beneran, jangan cuma liat /about aja. Kalo teksnya nggak ganti, kamu tau di mana nyarinya.