Lewati ke konten
← tulisan

Menerbitkan blog dwibahasa dari static export

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:

KesalahanDitangkap olehKapan
Judul / deskripsi per-locale hilangTypeScripttsc, di editor
Isi per-locale hilangPrerendernext build
Slug tak ada di registryRoutertak 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.