Metadata hasil chat diangkat dari SQL sampai popup
Chip waktu dan tag di popup chat ternyata bukan urusan CSS. Metadata harus naik dulu dari SQL sampai DTO sebelum UI bisa bergaya.
Ringkasan
Popup search kelihatan datar bukan karena CSS, tapi karena API nggak mengirim tags, categories, dan published_at dari database. Solusinya: naikkan metadata itu lewat SQL, domain, sampai DTO, baru frontend merender. Setelah pipeline utuh, TimelineRow dipakai ulang di tiga konteks popup dengan chip waktu, ikon teknologi, dan badge sitasi.
Baris hasil chat di popup search kelihatan datar, cuma judul sama excerpt, tanpa chip waktu, tanpa tag. Sementara di halaman artikel, timeline-nya udah cakep: spine vertikal, ikon teknologi, badge kategori, badge sitasi [n]. Kesenjangan visualnya jelas banget, dan pertanyaannya sederhana: kenapa popup chat nggak pakai gaya yang sama?
Dugaan awal: copy komponen timeline, taruh di popup, selesai. Urusan UI doang kan?
Ternyata nggak. Chip waktu nggak muncul bukan karena CSS-nya belum ada, tapi karena API-nya memang nggak mengirim published_at ke hasil search. Tags dan categories? Sama, nggak dikirim. Jadi mau secanggih apapun komponen frontend-nya, datanya nggak ada buat ditampilin. Dugaan bahwa masalahnya "cuma visual" itu salah total.
Yang dibutuhkan justru naikkan metadata dulu, dari SQL sampai ke browser. Serupa tapi nggak persis sama, artikel soal streaming jawaban AI lewat fetch biasa dulu juga bilang: kontrak datanya dulu, baru urusan tampilan.
Naikkan metadata dari SQL
Di repositories.py, query SELECT perlu ambil kolom tags_json dan categories_json dari tabel articles_posts. Nilainya berupa string JSON mentah, jadi perlu json.loads sebelum ditaruh ke SearchRow. Entity di domain layer dapat dua field baru, tags dan categories, keduanya list of string yang boleh kosong.
Di layer atas, SearchHitDTO yang berbasis Pydantic BaseModel menerima field tags dan categories dengan default list of string kosong. Di sinilah kontrak Pydantic kepakai: begitu data lolos validasi, field dijamin sesuai tipe yang dideklarasikan, jadi tags di DTO nggak akan pernah None walau database mengembalikan NULL [4]. Konsumen API tinggal pakai tanpa ngecek-ngecek. SQLModel di bawahnya cuma lapis tipis di atas Pydantic dan SQLAlchemy, jadi struktur data nggak perlu didefinisikan ulang di dua dunia [5].
Query FTS dan query LIKE sederhana keduanya diubah biar ikut mengambil kolom JSON ini. Response search punya tags dan categories sekarang. Sumber di mode about yang dikirim lewat SSE juga ikut membawa field baru plus published_at; karena payload SSE berupa string JSON, tanggalnya diserialisasi ke format ISO dulu, atau dikirim sebagai null kalau artikelnya nggak punya tanggal.
Baru setelah pipeline data ini utuh, frontend punya sesuatu untuk dirender.
Satu komponen, tiga konteks
TimelineRow dipakai ulang di tiga tempat dalam popup: hasil search mode, saran FTS, dan related articles di about-mode. Satu komponen, satu format. Tiap baris menampilkan chip waktu, tile ikon teknologi, chip kategori, badge nomor sitasi, lalu judul dan excerpt di sampingnya.
Tanggal dikonversi dari UTC ke WIB lewat formatDateWIB. Fungsi safeDate lama, yang isinya cuma fallback ke nilai kosong buat tanggal invalid, dihapus dari komponen. Fungsi konversi barunya udah nangani kasus itu sendiri: tanggal invalid dirender kosong tanpa error. Kode makin sedikit, perilakunya sama.
Saya pribadi lebih suka arah begini daripada copy-paste markup timeline lama ke popup. Copy markup memang cepat selesai, tapi begitu desain baris berubah, ada dua tempat yang harus diingat buat diubah, dan biasanya yang satu ketinggalan. Satu komponen dengan properti opsional rada lebih lambat dirancang, tapi nggak numpang hutang.
Konteks teknisnya singkat aja. API backend ini dibangun dengan FastAPI, dan pola lapisannya pola klasik: repository baca baris database, entity membawa data di domain, DTO merapikan kontrak untuk keluar, terus SSE atau JSON mengantarnya ke klien. Model SQLAlchemy di sini dibungkus SQLModel, jadi tabel dan skema validasi nggak perlu ditulis dua kali. Satu file kontrak berubah, dua query ikut menyesuaikan, dan tipe di frontend diperbarui supaya TypeScript ikut menjaga bentuk data barunya.
Perubahan backend-nya sendiri keliatan kecil, cuma nambah dua kolom di SELECT plus parsing JSON. Tapi dia menyebar ke tujuh file, 214 baris nambah, 91 baris kurang. Lumayan banyak untuk yang katanya "cuma nambahin timeline ke popup". Tapi emang begitu adanya: komponen UI yang menampilkan metadata harus didukung data yang benar-benar mengalir dari database. Tanpa pipeline yang utuh, timeline di popup cuma jadi hiasan kosong.
Setelah live, saya cek manual di popup: baris hasil kini punya chip waktu WIB yang sama dengan halaman artikel, tile ikon muncul dari tag pertama, dan badge sitasi tetap berfungsi buat lompat ke sumber. Yang tersisa cuma polesan kecil, misalnya jarak antar baris di layar sempit, tapi itu pekerjaan sore, bukan pekerjaan arsitektur.
Pelajaran yang saya bawa: pas komponen UI butuh data yang belum ada di API, jangan mulai dari CSS atau layout. Mulai dari model data di database, tarik ke atas selapis demi selapis, pastikan tiap layer meneruskan field yang dibutuhkan layer berikutnya. Data hidup dulu, baru komponen bisa bergaya.
Sumber
- [4] Pydantic docs: Models (akses 2026-09-01)
- [5] SQLModel docs (akses 2026-09-01)
- Artikel terkait: Jawaban AI Streaming di Blog Lewat fetch Biasa