Satu Template Rute Entitas untuk Semua Bidang di Next.js
Tiga salinan rute entitas ditebang jadi satu set views terparameterisasi: registry pillar, rute tipis per bidang, dan canonical yang mengikuti base modul.
Ringkasan
Laporan masuk tentang link bidang yang salah. Pemeriksaan menemukan pola sistemik yang salah. Refactor dilakukan untuk memperbaiki pola tersebut.
Pertama kali laporan itu masuk, isinya satu kalimat: "jangan-jangan semua link bidang masih ke pariwisata?" Pemeriksaan cepat di halaman live mengonfirmasi dugaan itu. Pillar kebudayaan, olahraga, dan kepemudaan menautkan detail entitasnya ke /pariwisata/*, sementara tautan kategori di mega-menu mereka justru bermuara di halaman 404. Dua arah salah sekaligus.
Tiga salinan yang tidak seharusnya ada
Asumsi awal: satu link yang salah konfigurasi. Kenyataannya polanya sistemik. Sejak masa portal masih pariwisata-sentris, halaman listing dan detail entitas disalin utuh ke rute bidang lain tanpa diparameterisasi; hasilnya tiga salinan kode yang tumbuh terpisah. Ketika struktur bidang diperluas, salinan-salinan itu tak lagi saling menyusul: menu menunjuk kategori yang rutenya tak pernah dibuat, dan halaman detail memakai namespace bidang lain. Membenahi satu per satu jelas bukan jalan keluar. Yang perlu diganti adalah polanya.
Registry pillar dan views bersama
Solusinya dua lapis. Lapis pertama, registry pillar berisi {base, module, label} dalam satu modul kecil, pillar.ts: satu sumber kebenaran untuk prefix URL, modul data, dan label tampilan setiap bidang. Lapis kedua, tampilan listing dan detail diekstrak menjadi server views terparameterisasi, entity-views.tsx, yang menerima pillar sebagai parameter: breadcrumb, canonical melalui generateMetadata, JSON-LD, teks berbagi, hingga set atribut khusus per modul.
Enam rute baru {kebudayaan,olahraga,kepemudaan}/[kategori]/[slug] tinggal tipis. Rute /pariwisata lama diubah menjadi wrapper yang mem-binding pillar PARIWISATA, dengan output byte-identical dibanding sebelumnya; canonical dan breadcrumb "Wisata" diverifikasi langsung di halaman live. Bentuk halamannya kini sependek ini:
// app/kebudayaan/[kategori]/[slug]/page.tsx (disederhanakan)
import { PILLARS } from "@/lib/pillar";
import { EntityDetail, entityMetadata } from "@/components/entity-views";
export async function generateMetadata(props) {
const { kategori, slug } = await props.params;
return entityMetadata(PILLARS.KEBUDAYAAN, kategori, slug);
}
export default async function Page(props) {
const { kategori, slug } = await props.params;
return <EntityDetail pillar={PILLARS.KEBUDAYAAN} kategori={kategori} slug={slug} />;
}
Semua ini berdiri di atas konvensi baku App Router. Segmen rute bisa statis, nilai literal yang dicocokkan persis, atau dinamis, placeholder penangkap nilai dari URL; nilai tangkapannya dikirim ke page lewat props params [1]. Halaman adalah leaf dari route subtree dan wajib ada agar sebuah segmen bisa diakses publik, serta secara default menjadi Server Component [2], bentuk yang tepat untuk views data-driven tanpa state klien. params sendiri berupa promise berisi parameter rute dinamis [5], sehingga seluruh views ditulis async.
Metadata dan sitemap mengikuti base modul
Canonical dan data terstruktur mengikuti base pillar, bukan namespace lama. generateMetadata yang bisa diprerender tanpa perilaku dinamis akan menyertakan metadata di HTML awal [3]; crawler tidak perlu menunggu hidrasi untuk membaca canonical. Di sisi sitemap, konvensi sitemap.(js|ts) mengembalikan array URL dari sebuah fungsi default [4]. Setelah refactor, entri entitas dipancarkan dengan base masing-masing modul, bukan seluruhnya di bawah /pariwisata.
Rute statis tetap menang
Satu jebakan mudah terlewat: precedence. Rute statis seperti /olahraga/sarana dan /kepemudaan/pemuda-pelorar harus tetap menjadi halamannya sendiri, bukan tertelan segmen [kategori]. Next.js mencocokkan segmen literal lebih dulu sebelum placeholder dinamis [1], jadi selama folder statis berdiri sejajar dengan folder dinamis, halaman khusus itu aman. Ujinya sederhana: buka rute statis setelah refactor, pastikan isinya bukan listing generik. Untuk bagian listing yang ingin diprarender, generateStaticParams mengisi segmen dinamis saat build alih-alih on-demand saat request [6].
Wrapper byte-identical menjadi jaring pengaman migrasi. Karena rute pariwisata lama hanya memanggil views yang sama dengan binding pillar yang sama, kebenarannya diverifikasi dari canonical dan breadcrumb yang identik di live, bukan dari keyakinan. Verifikasi akhirnya mekanis: tsc tanpa galat, lint bersih, 86 dari 86 tes vitest lulus, dan probe live menunjukkan /kebudayaan/sanggar-seni kini merespons 200 padahal sebelumnya 404, sementara canonical /pariwisata tak bergeser. Ekstraksi ini memangkas sekitar 800 baris. Refactor paling sehat adalah yang output-nya tidak berubah; duplikasi rute hampir selalu utang yang ditagih tepat saat struktur mulai berkembang.
Sumber
[1] Dynamic Route Segments, Next.js Docs [2] page.js, Next.js Docs [3] generateMetadata, Next.js Docs [4] sitemap.xml, Next.js Docs [5] route.js, Next.js Docs [6] generateStaticParams, Next.js Docs