D
P
0

Next.js

Search Cmd+K Balik "No matches" untuk Hampir Semua Keyword di Produksi? Index-nya Dibangun dari Placeholder

11 Juli 2026·4 menit baca
Search Cmd+K Balik "No matches" untuk Hampir Semua Keyword di Produksi? Index-nya Dibangun dari Placeholder

Di sebuah situs berita kripto klien yang saya bantu rapikan, ada command palette ala Cmd+K untuk cari apa saja: artikel, nama koin, istilah glosarium, penulis. Fitur yang enak dipakai, dan waktu demo lokal semuanya jalan. Beberapa minggu setelah live, klien lapor: search-nya "kosong". Saya buka produksi, tekan Cmd+K, ketik cardano, dan yang balik cuma satu baris dingin: "No matches".

Aneh, karena situs ini punya ratusan artikel asli dan ribuan istilah glosarium. cardano itu bukan keyword nyeleneh, itu salah satu koin paling umum. Saya coba bitcoin, dapat hasil. Coba ethereum, dapat. Coba solana, cardano, polkadot, nama penulis, judul artikel yang saya tahu ada, semuanya "No matches". Polanya ganjil: sebagian kecil keyword jalan, sisanya, yang justru mayoritas, mati total.

Gejalanya

Yang bikin bingung, tidak ada yang "rusak" secara kasat mata. Tidak ada error di konsol, tidak ada request gagal di tab Network, tidak ada layar merah. Command palette-nya muncul mulus, animasinya jalan, dan untuk segelintir keyword dia benar-benar mengembalikan hasil yang benar. Kalau saya cuma tes cepat dengan bitcoin, saya akan simpulkan fiturnya sehat dan lanjut. Itulah persis yang terjadi waktu demo, dan itulah kenapa ini sampai lolos ke produksi.

Refleks pertama saya jelek lagi: saya kira ini soal index yang belum ke-rebuild, atau data CMS yang belum sinkron ke produksi. Saya cek CMS, artikelnya ada semua. Saya cek endpoint konten, balik penuh. Datanya jelas hidup. Tapi search-nya tetap buta terhadap hampir semua data itu.

Menelusuri

Karena search ini berjalan di client, saya buka komponen yang membangun index-nya, CommandPalette.tsx. Di dalamnya ada fungsi buildIndex() yang menyusun daftar item yang bisa dicari. Saya lacak dari mana dia mengambil sumbernya, dan di situ semuanya klik. Setiap sumber di buildIndex() mengarah ke satu modul: lib/placeholder-data.ts.

// CommandPalette.tsx
import { coins, articles, nfts } from "@/lib/placeholder-data";
 
function buildIndex() {
  return [
    ...coins.map(toSearchEntry),
    ...articles.map(toSearchEntry),
    ...nfts.map(toSearchEntry),
  ];
}

Saya buka lib/placeholder-data.ts, dan isinya persis seperti namanya: data dummy dari fase mockup. Enam koin asli, bitcoin, ethereum, bnb, xrp, usdt, solana, dengan id yang dikarang, beberapa artikel placeholder berjudul "Lorem ipsum", dan daftar NFT yang di-hardcode. Tidak ada satu baris pun yang menyentuh CMS.

Di titik itu polanya masuk akal total. Keyword yang "jalan", bitcoin, ethereum, solana, adalah koin yang kebetulan ada di enam dummy itu. cardano tidak ada di daftar dummy, makanya "No matches". Search-nya tidak pernah buta terhadap data asli. Dia tidak pernah tahu data asli itu ada.

Akar masalahnya

Ini jenis bug yang paling menipu karena tidak pernah gagal dengan berisik. buildIndex() menyusun 100% index dari modul statis fase mockup dan tidak pernah sekali pun query ke CMS. Tidak ada type error, karena placeholder-data.ts bentuknya persis seperti data asli. Tidak ada crash, karena datanya valid, cuma palsu. Dan karena segelintir koin asli kebetulan ikut nyangkut di dummy itu, tes santai selalu lolos.

Jadi fitur ini live sambil menyajikan data bohongan. Bukan karena ada yang salah ketik, tapi karena sambungan ke sumber asli memang tidak pernah dibuat. Placeholder yang seharusnya cuma pengisi sementara waktu mockup, tidak pernah dicabut, dan malah jadi satu-satunya sumber untuk permukaan yang hidup.

Perbaikannya

Solusinya adalah membangun index dari konten asli. Saya bikin route /api/search-index yang di-cache, memanggil getSearchIndex() yang menyapu artikel, koin, glosarium, dan penulis asli, lalu komponen mengambilnya sekali per sesi dan memfilter di sisi client.

// app/api/search-index/route.ts
import { getSearchIndex } from "@/lib/search";
 
export const revalidate = 3600;
 
export async function GET() {
  const index = await getSearchIndex(); // artikel + koin + glosarium + penulis asli
  return Response.json(index);
}
// CommandPalette.tsx
const [index, setIndex] = useState<SearchEntry[]>([]);
 
useEffect(() => {
  fetch("/api/search-index")
    .then((r) => r.json())
    .then(setIndex);
}, []);
 
const results = index.filter((e) =>
  e.title.toLowerCase().includes(query.toLowerCase())
);

Sekarang cardano balik dengan hasil, begitu juga tiap koin, artikel, istilah, dan penulis asli. Search-nya akhirnya melihat situs yang sesungguhnya.

Setelah itu saya lakukan audit menyeluruh, karena kalau satu permukaan diam-diam disuapi placeholder, mungkin ada yang lain. Saya grep seluruh komponen untuk impor placeholder-data:

grep -rl 'placeholder-data' --include=*.tsx components/

Setiap hit saya periksa: apakah modul ini sumber utama dari permukaan yang hidup, atau cuma fallback? Placeholder data itu boleh, tapi hanya sebagai fallback hasil-kosong yang bertipe rapi, misalnya waktu fetch gagal, jangan pernah jadi satu-satunya sumber dari fitur yang tayang ke pengguna.

Pelajaran

Bug yang gagal dengan berisik itu mudah. Bug yang berhasil dengan tenang sambil menyajikan data palsu itu yang berbahaya, karena demo lolos, konsol bersih, dan tidak ada yang curiga sampai pengguna asli mengetik keyword yang tidak kebetulan ada di data dummy. Kalau kamu punya permukaan yang hidup, search, listing, feed, pastikan sumbernya benar-benar CMS, bukan modul mockup yang lupa dicabut. Dan sebelum menyimpulkan sebuah fitur sehat, tes dengan keyword yang kamu tahu ada di data asli tapi mustahil ada di dummy. Sejak itu, tiap kali sebuah fitur "jalan" cuma untuk contoh yang saya pilih sendiri, saya berhenti dulu dan tanya: fitur ini melihat data asli, atau cuma memantulkan yang saya ketik?