Setiap tulisan di blog ini ada dua kali — sekali dalam bahasa Inggris, sekali dalam bahasa Indonesia — dan situs ini adalah static export, jadi kedua versinya dirender saat build dan tak ada server yang tersisa untuk membetulkan apa pun sesudahnya. Batasan itu ternyata menentukan seluruh desainnya. Ini bentuk yang dipaksakannya, dan satu titik di mana sistem tipe diam-diam berhenti menolong.
Satu tulisan adalah satu entri registry dan dua isi
Metadata-nya hidup di TypeScript, dalam satu array terurut:
export type PostMeta = {
slug: string
date: string // ISO 8601
title: Record<Locale, string>
description: Record<Locale, string>
}
Isinya hidup sebagai berkas src/content/<locale>/<slug>.mdx — Markdown biasa,
dengan JSX tersedia kalau sebuah tulisan butuh komponen sungguhan. Route-nya
merender satu halaman statis per locale × slug, menarik isi yang tepat lewat
namanya:
export const dynamicParams = false
const { default: Body } = await import(`@/content/${lang}/${slug}.mdx`)
Record<Locale, string> bekerja lebih keras daripada tampaknya. Tambahkan satu
locale ke proyek ini dan semua tulisan berhenti dikompilasi sampai masing-masing
punya judul dan deskripsi dalam bahasa itu. Lupa judul bahasa Indonesia pada
tulisan baru, dan tsc menyebut barisnya persis. Menerjemahkan metadata adalah
kewajiban saat kompilasi.
Isi tulisan tak punya jaminan serupa
Tak ada bagian dari tipe itu yang menyebut berkas. Isi tulisan datang lewat
dynamic import bertemplat, yang diselesaikan bundler dengan membangkitkan sebuah
module context di atas src/content — ia mencocokkan path saat build, bukan tipe
saat pengecekan. Jadi registry bisa menjanjikan tulisan yang tak dimiliki
filesystem.
Saya coba: tambahkan satu entri tanpa .mdx di mana pun, lalu build.
Running TypeScript ...
Finished TypeScript in 4.7s ...
Generating static pages using 11 workers (0/18) ...
Error occurred prerendering page "/en/posts/probe-missing-body".
Error: Cannot find module '@/content/en/probe-missing-body.mdx'
at moduleContext (...[turbopack]_runtime.js:332:19)
Export encountered an error, exiting the build.
TypeScript lolos, bersih, dan melaporkannya begitu. Empat detik kemudian prerender mati pada modul yang memang tak pernah ada.
Gagal saat build justru fiturnya
Itu terlihat seperti lubang, dan selama semenit saya memperlakukannya begitu.
Bukan. Justru static export yang membuatnya aman: karena dynamicParams bernilai
false, setiap pasangan locale × slug didaftar dari registry lalu dirender,
sehingga isi yang tak ada berarti build yang tak selesai. Halaman rusak itu tak
mungkin sampai ke produksi, sebab produksi adalah sekumpulan berkas HTML dan
berkas tadi tak pernah ditulis.
Jalankan kode yang sama di atas SSR, dan kesalahan yang sama akan terkirim.
generateStaticParams berubah jadi petunjuk, bukan daftar; import-nya
diselesaikan saat permintaan pertama; dan terjemahan yang hilang muncul sebagai
500 bagi siapa pun yang kebetulan membuka tautan Indonesia lebih dulu — mungkin
bukan saya, dan mungkin tidak segera.
Bug-nya sama persis. Strategi rendering-lah yang menentukan apakah ia tertangkap oleh sebuah build atau oleh seorang pembaca.
Jadi kedua bagian itu saling menjaga, pada waktu yang berbeda:
| Kesalahan | Ditangkap oleh | Kapan |
|---|---|---|
| Judul / deskripsi per-locale hilang | TypeScript | tsc, di editor |
| Isi per-locale hilang | Prerender | next build |
| Slug tak ada di registry | Router | tak pernah render |
Baris terakhir adalah asas yang sama sekali lagi: dynamicParams = false berarti
slug tak dikenal tidak punya halaman, alih-alih halaman yang dibangun belakangan.
Alurnya
Menambah tulisan berarti satu .mdx di tiap src/content/<locale>/, satu entri
di src/lib/posts.ts, lalu next build. Kalau salah satu bagiannya hilang,
build-lah yang memberi tahu. Itu seluruh pipeline penerbitannya, dan itu sebabnya
saya tak pernah perlu mengingat apakah sesuatu sudah diterjemahkan — saya hanya
perlu ingat untuk build.
Ongkosnya jujur dan kecil: blog dwibahasa adalah dua blog, dan tak ada tipe yang bisa memberi tahu apakah isi bahasa Indonesia benar-benar mengatakan apa yang dikatakan versi Inggrisnya. Ia hanya menjanjikan berkasnya ada. Sisanya urusan saya.