Gambar Open Graph Dinamis dengan Next.js ImageResponse

Foto oleh mkhmarketing on flickr
ImageResponse adalah konstruktor yang diekspor dari next slash og yang mengubah JSX dan CSS menjadi PNG yang dirender saat permintaan atau saat waktu build. Konstruktor ini paling umum dipakai untuk menghasilkan gambar Open Graph dan Twitter card sehingga setiap halaman atau artikel blog mendapatkan gambar berbagi sosial yang unik dan sesuai identitas brand, bukan satu banner statis yang dipakai berulang kali.
ImageResponse hanya mendukung format font ttf, otf, dan woff, dan ia tidak akan otomatis mengenali font hanya karena font itu dimuat di bagian lain aplikasimu. Kamu harus membaca sendiri berkas font tersebut lalu mengirimkannya ke opsi fonts sebagai data biner mentah. Kesalahan umum adalah memuat font di dalam fungsi komponen pada setiap permintaan, bukan sekali saja di lingkup modul, yang tetap berfungsi tetapi membuang waktu pada setiap pemanggilan.
Buat berkas opengraph-image.tsx di dalam segmen rute dinamis, misalnya di dalam folder slug, lalu baca slug dari argumen params yang diterima fungsi tersebut. Gunakan slug itu untuk mencari judul dan kategori artikel, kemudian render keduanya ke dalam JSX ImageResponse. Next.js otomatis menyusun meta tag yang tepat di head halaman, sehingga kamu tidak perlu menambahkan apa pun secara manual.
Hanya jika rute tersebut berjalan di runtime Node.js, yang memang menjadi default. Jika kamu mengubah rute ke Edge runtime, node colon fs tidak tersedia dan setiap pembacaan filesystem akan gagal. Dalam kasus itu, ambil font dan aset lain lewat HTTP dari URL public, lalu cache byte yang diambil agar tidak diunduh ulang pada setiap permintaan.
Sebagian besar platform media sosial mencache versi pertama pratinjau tautan secara agresif dan akan tetap menampilkan gambar lama meskipun kamu sudah memperbaiki route handler. Pastikan dulu rute itu sendiri mengembalikan gambar terbaru lewat permintaan HTTP langsung, lalu gunakan alat debugging milik platform tersebut, seperti Sharing Debugger dari Facebook atau Post Inspector dari LinkedIn, untuk memaksa pemindaian ulang, bukan sekadar me-refresh halaman biasa.

Foto oleh mkhmarketing on flickr
Ringkasan Utama
Next.js dapat menghasilkan gambar Open Graph yang unik untuk setiap artikel blog secara langsung saat diakses, menggunakan konstruktor ImageResponse yang mengubah JSX dan CSS menjadi PNG lewat Satori dan resvg. Memuat font kustom sekali di lingkup modul, memilih strategi caching yang tepat, dan memverifikasi hasilnya dengan alat debugging platform adalah kunci agar pendekatan ini andal di produksi.
Setiap pratinjau tautan yang pernah kamu lihat di Slack, iMessage, LinkedIn, atau X didukung oleh beberapa meta tag di bagian head halaman, dan yang paling penting di antaranya adalah gambar. Selama bertahun-tahun, cara paling mudah memenuhi tag itu adalah mengekspor satu banner statis dari alat desain lalu memakainya berulang kali. Cara itu cukup baik untuk halaman utama. Namun cara itu langsung bermasalah begitu kamu memiliki lima puluh artikel blog, masing-masing dengan judul, kategori, dan estimasi waktu baca yang berbeda, dan kamu ingin tautan yang dibagikan benar-benar menyampaikan isi artikel sebelum orang mengekliknya.
Next.js menyelesaikan masalah ini lewat konstruktor ImageResponse yang diekspor dari modul next slash og. Konstruktor ini memungkinkan kamu menggambarkan sebuah gambar sebagai JSX dan CSS biasa, model berpikir yang sama seperti saat membangun komponen, lalu mendapatkan hasil PNG yang dirender secara langsung. Gabungkan dengan konvensi berkas opengraph-image dan kamu mendapatkan gambar berbagi yang unik dan sesuai identitas brand untuk setiap rute, dihasilkan dari data nyata, hampir tanpa infrastruktur tambahan. Artikel ini membahas mekanismenya, jebakan seputar font dan caching yang sering membuat orang keliru, serta cara memverifikasi hasilnya sebelum dirilis.
Next.js memperlakukan berkas bernama opengraph-image di dalam segmen rute mana pun sebagai route handler khusus. Alih-alih mengekspor fungsi GET, kamu mengekspor fungsi async default yang mengembalikan ImageResponse, sebuah Blob, atau Response biasa. Next.js otomatis menyusun meta tag og colon image yang dihasilkan, termasuk lebar, tinggi, dan tipe konten, sehingga kamu tidak perlu menulis tag tersebut secara manual. Contoh di bawah membaca parameter slug dinamis, mencari artikel yang sesuai, lalu merender judul dan kategorinya langsung ke dalam gambar yang dihasilkan.
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
import { getPostBySlug } from '@/lib/posts'
export const alt = 'Blog post cover'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export default async function Image({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPostBySlug(slug)
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: '80px',
backgroundColor: '#0b0f19',
color: 'white',
}}
>
<div style={{ fontSize: 56, fontWeight: 700 }}>{post.title}</div>
<div style={{ fontSize: 28, color: '#94a3b8', marginTop: 24 }}>
{post.category} · {post.readTimeMinutes} min read
</div>
</div>
),
{ ...size }
)
}Di balik layar, ImageResponse menggabungkan tiga proyek sumber terbuka yang berbeda: Satori mengubah JSX dan CSS kamu menjadi SVG, lalu renderer berbasis Rust bernama resvg merasterisasi SVG itu menjadi PNG, dan seluruh alur ini dikemas sebagai pustaka at vercel slash og yang diekspor ulang oleh Next.js. Karena Satori harus mengimplementasikan tata letak CSS sendiri alih-alih mendelegasikannya ke mesin browser sungguhan, hanya sebagian kecil properti CSS yang didukung.
Rancang tata letakmu dulu di Vercel OG Playground sebelum menghubungkannya ke aplikasi. Playground ini menjalankan mesin Satori yang sama persis di browser, sehingga apa yang kamu lihat di sana adalah apa yang akan kamu dapatkan dari route handler, dan bereksperimen di sana jauh lebih cepat daripada mendeploy ulang hanya untuk memeriksa selisih satu piksel.
ImageResponse sudah dilengkapi font sans serif bawaan, tetapi ia tidak akan mengenal font tampilan yang benar-benar dipakai brand kamu kecuali kamu memuatnya sendiri. Hanya format ttf, otf, dan woff yang didukung, dan dokumentasi resmi secara khusus menyarankan ttf atau otf dibanding woff karena keduanya diparsing lebih cepat di dalam alur rendering. Detail krusialnya adalah di mana kamu memuat berkas font tersebut. Jika kamu memanggil pembacaan filesystem atau fetch di dalam fungsi komponen itu sendiri, kamu membaca ulang byte yang sama pada setiap permintaan. Muat sekali saja di lingkup modul, sehingga byte tersebut tersimpan di memori selama masa hidup instance server dan dipakai ulang pada pemanggilan yang sudah hangat.
// Loading a custom font once, at module scope,
// so it survives across warm invocations
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
const fontData = await readFile(
join(process.cwd(), 'assets/PlusJakartaSans-Bold.ttf')
)
// Pass it into ImageResponse options
new ImageResponse(element, {
width: 1200,
height: 630,
fonts: [
{
name: 'Plus Jakarta Sans',
data: fontData,
style: 'normal',
weight: 700,
},
],
})Jika rute opengraph-image kamu berjalan di Edge runtime, node colon fs tidak tersedia, sehingga kamu tidak bisa membaca berkas font langsung dari disk. Sebagai gantinya, ambil font lewat HTTP dari CDN atau URL folder public milikmu sendiri, dan cache respons itu dengan cara yang sama seperti pada Node runtime, karena jika tidak, setiap cold start akan membayar ulang biaya unduhan penuh.
Sebuah opengraph-image yang dihasilkan bersifat statically optimized dan secara default hanya diproduksi sekali saat waktu build, sama seperti halaman yang tidak memiliki dependensi data dinamis. Ia baru beralih otomatis ke generasi saat permintaan ketika menyentuh API waktu permintaan, misalnya membaca cookie atau header, atau ketika mengambil data yang tidak dicache. Perbedaan ini sangat berpengaruh pada biaya dan latensi: gambar statis dibuat sekali dan disajikan dari cache selamanya, sementara gambar saat permintaan dirender ulang setiap kali crawler media sosial mengaksesnya kecuali kamu menambahkan lapisan caching sendiri di atasnya.
| Mode generasi | Kapan terjadi | Paling cocok untuk |
|---|---|---|
| Statis (waktu build) | Tidak ada cookie, header, atau panggilan fetch tanpa cache di dalam fungsi | Halaman tetap seperti halaman utama atau halaman tentang |
| Saat permintaan, dicache | Mengambil data dengan perilaku cache fetch bawaan | Artikel blog dan halaman produk dengan parameter slug |
| Saat permintaan, tanpa cache | Membaca cookie, header, atau secara eksplisit menonaktifkan cache fetch | Kartu personal, misalnya gambar referral dengan nama pengguna |
Untuk sebuah blog, titik idealnya hampir selalu berada di baris tengah tabel tersebut: biarkan pengambilan data di dalam opengraph-image memakai perilaku cache bawaan framework sehingga PNG yang dirender pada dasarnya dihasilkan sekali per artikel lalu dipakai ulang, sambil tetap memungkinkan rebuild penuh untuk memperbaruinya setiap kali konten berubah. Tiga kebiasaan berikut membuat pendekatan ini tetap dapat diandalkan di produksi.
Karena gambar tersebut diturunkan langsung dari metadata judul, kategori, dan waktu baca yang sama dengan yang sudah dirender halamanmu, gambar itu tidak akan pernah tidak sinkron dengan artikel sebenarnya seperti yang biasa terjadi pada banner buatan tangan setelah revisi konten yang kesepuluh.
Gambar yang dihasilkan hanyalah respons dari sebuah rute, sehingga pemeriksaan paling cepat adalah permintaan HTTP biasa ke URL opengraph-image untuk artikel tertentu, memastikan responsnya berstatus 200 dan bertipe konten gambar. Namun itu hanya membuktikan piksel-pikselnya ada, bukan bahwa tag di head halaman sudah benar atau bahwa platform-platform besar akan menampilkannya dengan baik. Setiap platform besar mencache versi pertama sebuah tautan secara agresif, sehingga setelah memperbaiki sesuatu kamu perlu memaksa pemindaian ulang menggunakan alat debugging milik platform tersebut, bukan sekadar me-refresh halaman di browser.
# Quick manual OG debugging checklist
curl -sI https://yoursite.com/blog/my-post/opengraph-image | grep -i content-type
# Should return: content-type: image/png
# Force a re-scrape after you fix something
# Facebook: https://developers.facebook.com/tools/debug/
# X/Twitter: https://cards-dev.twitter.com/validator (legacy, may 404)
# LinkedIn: https://www.linkedin.com/post-inspector/