Link Post Multibahasa Jadi /id/id/ dan 404 di Astro + EmDash: Akar Masalah dan Solusinya

TL;DR — Di site EmDash + Astro dua bahasa (Inggris + Indonesia), semua link ke post bahasa Indonesia — dari feed homepage, daftar blog, related post, sampai sidebar — mengarah ke /id/id/{slug} alih-alih /id/{slug}, dan berakhir 404. Slug di database bersih. Penyebab sebenarnya: begitu i18n aktif, CMS mengembalikan entry.id yang sudah berprefix locale ("id/slug-post"), sementara template kami menambahkan base path locale sekali lagi di depannya. Solusinya satu helper kecil yang membangun URL post dari data.locale + data.slug, dipakai di semua tempat link post dirender.
Daftar Isi
- Kondisi deployment
- Gejala
- Pertama: pastikan datanya tidak rusak
- Diagnosis: cari siapa yang membangun href
- Akar masalah: entry.id itu route ID, bukan slug
- Solusinya: satu helper URL, dipakai di mana-mana
- Akar masalah sama, korban lebih banyak
- Perlu redirect untuk URL yang salah?
- Gotcha bonus dari sesi yang sama: artikel ini sendiri menolak di-save
- Pelajaran yang bisa diambil
- Setup yang terdampak
Kondisi deployment
- Astro dengan
output: "server", EmDash CMS sebagai content layer - Dua locale:
en(default, dilayani di/{slug}) danid(dilayani di/id/{slug}) - Route post:
src/pages/[slug].astrodansrc/pages/id/[slug].astro - Semua permukaan list (feed homepage, grid blog, related post, sidebar) merender link dari entri
getEmDashCollection()
Kalau site kamu satu bahasa, bug ini tidak mungkin kena — dan justru itu alasannya dia lolos: semuanya berjalan sempurna sampai locale kedua dinyalakan. Stack-nya sama dengan yang dijelaskan di panduan setup EmDash + Astro kami.
Gejala
- Semua kartu post bahasa Indonesia di homepage
/idmengarah ke/id/id/{slug}. - URL dobel yang sama muncul di daftar blog, grid "Artikel Terkait", navigasi artikel sebelumnya/berikutnya, dan sidebar "Latest Articles".
- URL dobel itu balas 404 — setiap klik internal ke kartu post terjemahan berakhir buntu.
- Kalau URL yang benar
/id/{slug}diketik manual, post-nya tampil normal. - Halaman bahasa Inggris sama sekali tidak terpengaruh.
Kombinasi itu — halamannya ada, link hasil generate salah, dan hanya satu locale yang kena — sudah cukup bilang bahwa bug-nya ada di pembentukan link, bukan di routing dan bukan di kontennya.
Pertama: pastikan datanya tidak rusak
Saat segmen URL dobel, tersangka pertama adalah slug-nya sendiri: jangan-jangan slug tersimpan sebagai id/litertjs-... di CMS (misalnya bawaan script migrasi). Tiga puluh detik SQL langsung menjawab:
SELECT slug, locale FROM ec_posts WHERE slug LIKE '%/%';
-- 0 rowsBersih. Slug aman, berarti dobelnya terjadi saat render. (Disiplin yang sama pernah menyelamatkan kami sebelumnya — cek datanya dulu sebelum menyalahkan data.)
Diagnosis: cari siapa yang membangun href
Grep template untuk pola pembentukan link post. Semua permukaan list punya variasi dari pola ini:
<a href={basePath + "/" + post.id}>{post.data.title}</a>
// basePath = "/id" di halaman bahasa IndonesiaSupaya hasilnya dobel, berarti post.id sendiri sudah berbentuk "id/{slug}". Loader CMS-nya mengonfirmasi — di dalam collection loader emdash, entry ID dibentuk seperti ini:
const entryId =
i18nEnabled && entryLocale !== "" &&
(entryLocale !== i18nConfig.defaultLocale || i18nConfig.prefixDefaultLocale)
? entryLocale + "/" + entrySlug
: entrySlug;Ketemu. Begitu ada lebih dari satu locale, entry.id untuk entri non-default berbentuk "{locale}/{slug}". Template kami menghitung "/id" + "/" + "id/litertjs-..." — dan lahirlah prefix dobel.
Akar masalah: entry.id itu route ID, bukan slug
Ini bukan bug CMS — ini salah paham kontrak. Di site satu bahasa, entry.id kebetulan sama dengan slug, jadi `"/" + entry.id` berfungsi dan semua template ditulis dengan pola itu. Begitu i18n aktif, bentuk ID-nya diam-diam berubah membawa locale, dan semua `basePath + "/" + entry.id` di codebase langsung salah serentak.
Ada jebakan halus di perilaku yang sama: `"/" + entry.id` tanpa base path justru tetap benar secara kebetulan (hasilnya /id/{slug}). Beberapa halaman kami — arsip tag campur locale, daftar post tersimpan — tetap jalan persis karena itu, yang bikin bug-nya kelihatan tidak konsisten dan lebih susah dilacak.
Solusinya: satu helper URL, dipakai di mana-mana
Berhenti total menurunkan URL dari entry.id. Bangun dari field yang maknanya tidak bisa bergeser:
// src/lib/post-path.ts
export function postPath(post: {
id: string;
data?: { slug?: string | null; locale?: string | null };
}): string {
const slug = post.data?.slug;
if (!slug) return "/" + post.id; // entry.id sudah membawa prefix
const locale = post.data?.locale;
return !locale || locale === "en" ? "/" + slug : "/" + locale + "/" + slug;
}Lalu ganti semua penggabungan string manual. Di site kami itu berarti:
- Feed homepage — kartu hasil render server maupun kartu infinite scroll
- Daftar blog dan grid kartu post bersama
- Related post, navigasi artikel sebelumnya/berikutnya, dan kotak "Baca juga"
- Sidebar "Latest Articles"
- Endpoint JSON
/api/posts— sekarang mengembalikan fieldurlyang sudah jadi, supaya script sisi client berhenti merakit URL dari ID mentah - Post tersimpan di halaman profil, dan hasil pencarian
Poin API-nya penting: script infinite scroll merender ulang kartu di JavaScript sisi client, jadi tidak bisa memanggil helper server. Mengembalikan URL final dari API menjaga logika URL tetap di satu tempat saja.
Akar masalah sama, korban lebih banyak
Begitu tahu bentuk ID-nya berubah, audit semua yang mengonsumsinya. Kami menemukan tiga kerusakan lain yang bersembunyi di balik yang kelihatan:
- Share URL dan JSON-LD — halaman artikel membangun URL halamannya dari
entry.id, jadi structured data dan tombol share ikut membawa URL dobel. - Tombol simpan post — dia mengirim
entry.id("id/slug") ke/api/saved-posts/[id], route satu segmen. Garis miringnya bikin request 404: menyimpan post terjemahan ternyata sudah lama rusak diam-diam. - Hasil pencarian — gejala kebalikannya: link dibangun dari slug polos tanpa prefix locale, mengirim hasil bahasa Indonesia ke
/{slug}. Arah beda, penyakit sama: URL dirakit dari field yang salah.
Perlu redirect untuk URL yang salah?
Cek dulu URL dobel itu sebenarnya balas apa, sebelum buru-buru pasang aturan redirect:
curl -I https://site-kamu.com/id/id/suatu-slug
# HTTP/1.1 404 Not FoundDi kasus kami route /id/[slug] hanya cocok dengan satu segmen path, jadi URL dobel selalu 404 — tidak pernah balas 200, mesin pencari tidak sempat mengindeks apa pun, dan memperbaiki link-nya saja sudah cukup. Kalau routing kamu ternyata melayani konten di dua URL sekaligus, pilih satu yang kanonis dan 301-kan sisanya — redirect manager EmDash menyelesaikannya dalam beberapa klik.
Gotcha bonus dari sesi yang sama: artikel ini sendiri menolak di-save
Ada ironinya. Waktu post ini dimasukkan ke CMS dan kami coba pasang featured image, setiap save gagal dengan 403 Forbidden teks polos. CMS-nya tidak salah, permission juga tidak: site ini berada di belakang CDN yang Web Application Firewall-nya menilai setiap body request untuk pola yang mirip injection. Artikel teknis ya persis seperti itu. Contoh kode di atas penuh sintaks placeholder dan one-liner shell, dan setiap save mengirim seluruh isi artikel bersama field apa pun yang kamu ubah. Tiap snippet menambah beberapa poin ke skor anomali; cukup banyak snippet dalam satu request dan firewall memblokirnya di edge — sebelum sempat menyentuh CMS.
Cara mengenali varian "gagal save mulu" yang ini:
- Responsnya teks polos (cuma kata "Forbidden"), bukan bentuk error JSON dari CMS-mu — dan header
Servermenyebut nama CDN, bukan aplikasimu. - Post yang sama bisa di-save kalau isinya ringan, dan post lain aman — hanya save berisi konten berat yang mati.
- Mengulang payload save yang sama tanpa cookie sesi balas 403, bukan 401 — bukti blokirnya terjadi sebelum autentikasi dicek, alias di depan aplikasimu.
Dua jalan keluar: izinkan path admin CMS melewati WAF (solusi permanen), atau tulis contoh kode secara defensif — konkatenasi string alih-alih template literal, tanpa pipe di contoh shell. Kebetulan, itulah alasan snippet di artikel ini ditulis gaya konkatenasi.
Pelajaran yang bisa diambil
- Perlakukan
entry.idsebagai identifier route yang opaque. URL publik datang daridata.locale+data.slug— field yang maknanya tidak berubah saat konfigurasi berubah. - Pusatkan pembentukan URL. Perbaikan kami menyentuh lebih dari sepuluh titik karena setiap template merakit string-nya sendiri. Dengan helper
postPath()sejak hari pertama, ini cukup perbaikan satu baris. - Singkirkan kemungkinan data rusak dengan query tiga puluh detik sebelum menyentuh kode. Segmen URL dobel baunya seperti slug rusak; buktikan bukan, lalu lanjut dengan yakin.
- Saat mengaktifkan locale kedua, telusuri seluruh navigasi locale itu — setiap list, setiap widget, setiap kartu hasil render client. Halaman bahasa Inggris kami menutupi bug ini sepenuhnya, dan "site-nya jalan kok" selama ini cuma pernah diuji dalam bahasa Inggris.
Setup yang terdampak
Teramati di site EmDash + Astro dengan i18n aktif (dua locale, locale default tanpa prefix). Site satu bahasa kebal secara struktural, karena entry.id di sana sama dengan slug. Perilakunya sendiri mengikuti cara content layer Astro menamai entri terlokalisasi, jadi polanya — beserta solusinya — berlaku untuk kebanyakan setup content collection multibahasa, bukan cuma stack ini.








Komentar