Procesamiento seguro de Markdown y MDX con Claude Code
Procesa Markdown/MDX con Claude Code: AST, frontmatter, XSS, enlaces y QA multilingüe.
Markdown no es solo texto
Un artículo publicado en Markdown o MDX combina muchas responsabilidades: frontmatter, título SEO, description, jerarquía de encabezados, anclas generadas, bloques de código, enlaces internos, enlaces oficiales, rutas por idioma y a veces HTML sin procesar. Si pides a Claude Code “mejora este artículo” sin un contrato técnico, puede mejorar el tono y al mismo tiempo romper un slug, borrar el CTA, cambiar la imagen principal o dejar una traducción como resumen fino.
La solución práctica es separar escritura y verificación. Claude Code puede reescribir, localizar y ampliar el contenido, pero la estructura debe comprobarse con herramientas. Para Markdown y MDX conviene usar AST, es decir, árbol de sintaxis abstracta. El frontmatter debe validarse como datos. El HTML debe pasar por una capa explícita de sanitización. Y los diez archivos de locale se revisan como un conjunto, no como archivos aislados.
Verifiqué las fuentes principales el 2 de junio de 2026. La guía de unified explica la cadena parse, transform y stringify, y la guía de syntax trees explica por qué un AST es más seguro que leer líneas sueltas. Para Markdown están remark y remark-parse. Para MDX está la documentación oficial de MDX. Para frontmatter uso gray-matter. Para seguridad HTML conviene contrastar rehype-sanitize con la guía OWASP de XSS. Para delimitar el trabajo del agente, revisa también Claude Code overview y settings.
flowchart LR
A["Archivo MDX"] --> B["frontmatter"]
B --> C["validación de schema"]
A --> D["AST de remark / MDX"]
D --> E["encabezados, fences, enlaces"]
D --> F["pipeline rehype"]
F --> G["sanitización"]
C --> H["locale y build checks"]
E --> H
G --> H
Elegir parser antes de pedir cambios
La primera instrucción a Claude Code debe decir qué herramientas usar. Si solo dices “parsea Markdown”, una expresión regular corta parece suficiente. En un blog real no lo es.
| Necesidad | Mejor opción | Atajo riesgoso |
|---|---|---|
| Leer encabezados, enlaces y bloques de código | remark-parse con recorrido AST | Regex ^## sobre texto |
Soportar JSX en .mdx | remark-mdx o compiler de MDX | Parser solo de Markdown |
| Generar HTML | remark-rehype hacia rehype | Concatenar strings HTML |
| Permitir raw HTML | rehype-raw y luego rehype-sanitize | Solo allowDangerousHtml |
| Leer frontmatter | gray-matter más validación | Dividir YAML a mano |
El AST distingue significado. Un ## título falso dentro de un bloque de código no debe entrar en la tabla de contenidos. Un link dentro de props de un componente MDX no siempre es un link de contenido. Y tags: Claude Code, Markdown en YAML es un string, no una lista. Esos errores se detectan mejor con parser y schema que con revisión visual.
Cuatro casos de uso concretos
El primer caso es refrescar un artículo publicado. Hay que actualizar title, description, updatedDate, enlaces oficiales, links internos, ejemplos de código y CTA. En ClaudeCodeLab, este flujo se conecta con buenas prácticas de CLAUDE.md y web scraping con Claude Code, sin tocar slugs ajenos.
El segundo caso es una documentación con componentes MDX. Callouts, tabs, tarjetas de precios, FAQ y ejemplos vivos son útiles, pero mezclan Markdown y JSX. Si el checker no entiende MDX, acabará rompiendo componentes o ignorando enlaces importantes.
El tercer caso es publicación multilingüe. Un canonical japonés fuerte no compensa que español, francés, portugués o indonesio sean resúmenes. Cada idioma necesita ejemplos, errores, snippets ejecutables, enlaces oficiales, enlaces internos, CTA y una nota de verificación.
El cuarto caso es contenido comercial. Páginas de Gumroad, training, recursos gratuitos y secuencias de email suelen vivir cerca de Markdown. Cuanto más cerca está el contenido de una compra o consulta, más importante es que código, links y HTML sean verificables.
Setup mínimo para copiar y pegar
Los ejemplos usan Node.js 18 o superior y módulos ESM. Empieza en una carpeta de prueba antes de moverlo al repositorio real.
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
Ejemplo 1: auditar frontmatter, encabezados, fences y enlaces
Este script lee frontmatter con gray-matter, parsea el cuerpo con remark y soporte MDX, y falla si faltan campos, la description supera 120 caracteres, falta lenguaje en un bloque de código o no hay enlaces internos y externos.
// 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-es/example.mdx
Ejemplo 2: convertir a HTML seguro
Si no necesitas raw HTML, no lo habilites. Si sí lo necesitas, parsea raw HTML y sanitiza justo después. allowDangerousHtml por sí solo no es una defensa.
// 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));
El orden importa. rehype-raw convierte HTML crudo en árbol HTML; rehype-sanitize elimina etiquetas y atributos no permitidos. Sin esa segunda fase, un atributo peligroso puede llegar al DOM renderizado.
Ejemplo 3: controlar los diez locales
Este script confirma que el mismo slug existe en todos los idiomas, que heroImage no cambió, que updatedDate está actualizado y que cada body incluye enlaces internos y externos.
// 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");
Fallos concretos que conviene anticipar
| Fallo | Resultado | Guardia |
|---|---|---|
| Leer encabezados con regex | Entrarán títulos falsos dentro de código | Recorrer nodos heading |
tags como string | Filtros y artículos relacionados fallan | Validar tipos de frontmatter |
| Slugs inconsistentes | Anclas rotas entre idiomas | Usar el mismo slugger |
| Confiar en raw HTML | Riesgo de XSS en atributos o tags | Sanitizar con schema |
| No revisar enlaces externos | Docs oficiales movidas sin aviso | Probar antes de publicar |
| Prompt demasiado amplio | Se modifican archivos de otros trabajadores | Fijar owned_files |
Estos fallos deben aparecer en el prompt. Claude Code trabaja mejor con restricciones verificables que con comentarios subjetivos como “hazlo más profesional”.
Prompt seguro para Claude Code
task: "Refresh one published MDX article"
owned_files:
- "site/src/content/blog-es/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"
Checklist de publicación y CTA
Antes de publicar, ejecuta scripts y revisión humana. Los scripts miran estructura, metadata, fences, enlaces y profundidad. La persona revisa naturalidad, intención de búsqueda, lectura móvil y CTA.
node tools/audit-mdx.mjs site/src/content/blog-es/claude-code-markdown-processing.mdx
node tools/check-locales.mjs
node scripts/check-code-fences.mjs
node scripts/check-updated-article-quality.mjs
Para empezar de forma individual, usa la cheatsheet gratuita de Claude Code. Si necesitas prompts repetibles para revisión y escritura, compra las plantillas de prompts de Claude Code. Para equipos que quieren permisos, CI, flujo multilingüe y revisión editorial, el siguiente paso es training y consultoría de Claude Code.
Resultado de la prueba práctica
En esta actualización, Masa trató el artículo como un pipeline real de contenido: description corta, updatedDate, preservación de heroImage, lenguajes en code fences, enlaces oficiales, locales completos y CTA interno. La auditoría basada en AST cubre errores que una regex no ve, especialmente encabezados dentro de código y MDX cerca de componentes. El cierre fue ejecutar node scripts/check-code-fences.mjs y node scripts/check-updated-article-quality.mjs. La lección práctica es que Claude Code mejora mucho cuando el contrato del artículo es ejecutable, no solo cuando el prompt pide “mejor calidad”.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Escalera de permisos de Claude Code para ampliar acceso sin perder control
Pasa de read-only a ediciones limitadas, comandos de prueba y checks de deploy con menos riesgo.
Claude Code Small PR Proof Pack: cambios pequeños que sí se pueden revisar
Un paquete de prueba para PRs de Claude Code: diff, checks, URL pública, CTA y rollback.
Gate de revisión antes del commit con Claude Code
Cómo revisar con Claude Code antes del commit: diff, build, URL pública, Gumroad, consultoría, tests y archivos ajenos.