D
P
0
← Semua artikel Read in English

Cache & CDN: Deploy Sukses, Perubahan Tak Muncul

`revalidateTag` Menyala Tapi Halaman Daftar Tetap Basi? Halaman Prerender Butuh `revalidatePath` Sendiri

· · 7 menit baca
`revalidateTag` Menyala Tapi Halaman Daftar Tetap Basi? Halaman Prerender Butuh `revalidatePath` Sendiri

Keluhan yang masuk pada 5 Juni 2026 sama sekali tidak menyebut kata cache. Bunyinya soal color picker yang tidak nyambung. Situsnya editorial, kontennya dikelola di Sanity, dan sisi depannya Next.js dengan ISR.

Yang bikin keluhan itu enak dikejar, gejalanya cuma berlaku di separuh tempat. Profil penulis disunting, halaman detail di /writer/{slug} ikut berubah saat itu juga. Halaman daftar /contributors tidak. Halaman daftar itu bertahan menyajikan HTML lama.

Catatan kasusnya juga menyimpan satu belokan yang salah di awal. Dugaan pertama jatuh ke cache Cloudflare, padahal bukan itu lapisan yang basi.

Satu edit, dua nasib

Untuk memastikan gejalanya berulang dan bukan kebetulan sekali lewat, saya ganti highlightColor jadi merah, lalu jadi putih. Halaman detail berubah tiap kali. Halaman daftar tertinggal tiap kali.

Pengulangan itu penting karena sekaligus membuang satu kemungkinan besar. Kalau webhook-nya tidak sampai, atau autentikasinya gagal, tidak akan ada halaman yang berubah sama sekali. Yang berubah cuma satu dari dua, jadi webhook-nya sampai dan handler-nya jalan sampai tuntas. Yang tidak lengkap adalah apa yang handler itu kerjakan.

Tag membatalkan data, path membatalkan halaman

Route revalidate di projek ini mengerjakan dua hal berbeda untuk setiap event yang masuk. Satu panggilan revalidateTag yang berlaku umum, dan satu atau beberapa panggilan revalidatePath yang dipilih berdasarkan tipe dokumen.

const body = await req.json();
 
// jalan untuk SETIAP tipe dokumen yang mengirim event
revalidateTag(body._type, "max");
 
// cabang path cuma ada untuk tipe yang sudah terdaftar
switch (body._type) {
  case "article": /* ... */ break;
  case "category": /* ... */ break;
  case "staffProfile":
    revalidatePath(`/writer/${slug}`);
    break;
  // writerProfile: tipe dokumen baru, belum punya cabang di sini
}

Tipe writerProfile ditambahkan ke skema belakangan, sebagai pasangan manusia dari staffProfile yang sudah lebih dulu ada. Skema-nya diperbarui, halamannya jadi, kueri-nya jalan. Yang tidak ikut diperbarui adalah switch di atas. staffProfile tetap memegang cabang path-nya, writerProfile tidak punya padanannya.

Jadi tiap kali profil atau warna aksennya disunting, yang benar-benar tereksekusi cuma separuh:

Edit highlightColor pada dokumen writerProfile
 
menyala   revalidateTag("writerProfile", "max")   cache lapisan data getWriterProfiles dibatalkan
diam      revalidatePath("/contributors")
diam      revalidatePath("/writer/editor-a")

Di situlah dua halaman tadi berpisah nasib. /writer/{slug} dirender server on demand, jadi begitu cache lapisan datanya dibatalkan, request berikutnya menyusun ulang halamannya dari data yang baru. /contributors diprerender statis saat build. HTML-nya sudah jadi dan tersimpan, dan membatalkan cache data tidak dengan sendirinya membuang HTML yang sudah jadi itu.

Invalidasi tag di sini perlu, tapi tidak cukup. Halaman yang diprerender statis butuh revalidasi berbasis path yang eksplisit supaya berpindah dari basi ke segar dengan cepat.

Perbaikannya

Perbaikannya satu commit, dan bentuknya sesederhana yang terlihat. Kedua tipe profil digabung ke satu cabang yang membatalkan kedua URL tempat mereka muncul.

case "staffProfile":
case "writerProfile":
  revalidatePath("/contributors");
  revalidatePath(`/writer/${slug}`);
  break;

staffProfile ikut dimasukkan meski sebelumnya sudah punya cabang sendiri, karena cabang lamanya cuma menyentuh halaman detail dan melewatkan halaman daftar yang sama.

Setelah perbaikan itu, cabang path eksplisit ada untuk tujuh tipe dokumen: article, category, staffProfile, writerProfile, termEntry, assetPage, dan specialEdition. Sisanya, termasuk semua singleton konfigurasi seperti layoutSettings, menuConfig, bannerConfig, serviceSettings, dan siteConfig, cuma dapat invalidasi tag.

Aturan yang saya pasang setelahnya punya dua sisi, dan yang kedua paling gampang terlupa. Sisi pertama, tiap tipe dokumen Sanity baru yang punya halaman daftar publik atau halaman detail berbasis slug harus memperbarui route revalidate di PR yang sama. Sisi kedua, filter webhook di Sanity Studio juga harus benar-benar memicu untuk tipe itu. Route yang sempurna tidak ada gunanya kalau event untuk tipe barunya memang tidak pernah dikirim.

Dua keluhan susulan yang ternyata bukan bug ini

Dua bulan kemudian datang dua keluhan yang bentuknya mirip tapi penyebabnya sama sekali lain. Keduanya layak ditulis karena keduanya nyaris memancing perubahan kode yang tidak dibutuhkan.

Pada 2 Agustus 2026 klien menyembunyikan section featuredStrip di homepage, menerbitkan, lalu 19 detik kemudian melapor: "udah gw hide tapi kok masih ada". Praktis seketika.

10 curl dengan cache-buster   ->  0 dari 10 masih menampilkan section itu
konteks Playwright bersih     ->  sepakat, section-nya memang sudah hilang
homepage                      ->  cf-cache-status: DYNAMIC, tidak pernah di-cache di edge

Servernya sudah benar. Yang basi adalah cache browser klien sendiri. Cloudflare juga tidak bersalah, karena homepage-nya berstatus DYNAMIC dan memang tidak pernah masuk cache edge. Aturan yang saya ambil dari situ: jangan mulai men-deploy perbaikan untuk keluhan "tidak berefek" sebelum selisih antara curl dan browser benar-benar diukur.

Purge CDN saat origin masih basi justru memperpanjang basinya

Sehari kemudian, 3 Agustus, giliran kejadian kedua. Dokumen human-editor-b saya tambal langsung ke dataset, dan /writer/editor-b maupun /contributors tetap menyajikan byline lama. Saya jalankan purge Cloudflare, dan tidak ada yang berubah, karena Cloudflare memang bukan lapisan yang basi.

Tiga header ini yang akhirnya menunjukkan siapa yang menahan versi lama:

cf-cache-status: MISS      <- CF sudah ke origin, purge-nya bekerja
x-nextjs-cache: HIT        <- ORIGIN yang menyajikan render ter-cache
x-nextjs-prerender: 1      <- dan ini entri halaman ISR

Urutan kejadiannya yang bikin semuanya lebih lama:

1. dataset ditambal              origin masih menyajikan render pra-tambal,
                                 sisa fetch cache-nya sekitar 1 jam
2. purge Cloudflare dijalankan   CF ke origin, dan yang diambil render yang masih BASI
3. CF menyimpan render basi itu  selama penuh edge TTL 2 jam di /writer/*

Hitungannya bisa diulang sendiri. Tanpa purge, origin pulih sendiri begitu sisa fetch TTL sekitar 1 jam itu berguling, dan itu memang yang akhirnya terjadi. Dengan purge di tengah jalan, salinan basi tadi terpaku di edge selama 2 jam penuh dihitung dari saat purge. Purge-nya sendiri jalan setelah dataset ditambal, jadi sisa TTL origin waktu itu paling banyak satu jam, dan tambahan kebasiannya sekurang-kurangnya satu jam.

Efek bersih dari purge itu bukan mengakhiri kebasian, tapi memperpanjangnya. Aturannya jadi begini: pastikan origin sudah segar dulu, baru purge. Purge di tengah jalan cuma memaku ulang salinan lama.

Satu tafsiran yang lebih awal di catatan kasus juga perlu diluruskan di sini. x-nextjs-prerender: 1 tidak berarti halamannya beku selamanya. Route-route ini tetap revalidate berdasarkan waktu dari revalidate di tingkat fetch, meski tidak ada export const revalidate sama sekali. Origin memang menyegarkan dirinya sendiri, dan di atas premis keliru tadi catatan kasusnya menyebut sebuah export const revalidate yang nyaris dikirim tanpa perlu.

Urutan diagnostik dari yang termurah

Setelah dua kejadian itu urutan pemeriksaannya jadi tetap, disusun dari yang paling murah:

  1. URL polos dibanding URL dengan ?cb=<acak>. Ini memisahkan jawaban edge dari jawaban origin.
  2. x-nextjs-cache. Ini menjawab apakah origin sendiri yang menyajikan render ter-cache.
  3. curl dibanding browser. Ini menjawab apakah yang basi cache di sisi klien.

Route handler atau filter webhook Sanity baru pantas dicurigai kalau ?cb= pun masih basi.

curl -sI "https://situs-klien.example/writer/editor-b?cb=8412" | grep -i cf-cache-status
# cf-cache-status: MISS      aturan CF di situs ini tidak mengabaikan query string
 
curl -sI https://situs-klien.example/contributors | grep -i cf-cache-status
# cf-cache-status: DYNAMIC   tidak pernah di-cache di edge, jadi enak dipakai sebagai kontrol

Versi cepatnya, untuk dipakai sambil berdiri: curl halaman produksi setelah sebuah edit. Kalau HTML-nya sudah mencerminkan perubahan dalam sekitar 10 detik tapi halaman yang terlihat belum, curigai cache Cloudflare atau cache browser. Kalau HTML-nya juga basi selama menitan, barulah route handler-nya yang pantas dicurigai.

Satu batasan yang perlu diketahui sebelum mencoba jalan pintas: produksi tidak bisa dipaksa bust dari lokal. Secret webhook-nya berbeda per lingkungan, jadi webhook palsu dari mesin sendiri akan dibalas 401.

Aturan yang akhirnya menempel