generateMetadata Dinamis
Metadata per-halaman dinamis: title artikel dari database.
Kenapa metadata statis tidak cukup?
Modul sebelumnya mengajarkan metadata object statis, cukup untuk halaman seperti About atau Kontak yang isinya tetap. Tapi bagaimana dengan halaman blog/[slug] yang punya 100 artikel? Kamu tidak mungkin menulis 100 object metadata manual. Kamu butuh metadata yang dibuat saat request, berdasarkan data artikel yang sedang dibuka. Di sinilah generateMetadata berperan.
generateMetadata: metadata dari data
Ini adalah fungsi async yang di-export dari page route dinamis. Next.js memanggilnya di server sebelum me-render halaman:
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
import { getPost } from "./data";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
return { title: "Artikel tidak ditemukan" };
}
return {
title: post.judul,
description: post.ringkasan,
openGraph: {
title: post.judul,
description: post.ringkasan,
images: [post.cover],
type: "article",
publishedTime: post.tanggal,
},
};
}Perhatikan di Next.js 15 params adalah Promise yang harus di-await. Lupa meng-await adalah error paling umum di sini, dan TypeScript biasanya langsung protes.
Jangan takut fetch dua kali
Satu kekhawatiran wajar: "kalau generateMetadata fetch artikel, lalu page-nya fetch artikel yang sama, bukankah itu dua request database?" Jawabannya: tidak. Next.js punya request memoization: fetch yang identik dalam satu render digabung otomatis menjadi satu request.
// app/blog/[slug]/page.tsx (lanjutan)
import { notFound } from "next/navigation";
export default async function ArtikelPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug); // digabung dengan fetch di generateMetadata
if (!post) notFound();
return <Artikel post={post} />;
}Ini pola resmi yang direkomendasikan. Jangan mengakali dengan mengoper hasil fetch lewat props dari generateMetadata ke page, itu tidak bisa dilakukan dan tidak perlu.
Lapisan data kecil
Agar contoh di atas nyata, getPost bisa sesederhana ini:
// app/blog/[slug]/data.ts
export async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600 }, // cache 1 jam, metadata ikut segar
});
if (!res.ok) return null;
return res.json();
}Karena fetch-nya memakai revalidate, metadata setiap artikel ikut ter-cache dan diperbarui berkala tanpa me-render ulang manual.
Kapan memakai dinamis vs statis
Jangan buru-buru memakai generateMetadata di semua halaman. Halaman seperti About, Kontak, atau Pricing cukup memakai metadata object biasa: lebih simpel dan bisa di-prerender penuh sehingga lebih cepat. Pakai generateMetadata hanya untuk route dinamis yang kontennya bergantung pada parameter, seperti artikel blog, halaman produk, atau profil user. Aturan praktisnya sederhana: kalau title-nya bisa kamu tulis hari ini tanpa melihat database, pakai object statis. Kalau title-nya baru diketahui saat request masuk, pakai generateMetadata.
Catatan teknis:
generateMetadatajuga bisa me-returnalternates,robots, danmetadataBase. Yang terakhir penting: setmetadataBase: new URL("https://tokoku.id")di root layout agar URL gambar Open Graph yang relatif di-resolve menjadi absolut, karena scraper media sosial butuh URL absolut untuk mengambil gambarmu.
Tantangan
Metadata produk
Di halaman produk/[id], buat generateMetadata yang mengambil nama + deskripsi produk dari API dan menjadikannya title/description. Verifikasi via view-source.