Memproses Markdown dan MDX dengan Aman memakai Claude Code
Proses Markdown/MDX dengan Claude Code: AST, frontmatter, XSS, link, dan QA locale.
Markdown bukan sekadar teks
Artikel Markdown atau MDX yang sudah publish bukan hanya kumpulan paragraf. Di dalamnya ada frontmatter, SEO description, struktur heading, anchor yang dihasilkan otomatis, code fence, internal link, referensi resmi, route per bahasa, dan kadang raw HTML. Jika Anda meminta Claude Code “rapikan artikel ini” tanpa kontrak teknis, tulisannya bisa lebih enak dibaca tetapi slug berubah, CTA hilang, hero image tersentuh, atau salah satu locale menjadi ringkasan tipis.
Pendekatan yang aman adalah memisahkan penulisan dan verifikasi. Claude Code boleh menulis ulang, melokalkan, dan memperluas konten. Struktur harus diperiksa dengan script. Markdown dan MDX sebaiknya dibaca sebagai AST, yaitu abstract syntax tree. Frontmatter divalidasi seperti data. HTML harus melewati batas sanitization yang jelas. Semua file locale diperiksa sebagai satu paket.
Referensi utama sudah saya verifikasi pada 2 Juni 2026. unified guide menjelaskan pipeline parse, transform, dan stringify. Panduan syntax trees menjelaskan mengapa AST lebih aman daripada membaca baris mentah. Untuk Markdown, lihat remark dan remark-parse. Untuk MDX, rujuk dokumentasi MDX. Untuk frontmatter, gunakan gray-matter. Untuk HTML dan XSS, bandingkan rehype-sanitize dengan OWASP XSS Prevention Cheat Sheet. Untuk batas kerja agent, baca Claude Code overview dan settings.
flowchart LR
A["File MDX"] --> B["frontmatter"]
B --> C["schema validation"]
A --> D["remark / MDX AST"]
D --> E["heading, fence, link"]
D --> F["pipeline rehype"]
F --> G["sanitize"]
C --> H["locale dan build checks"]
E --> H
G --> H
Pilih parser sebelum Claude Code mengedit
Instruksi pertama untuk Claude Code harus menyebut toolchain. “Parse Markdown” terlalu luas. Untuk contoh kecil, regex terlihat cukup. Untuk artikel produksi, regex sering membaca heading palsu di dalam code block atau melewatkan sintaks MDX.
| Kebutuhan | Pilihan lebih aman | Shortcut berisiko |
|---|---|---|
| Membaca heading, link, code fence | remark-parse dengan AST traversal | Regex ^## pada teks |
Mendukung JSX di .mdx | remark-mdx atau compiler MDX | Parser Markdown saja |
| Menghasilkan HTML | remark-rehype ke rehype pipeline | Menyambung string HTML |
| Mengizinkan raw HTML | rehype-raw lalu rehype-sanitize | Hanya allowDangerousHtml |
| Membaca frontmatter | gray-matter dan schema checks | Split YAML manual |
AST memisahkan makna. ## heading palsu di dalam code fence tidak boleh masuk daftar isi. URL di props komponen MDX belum tentu link editorial. tags: Claude Code, Markdown dalam YAML adalah string, bukan array. Parser dan schema validation menangkap masalah ini lebih cepat daripada review manual.
Empat use case praktis
Use case pertama adalah refresh artikel yang sudah publish. Title, description, updatedDate, link resmi, internal link, contoh kode, dan CTA harus diperbarui bersama. Di ClaudeCodeLab, alur ini bisa tersambung ke best practices CLAUDE.md dan web scraping dengan Claude Code, tetapi tidak boleh menyentuh slug lain.
Use case kedua adalah dokumentasi dengan komponen MDX. Callout, tab, pricing card, FAQ, dan contoh live berguna, tetapi mencampur Markdown dan JSX. Checker yang tidak memahami MDX mudah merusak komponen atau melewatkan link penting.
Use case ketiga adalah publishing multibahasa. Canonical Jepang yang kuat tidak cukup jika bahasa Inggris, Spanyol, Prancis, Hindi, atau Indonesia hanya ringkasan. Setiap locale perlu contoh konkret, failure mode, snippet yang bisa dijalankan, link resmi, internal link, CTA, dan catatan verifikasi.
Use case keempat adalah operasi konten komersial. Halaman Gumroad, training, resource gratis, dan email sering memakai Markdown. Semakin dekat konten ke pembelian atau konsultasi, semakin penting code fence, link, dan keamanan HTML.
Setup minimum yang bisa disalin
Contoh berikut memakai Node.js 18 atau lebih baru dan ESM. Jalankan di folder demo dulu sebelum dipindah ke repo produksi.
mkdir mdx-audit-demo
cd mdx-audit-demo
npm init -y
npm pkg set type=module
npm install unified remark-parse remark-mdx remark-gfm gray-matter
npm install unist-util-visit github-slugger
npm install remark-rehype rehype-raw rehype-sanitize rehype-stringify
mkdir tools
Contoh 1: audit frontmatter, heading, fence, dan link
Script ini membaca frontmatter dengan gray-matter, mem-parse body dengan remark dan dukungan MDX, lalu gagal jika field wajib kosong, description lebih dari 120 karakter, code fence tidak punya bahasa, atau tidak ada internal dan external link.
// tools/audit-mdx.mjs
import fs from "node:fs/promises";
import matter from "gray-matter";
import GithubSlugger from "github-slugger";
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkMdx from "remark-mdx";
import remarkGfm from "remark-gfm";
import { visit } from "unist-util-visit";
const file = process.argv[2];
if (!file) {
throw new Error("Usage: node tools/audit-mdx.mjs article.mdx");
}
const source = await fs.readFile(file, "utf8");
const { data, content } = matter(source);
const errors = [];
const links = { internal: [], external: [] };
const headings = [];
const codeBlocks = [];
for (const key of ["title", "description", "pubDate", "heroImage", "lang"]) {
if (typeof data[key] !== "string" || data[key].trim() === "") {
errors.push(`frontmatter.${key} is required`);
}
}
if ([...String(data.description ?? "")].length > 120) {
errors.push("description must be 120 characters or fewer");
}
if (!Array.isArray(data.tags) || data.tags.length === 0) {
errors.push("frontmatter.tags must be a non-empty array");
}
const tree = unified()
.use(remarkParse)
.use(remarkMdx)
.use(remarkGfm)
.parse(content);
const slugger = new GithubSlugger();
visit(tree, (node) => {
if (node.type === "heading") {
const text = plainText(node);
headings.push({ depth: node.depth, text, slug: slugger.slug(text) });
}
if (node.type === "code") {
codeBlocks.push({ lang: node.lang || "", meta: node.meta || "" });
if (!node.lang) errors.push("code fence is missing a language");
}
if (node.type === "link") {
const url = String(node.url || "");
if (url.startsWith("http")) links.external.push(url);
if (url.startsWith("/")) links.internal.push(url);
}
});
if (links.internal.length === 0) errors.push("missing internal link");
if (links.external.length === 0) errors.push("missing external link");
if (errors.length > 0) {
console.error(errors.map((error) => `- ${error}`).join("\n"));
process.exit(1);
}
console.log(JSON.stringify({ headings, codeBlocks, links }, null, 2));
function plainText(node) {
if (typeof node.value === "string") return node.value;
if (!Array.isArray(node.children)) return "";
return node.children.map(plainText).join("");
}
node tools/audit-mdx.mjs site/src/content/blog-id/example.mdx
Contoh 2: mengubah Markdown menjadi HTML aman
Jika raw HTML tidak diperlukan, jangan aktifkan. Jika diperlukan, parse lalu sanitize langsung. allowDangerousHtml saja bukan strategi keamanan.
// tools/markdown-to-safe-html.mjs
import fs from "node:fs/promises";
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkGfm from "remark-gfm";
import remarkRehype from "remark-rehype";
import rehypeRaw from "rehype-raw";
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
import rehypeStringify from "rehype-stringify";
const file = process.argv[2];
const markdown = await fs.readFile(file, "utf8");
const schema = {
...defaultSchema,
attributes: {
...defaultSchema.attributes,
code: [["className", /^language-/]],
},
};
const html = await unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeSanitize, schema)
.use(rehypeStringify)
.process(markdown);
console.log(String(html));
Urutannya penting. rehype-raw mengembalikan raw HTML ke HTML tree. rehype-sanitize menghapus tag dan atribut yang tidak diizinkan. Tanpa langkah kedua, konten berisiko bisa sampai ke DOM.
Contoh 3: mengecek sepuluh file locale
Script ini memastikan slug yang sama ada di semua bahasa, heroImage tetap sama, updatedDate benar, description tidak terlalu panjang, dan body punya link internal serta eksternal.
// tools/check-locales.mjs
import fs from "node:fs";
import path from "node:path";
import matter from "gray-matter";
const slug = "claude-code-markdown-processing.mdx";
const expectedHero = "/images/hero/hero-077.png";
const locales = [
["ja", "site/src/content/blog"],
["en", "site/src/content/blog-en"],
["zh", "site/src/content/blog-zh"],
["ko", "site/src/content/blog-ko"],
["es", "site/src/content/blog-es"],
["fr", "site/src/content/blog-fr"],
["de", "site/src/content/blog-de"],
["pt", "site/src/content/blog-pt"],
["hi", "site/src/content/blog-hi"],
["id", "site/src/content/blog-id"],
];
const errors = [];
for (const [lang, dir] of locales) {
const file = path.join(dir, slug);
const source = fs.readFileSync(file, "utf8");
const { data, content } = matter(source);
if (data.lang !== lang) errors.push(`${lang}: lang mismatch`);
if (data.heroImage !== expectedHero) errors.push(`${lang}: hero changed`);
if (data.updatedDate !== "2026-06-02") {
errors.push(`${lang}: updatedDate mismatch`);
}
if ([...String(data.description ?? "")].length > 120) {
errors.push(`${lang}: description too long`);
}
if (!content.includes("https://")) errors.push(`${lang}: no external link`);
if (!content.includes("](/")) errors.push(`${lang}: no internal link`);
}
if (errors.length > 0) {
console.error(errors.map((error) => `- ${error}`).join("\n"));
process.exit(1);
}
console.log("locale set is consistent");
Failure mode yang perlu disebutkan
| Failure | Dampak | Guardrail |
|---|---|---|
| Heading dibaca dengan regex | Heading palsu di code block masuk TOC | Baca node heading saja |
tags menjadi string | Filter dan related posts rusak | Validasi tipe frontmatter |
| Slug tidak konsisten | Anchor link pecah per bahasa | Pakai slugger yang sama |
| Raw HTML dipercaya | Risiko XSS lewat tag atau atribut | Sanitize dengan schema |
| External link tidak dicek | Dokumen resmi pindah tanpa terlihat | Tes sebelum publish |
| Prompt terlalu luas | File worker lain ikut berubah | Kunci owned_files |
Failure ini harus masuk prompt. Claude Code lebih stabil dengan batas yang bisa diuji daripada instruksi umum seperti “buat lebih berkualitas”.
Prompt aman untuk Claude Code
task: "Refresh one published MDX article"
owned_files:
- "site/src/content/blog-id/claude-code-markdown-processing.mdx"
preserve:
- "slug path"
- "heroImage"
- "unrelated dirty files"
required:
- "updatedDate: 2026-06-02"
- "description <= 120 characters"
- "AST-based Markdown checks"
- "official external links"
- "internal links and monetization CTA"
forbidden:
- "regex-only heading parsing"
- "raw HTML without sanitization"
- "thin locale summaries"
verification:
- "node scripts/check-code-fences.mjs"
- "node scripts/check-updated-article-quality.mjs"
Cek sebelum publish dan CTA
Sebelum publish, jalankan script dan review manusia. Script memeriksa struktur, metadata, fences, link, dan kedalaman body. Reviewer memeriksa bahasa lokal, mobile readability, search intent, dan CTA.
node tools/audit-mdx.mjs site/src/content/blog-id/claude-code-markdown-processing.mdx
node tools/check-locales.mjs
node scripts/check-code-fences.mjs
node scripts/check-updated-article-quality.mjs
Untuk workflow individu, mulai dari cheatsheet Claude Code gratis. Jika perlu prompt review dan writing yang bisa dipakai ulang, gunakan Claude Code prompt templates. Untuk tim yang ingin menata permission, CI, locale workflow, dan editorial review, lanjut ke training dan konsultasi Claude Code.
Hasil verifikasi praktis
Dalam refresh ini, Masa memperlakukan artikel sebagai content pipeline, bukan sekadar rewrite prose. Checklist mencakup panjang description, updatedDate, heroImage, bahasa pada code fence, link resmi, kedalaman locale, dan CTA. Audit berbasis AST menangkap kasus yang sering lolos dari regex, terutama heading di dalam code block dan sintaks MDX dekat komponen. Perintah akhir adalah node scripts/check-code-fences.mjs dan node scripts/check-updated-article-quality.mjs. Pelajaran utamanya: Claude Code jauh lebih dapat diandalkan saat kontrak artikel bisa dieksekusi, bukan hanya saat prompt meminta “kualitas lebih baik”.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Permission safety ladder Claude Code: perluas akses tanpa kehilangan kontrol
Naik dari read-only ke edit terbatas, command bukti, dan cek deploy dengan kontrol yang jelas.
Claude Code Small PR Proof Pack: perubahan kecil yang mudah direview
Paket bukti untuk PR Claude Code: diff, check, URL publik, jalur CTA, dan rollback.
Review gate Claude Code sebelum commit: diff, test, URL publik, dan CTA
Cara memakai Claude Code sebelum commit: diff scope, build, URL publik, link Gumroad, CTA konsultasi, missing test, dan file tidak terkait.