D
P
0
← Semua artikel Read in English

Next.js & React di Produksi

`--` di MDX Berubah Jadi Em Dash Setelah Pindah ke Astro? `smartypants` Menyala Bawaan, Hanya `false` yang Mematikannya

· · 8 menit baca
`--` di MDX Berubah Jadi Em Dash Setelah Pindah ke Astro? `smartypants` Menyala Bawaan, Hanya `false` yang Mematikannya

Ada satu aturan gaya yang saya pegang untuk semua artikel di blog ini: tidak ada em dash di prosa, baik di versi Indonesia maupun Inggris. Di dalam blok kode, em dash tetap diizinkan. Waktu blog ini akan dipindahkan dari Next.js ke Astro, ternyata ada satu opsi yang bisa melanggar aturan itu di artikel mana pun yang memuat dua strip di prosa, tanpa satu pun berkas artikel berubah. Opsinya bernama smartypants, dan di Astro dia menyala selama tidak dimatikan.

Ketahuan sebelum ada kodenya

Rencana saya untuk membangun ulang situs ini di Astro sudah jadi dan lolos tinjauan adversarial pada 6 September 2026. Saat itu folder Astro-nya baru berisi kerangka 7.3.1 polos hasil npm create astro dengan dependensi terpasang, dan belum ada satu baris kode pun yang ditulis. Yang akan disalin ke sana adalah 238 pasang berkas MDX berisi markdown murni plus frontmatter, yang hari itu sudah dipastikan tidak memuat JSX. Satu pasang berarti versi Indonesia dan versi Inggris, jadi 238 kali dua menghasilkan 476 artikel.

Tinjauan itu menghasilkan 22 temuan, tiga di antaranya berderajat berat, dan semuanya diterima setelah masing-masing dicek ulang ke sumbernya. Dari tiga temuan yang dianggap paling penting karena masing-masing akan mengirim cacat nyata ke situs, smartypants ada di urutan pertama. Isinya: smartypants menyala secara bawaan di prosesor markdown Astro dan mengubah -- menjadi en dash atau em dash. Catatan temuan itu menulis bahwa aturan tanpa em dash akan dilanggar diam-diam di 476 artikel sekaligus. Angka itu tidak lain jumlah seluruh korpus tadi, jadi yang digambarkannya adalah jangkauan opsi: setiap artikel akan lewat prosesor yang sama. Pelanggarannya sendiri hanya terjadi di artikel yang memuat dua strip di prosa.

Setidaknya kata diam-diam di situ tepat. Di situs Next.js lama, artikel dirender MDXRemote dengan remarkGfm sebagai satu-satunya plugin remark, ditambah rehypeSlug dan rehypePrettyCode di sisi rehype. Tidak ada smartypants di daftar itu. Berkas MDX yang sama, disalin apa adanya ke Astro, akan keluar dengan tanda baca yang berbeda kalau opsi itu dibiarkan menyala, dan tidak ada diff yang memperlihatkannya, karena perubahan itu terjadi di prosesor saat build, bukan di berkasnya.

Em dash, bukan en dash

Tinjauan tadi masih menulis en dash atau em dash, dan dokumentasi opsi markdown.smartypants di definisi tipe Astro 7.3.1 juga menyebut keduanya. Menurut dokumentasi itu, bawaannya true, dan formatter SmartyPants mengubah kutip lurus jadi kutip pintar, tanda hubung jadi "en/em dashes", serta titik tiga jadi elipsis. Dugaan awalnya en dash. Spike pada 8 September 2026, percobaan kecil sebelum membangun yang sebenarnya, mengukurnya langsung, dan hasilnya em dash. Itu persis karakter yang dilarang aturan blog ini.

Dua kemungkinan itu memang ada, tapi bergantung mode. Dokumentasi opsi retext-smartypants, paket yang melakukan penggantiannya, menulis bahwa dashes bawaannya true, yang mengubah dua tanda hubung jadi em dash, sedangkan mode 'oldschool' mengubah tiga tanda hubung jadi em dash dan dua tanda hubung jadi en dash. Kodenya cocok dengan penjelasan itu. Nilai dashes apa pun yang bukan 'inverted', bukan 'oldschool', dan bukan false memakai dashesDefault, dan fungsi itulah yang mengganti --:

// retext-smartypants 6.2.0, lib/index.js (dikutip sebagian)
if (settings.dashes === 'inverted') {
  methods.push(dashesInverted)
} else if (settings.dashes === 'oldschool') {
  methods.push(dashesOldschool)
} else if (settings.dashes !== false) {
  methods.push(dashesDefault)
}
 
/**
 * Transform two dashes into an em dash.
 *
 * @type {Method}
 */
function dashesDefault(_, node) {
  if (node.value === '--') {
    node.value = '—'
  }
}

Astro sendiri tidak memilih mode. Kalau nilainya true, plugin dipasang dengan objek opsi kosong, jadi dashes tidak pernah diisi dan yang berlaku adalah cabang terakhir di atas. Spike yang sama juga mencatat titik tiga berubah jadi elipsis dan kutip lurus jadi kutip keriting, sementara kata ulang bertanda hubung tunggal seperti benar-benar aman.

Wilayahnya hanya prosa

Ada satu hal yang membatasi jangkauannya. Plugin yang dipasang Astro, remark-smartypants 3.0.3, hanya mengunjungi node bertipe text, inlineCode, paragraph, heading, dan tableCell, dan dari semua itu hanya node text yang nilainya ditulis ulang:

// remark-smartypants 3.0.3, dist/plugin.js (dikutip sebagian)
const VISITED_NODES = new Set([
    "text",
    "inlineCode",
    "paragraph",
    "heading",
    "tableCell",
]);
// ...
if (node.type === "text") {
    const processedText = allText.slice(startIndex, endIndex);
    node.value = processor2.processSync(processedText).toString();
}

Blok kode berpagar, yang di pohon markdown bertipe code, tidak ada di daftar itu. Kode sebaris memang dikunjungi, tapi nilainya tidak diubah. Jadi -- di contoh perintah terminal, baik di blok kode maupun kode sebaris, dibiarkan apa adanya. Yang berubah adalah teks biasa, dan itu persis wilayah yang dijaga aturan blog ini.

Kenapa hanya false yang mematikannya

Komentar yang sekarang berdiri di atas baris perbaikannya berbunyi: wajib false dan wajib eksplisit, karena bawaannya true dan hanya nilai false yang mematikannya. Dua klaim itu bisa ditelusuri di kode Astro sendiri.

Nilai bawaan konfigurasi markdown di @astrojs/internal-helpers menetapkan smartypants: true. createMarkdownProcessor di @astrojs/markdown-remark 7.3.0 memakai bawaan itu kalau opsinya tidak disetel, lalu memasang remark-smartypants selama nilainya bukan false. Jalur MDX di paket yang sama punya pemeriksaan serupa:

// @astrojs/markdown-remark 7.3.0, dist/mdx/create-processor.js (dikutip sebagian)
smartypants: shared.smartypants ?? options.smartypants ?? true,
// ...
if (mdxOptions.smartypants !== false) {
  const smartypantsConfig = typeof mdxOptions.smartypants === "object" ? mdxOptions.smartypants : {};
  remarkPlugins.push([remarkSmartypants, smartypantsConfig]);
}

Operator ?? hanya pindah ke ruas berikutnya kalau ruas kirinya null atau undefined. Opsi yang tidak disetel di mana pun karenanya berakhir di true, sedangkan false tidak pernah dilewati. Pemeriksaan sesudahnya memakai !== false, jadi hanya false itu sendiri yang mencegah plugin dipasang. Dokumentasi Astro juga menulis bahwa untuk mematikannya, smartypants disetel ke false. Tidak menulis apa-apa sama artinya dengan menyalakannya.

Berkas artikel blog ini berekstensi .mdx, dan dua koleksi kontennya, satu per bahasa, memuat *.mdx. Jadi yang menentukan adalah integrasi @astrojs/mdx 8.0.0. Integrasi itu memakai markdown.processor dari konfigurasi Astro untuk berkas MDX selama extendMarkdownConfig menyala, dan bawaan opsi itu true. Kalau mdx() tidak menyetel smartypants sendiri, nilainya diambil dari prosesor tadi. Kalau tidak ada prosesor yang dikonfigurasi sama sekali, integrasinya memakai satteri(), dan komentar di skema konfigurasi Astro 7.3.1 menyebut prosesor aktif itu memasok bawaan dengan smart punctuation menyala. Prosesor mana pun yang dipakai, titik awalnya menyala.

Satu catatan khusus Astro 7.3.1: menaruh smartypants langsung di bawah markdown membuat validator konfigurasi mengeluarkan peringatan bahwa opsi itu usang. Peringatannya menyarankan memindahkannya ke prosesor, misalnya unified({ gfm: false, smartypants: false }) dari @astrojs/markdown-remark atau satteri({ features: { gfm: false, smartPunctuation: false } }), dan menyebut opsi lama itu akan dibuang di versi mayor mendatang.

Perbaikannya

Rencana disunting supaya menyetel smartypants: false dan menjadikan grep atas HTML hasil build sebagai gerbang. Prosesor situs ini unified() plus rehype-pretty-code, bukan Sätteri, karena alasan lain yang tercatat di rencana: Sätteri diam-diam mengabaikan plugin rehype di Astro 7, padahal CSS blog bergantung pada bentuk figure dan data-line. Maka false itu ditaruh di dalam unified(), lengkap dengan hasil ukur spike sebagai komentarnya:

// astro.config.mjs (dipangkas ke bagian yang relevan)
import { unified } from '@astrojs/markdown-remark';
 
export default defineConfig({
  // ...
  integrations: [mdx(), react()],
 
  markdown: {
    syntaxHighlight: false,
    processor: unified({
      // WAJIB false dan wajib eksplisit. Bawaannya true, dan hanya nilai false
      // yang mematikannya. Terukur di spike: "--" berubah jadi em dash (bukan en
      // dash seperti dugaan awal), titik tiga jadi elipsis, kutip lurus jadi
      // keriting. Kata ulang bertanda hubung tunggal aman.
      smartypants: false,
      // ...
    }),
  },
  // ...
});

Blok itu masuk lewat commit fondasi repo Astro pada 8 September 2026 pukul 09.38.55. Pesan commit-nya menyatakan konfigurasinya sudah mencerminkan hasil spike, bukan draf rencana, dan mencatat alasannya secara singkat: bawaannya true dan mengubah dua tanda hubung jadi em dash.

Karena mdx() dipasang tanpa opsi, extendMarkdownConfig tetap pada bawaannya, berkas MDX memakai prosesor yang sama, dan integrasi MDX mengambil false dari sana. Versi yang dikunci di package.json adalah astro 7.3.1, @astrojs/mdx 8.0.0, dan @astrojs/markdown-remark 7.3.0, sementara lockfile-nya tetap memuat remark-smartypants 3.0.3 dan retext-smartypants 6.2.0. Paketnya masih terpasang. Yang dimatikan adalah pemakaiannya.

Yang terbukti, dan yang belum jadi gerbang

Hari itu juga, 476 halaman artikel plus indeks dua bahasa sudah berdiri di Astro dengan build 8 detik. Pemeriksaan di tahap itu menyatakan smartypants terbukti mati. Korpus memuat 26 em dash, semuanya di dalam blok kode dan nol di prosa, dan tiga di antaranya justru wajib ada karena em dash itu nilai yang dirender contoh kodenya. Pesan commit tahap itu mencatat hal yang sama dari sisi pipeline: nol tanda tipografis diproduksi. Commit itu bertanda 10.59.51, jadi dari commit fondasi pukul 09.38.55 jaraknya 1 jam 20 menit 56 detik. Di hari yang sama, domain utama blog ini mulai dilayani situs Astro.

Yang belum ada juga perlu saya tulis terang. Rencana meminta grep atas HTML hasil build sebagai gerbang, tapi yang tercatat adalah hasil sapuan di tahap itu, bukan langkah yang berjalan di setiap build. Hook prebuild di package.json menjalankan pemeriksaan env lalu pembangun graph, postbuild memeriksa bundel server, dan gerbang em dash yang tertulis di README menyapu folder src/, bukan keluaran build. Jadi yang menjaga smartypants tetap mati dari build ke build adalah baris konfigurasi tadi. Baris itu tinggal di dalam unified(), sementara satteri() membawa bawaannya sendiri yang menyala, sehingga mengganti prosesor berarti ikut membuang false-nya. Kalau sapuan HTML itu kelak dijadikan langkah otomatis, dia harus bisa membedakan prosa dari blok kode, karena 26 em dash yang sah tadi ikut tercetak di halaman.

Pelajaran

Memindahkan konten ke renderer lain bukan cuma urusan komponen dan rute. Tanda baca juga bagian dari keluaran, dan opsi yang mengubahnya bisa menyala tanpa satu baris pun di repo yang menyebut namanya. Sebelum menyalin konten ke renderer baru, daftar bawaan prosesor markdown-nya layak dibaca, dan setiap opsi yang menulis ulang teks dimatikan secara eksplisit. Di Astro, eksplisit berarti benar-benar false, karena pemeriksaannya !== false.

Pertanyaan en atau em juga bukan soal selera. Aturan blog ini melarang em dash, jadi karakter mana yang keluar menentukan apakah aturannya dilanggar, dan dokumentasi yang menyebut dua kemungkinan tidak bisa menjawabnya. Yang menjawab adalah spike.

Terakhir, konfigurasi menyatakan niat, sedangkan HTML hasil build menunjukkan hasilnya. Sapuan di tahap migrasi membuktikan smartypants mati hari itu. Menjadikannya langkah yang ikut berjalan di setiap build adalah pekerjaan lain, dan selama langkah itu belum ada, sapuan tadi belum boleh disebut gerbang.