Pengembang di sisi klien mengirim sebuah CSV ekspor Google Sheet: 33 URL, masing-masing dengan meta description yang sudah disetujui, untuk halaman-halaman yang sampai saat itu belum punya meta description sama sekali. Permintaannya jelas, unggah meta description ini dulu sebelum pekerjaan yang lain. Saya sedang memegang satu batch perbaikan lain untuk situs yang sama, dan saya putuskan 33 URL ini masuk lebih dulu.
Yang saya cari sejak awal adalah metode yang tidak butuh unggah berkas, tidak butuh membuka UI tiap halaman, dan bisa dipakai seragam untuk Page, semua custom post type tema, dan post type podcast. Sebagian alasannya datang dari pekerjaan sebelumnya di situs yang sama: halaman settings Rank Math ternyata React SPA, state field-nya hidup di React dan bukan di input DOM, jadi satu-satunya cara menggerakkannya dari skrip adalah klik Playwright sungguhan, bukan set nilai lewat JavaScript. Kalau untuk halaman settings saja sudah begitu, menggerakkan UI 33 kali bukan jalan yang mau saya tempuh.
Rank Math punya endpoint REST sendiri
Yang akhirnya saya pakai adalah endpoint internal milik Rank Math sendiri: POST /wp-json/rankmath/v1/updateMeta. Body-nya objek dengan objectID, objectType: 'post', dan meta yang berisi rank_math_description. Header X-WP-Nonce diisi dari window.wpApiSettings.nonce, nonce yang sudah saya kenal dari pekerjaan redirect sehari sebelumnya, dan kalau sukses endpointnya membalas {slug: true}.
Semua ini dijalankan dari browser dalam sesi wp-admin live, jadi cookie login ikut terkirim dan nonce-nya valid.
// dijalankan di browser, dalam sesi wp-admin yang sudah login
async function setDescription(objectID, description) {
const res = await fetch('/wp-json/rankmath/v1/updateMeta', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': window.wpApiSettings.nonce,
},
body: JSON.stringify({
objectID,
objectType: 'post',
meta: { rank_math_description: description },
}),
});
return res.json(); // {slug: true} kalau sukses
}Ada juga endpoint updateMetaBulk di sebelahnya. Waktu itu saya cuma mencatat keberadaannya dan tidak memakainya, jadi bentuk body-nya tidak saya bahas di sini.
Satu hal yang membuat metode ini seragam: objectType: 'post' berlaku untuk semua post type. Page, empat CPT tema, dan post type podcast semuanya lewat nilai yang sama, tanpa perlu tahu post type mana yang sedang disentuh.
Mengambil ID post dari halaman publik
CSV-nya berisi URL, bukan ID. Daripada mencari tiap ID lewat daftar admin, saya fetch halaman publiknya dengan parameter cache-buster dan membaca class postid-NNN di elemen <body>.
async function resolvePostId(url) {
const html = await (await fetch(url + '?cb=' + Date.now())).text();
const m = html.match(/postid-(\d+)/);
return m ? Number(m[1]) : null;
}Karakter khusus jangan diketik ulang
Meta description yang disetujui klien memuat karakter yang gampang rusak kalau lewat clipboard atau diketik ulang: simbol GBP, em dash, kutip keriting, tanda TM. Supaya byte-nya sampai utuh, teksnya saya bawa dari CSV ke browser lewat rantai yang tidak pernah menyentuh keyboard: Import-Csv -Encoding UTF8 di PowerShell, ConvertTo-Json, lalu base64, dan di browser dibuka kembali dengan atob plus TextDecoder.
$rows = Import-Csv .\meta-description.csv -Encoding UTF8
$json = $rows | ConvertTo-Json -Compress
[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($json))const rows = JSON.parse(
new TextDecoder().decode(
Uint8Array.from(atob(B64), c => c.charCodeAt(0))
)
);Encoding HTML-nya diurus Rank Math sendiri: & jadi &, < jadi <, sedangkan karakter UTF-8 tadi dibiarkan apa adanya. Artinya yang dikirim ke endpoint adalah teks mentah dari CSV, bukan versi yang sudah di-escape.
Verifikasi 33 dari 33
Tiap URL saya fetch ulang dengan cache-buster, ambil isi <meta name="description">, decode entitasnya, lalu bandingkan dengan salinan CSV. Hasilnya 33 dari 33 cocok, nol selisih.
Satu catatan buat yang memeriksa lewat URL kanonik tanpa parameter: perubahannya langsung terlihat lewat ?cb, tapi URL kanoniknya bisa tertinggal sampai sekitar satu jam karena TTL HTML di Cloudflare. Kalau kamu cek URL bersihnya dan masih melihat yang lama, itu cache, bukan gagal simpan.
Empat URL yang tidak ada di CSV
Audit setelahnya menemukan empat URL yang masih bolong dan memang tidak ada di lembar klien: dua halaman statis yang meta description-nya cuma sekitar 5 dan 12 karakter, dan dua URL arsip podcast yang tidak punya meta description sama sekali. Untuk keempatnya saya putuskan memakai metode updateMeta yang sama, tapi baru setelah copy-nya saya setujui sendiri; copy SEO publik tidak saya kirim tanpa ada yang menyetujuinya lebih dulu. Copy yang saya setujui tetap mempertahankan suara merek yang blak-blakan.
Keesokan harinya keempatnya diisi. Dua halaman statis lewat objectType: 'post' seperti biasa. Dua URL podcast ternyata bukan halaman: dari body class halaman publiknya (archive tax-... term-...), keduanya adalah arsip term taxonomy milik tema, bukan Page dan bukan term series milik plugin podcast. Untungnya Rank Math memang mengelola meta term taxonomy tema itu, jadi updateMeta dengan objectType: 'term' dan objectTypeID berisi nama taxonomy langsung muncul di frontend tanpa perubahan tema apa pun.
await fetch('/wp-json/rankmath/v1/updateMeta', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': window.wpApiSettings.nonce,
},
body: JSON.stringify({
objectID: termId, // dari class term-NNN di body
objectType: 'term',
objectTypeID: 'client_show', // nama taxonomy temanya
meta: { rank_math_description: description },
}),
});Keempatnya saya verifikasi byte-exact secara live, kali ini dengan credentials: 'omit' supaya yang diperiksa adalah apa yang dilihat pengunjung tanpa sesi login: <meta name="description"> dan og:description sama-sama cocok, simbol GBP dan apostrofnya utuh. Yang tersisa dari dua arsip term itu adalah canonical-nya kosong; saya catat sebagai isu minor terpisah dan tidak saya tangani di pekerjaan ini.
Setelah itu saya kembali ke batch yang sempat saya geser.
Pelajaran
- Plugin besar sering membawa REST-nya sendiri. Sebelum membangun route custom untuk menulis meta milik plugin, cek dulu apakah plugin itu sudah punya endpoint tulisnya.
objectType: 'post'di updateMeta menutup semua post type sekaligus; yang perlu dibedakan cuma term, lewatobjectType: 'term'danobjectTypeID.- ID post ada di body class halaman publik. Kalau URL-nya sudah di tangan, tidak perlu mencarinya lewat admin.
- Teks yang disetujui klien jangan pernah diketik ulang. Bawa byte-nya lewat base64 dan biarkan plugin yang mengurus encoding HTML.
- Verifikasi lewat URL bercache-buster dan tanpa kredensial. URL kanonik di belakang CDN bisa tertinggal sampai satu jam.