Tips & Tricks (更新: 2026/6/2)

Claude CodeでMarkdown/MDX処理を安全に自動化する実装ガイド

Claude CodeでMarkdown/MDX処理を安全に刷新する実装ガイド。AST、frontmatter、XSS、リンク検査まで。

Claude CodeでMarkdown/MDX処理を安全に自動化する実装ガイド

Markdown処理をClaude Codeに任せる前に決めること

MarkdownやMDXの記事更新は、一見すると文字列置換の作業に見えます。しかし実務では、frontmatter、見出しID、コードフェンス、内部リンク、多言語ファイル、HTMLの安全性、ビルド確認が同時に絡みます。Claude Codeに「この記事を直して」とだけ依頼すると、本文は増えてもslugが変わる、descriptionが長くなる、コードブロックが壊れる、別localeだけ薄い要約になる、という事故が起きます。

この記事では、Claude CodeをMarkdown/MDX処理の作業者として使う前提で、正規表現ではなくASTを使う設計、frontmatter検証、コードフェンス検査、slug生成、XSS対策、リンクチェック、locale監査、ビルドチェック、そして安全な依頼文をまとめます。Markdownを単なるテキストではなく「構文木として扱う」ことが品質の分かれ目です。

公式情報は2026年6月2日時点で確認しました。unifiedはMarkdownやHTMLを構文木に変換して処理するエコシステムで、unifiedの入門ASTの説明が基本です。Markdown処理はremarkremark-parse、HTML側はrehype-sanitizeを確認します。MDXはMDX公式ドキュメント、frontmatterはgray-matter、XSSはOWASP XSS Prevention Cheat Sheetを参照します。Claude Codeの基本はClaude Code overviewsettingsを読んでおくと安全です。

flowchart LR
  A["MDX file"] --> B["frontmatter parser"]
  B --> C["schema validation"]
  A --> D["remark / MDX AST"]
  D --> E["headings, code fences, links"]
  D --> F["remark to rehype"]
  F --> G["sanitize HTML"]
  C --> H["locale and build checks"]
  E --> H
  G --> H

どのパーサーを選ぶか

最初に、Claude Codeに「どの道具を使ってよいか」を指定します。ここを曖昧にすると、短い正規表現で見出しやリンクを拾い、コードフェンス内のサンプルまで本文として処理してしまいます。

目的推奨避けたい実装
Markdownの見出し、リンク、コードを読むremark-parseとAST traversal^##だけを正規表現で拾う
MDXのJSXを含む記事を扱うremark-mdxまたはMDX公式コンパイラMarkdown専用パーサーだけで通す
HTMLへ変換するremark-rehypeからrehypeへ渡す文字列連結でHTMLを作る
raw HTMLを許可するrehype-raw後にrehype-sanitizeallowDangerousHtmlだけで公開する
frontmatterを読むgray-matterで分離し、別途schema検証YAMLを手書きsplitで読む

ASTとは抽象構文木のことです。Markdownの「見出し」「リンク」「コードブロック」を、ただの行ではなくノードとして扱います。これにより、コードフェンス内に## 見出しっぽい文字があっても、本物の見出しと区別できます。

実務で使う3つ以上のユースケース

1つ目は、公開済みブログ記事の品質リフレッシュです。タイトル、description、updatedDate、内部リンク、公式リンク、CTAを同時に見直すと、検索流入と収益導線の両方に効きます。関連するClaude Code運用はCLAUDE.mdのベストプラクティスも参考になります。

2つ目は、ドキュメントサイトや社内ポータルのMDXコンポーネント化です。注意書き、比較表、設定例、FAQをMDXに入れると便利ですが、JSXとMarkdownが混ざるため、正規表現だけの変換は危険です。

3つ目は、多言語記事の同期です。日本語canonicalだけ濃く、英語やスペイン語が薄い要約になると、SEOだけでなく読者体験も落ちます。localeごとにupdatedDateheroImage、内部リンクprefix、公式リンク、CTAを監査する必要があります。

4つ目は、コード例を含む教材やテンプレート販売ページです。ClaudeCodeLabでは記事から無料チートシート商品一覧へ誘導しますが、コードフェンスが壊れている記事は信頼されません。コンバージョンに近い記事ほど、本文より先に検証コマンドを決めます。

コピペで試せる最小セットアップ

以下はNode.js 18以降を前提にした検証用セットアップです。Claude Codeの公式セットアップもNode.js 18以上を前提にしているため、記事処理のローカルツールも同じ条件に寄せると運用が楽です。

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

実装例1: frontmatterとASTをまとめて監査する

次のスクリプトは、frontmatterをgray-matterで読み、本文をremark/MDX ASTとして解析します。descriptionの長さ、必須項目、コードフェンスの言語、長すぎるコード行、見出しslug、内部リンクと外部リンクを一度に確認できます。

// 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];
const checkExternal = process.argv.includes("--check-external");
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 codeBlocks = [];
const headings = [];

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");
    for (const line of String(node.value).split(/\r?\n/)) {
      if ([...line].length > 96) {
        errors.push(`long code line in ${node.lang || "unknown"} block`);
      }
    }
  }

  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 (checkExternal) {
  for (const url of links.external) {
    if (!(await reachable(url))) {
      errors.push(`external link failed: ${url}`);
    }
  }
}

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("");
}

async function reachable(url) {
  try {
    const response = await fetch(url, {
      method: "HEAD",
      redirect: "follow",
    });
    if (response.status === 405) {
      const fallback = await fetch(url, { redirect: "follow" });
      return fallback.status < 400;
    }
    return response.status < 400;
  } catch {
    return false;
  }
}

実行例です。まず外部リンク確認なしで高速に回し、公開前だけ--check-externalを付けるとCIが重くなりすぎません。

node tools/audit-mdx.mjs site/src/content/blog/example.mdx
node tools/audit-mdx.mjs site/src/content/blog/example.mdx --check-external

実装例2: raw HTMLを安全にHTMLへ変換する

MDXやMarkdownでHTMLを許可する場合は、XSSを先に考えます。OWASPは、文脈に合ったエンコードとHTML sanitizationを分けて考える必要があると説明しています。MarkdownからHTMLへ変換するだけならraw HTMLを無効にするのが一番簡単です。どうしても許可する場合は、rehype-rawでHTML ASTへ戻し、rehype-sanitizeで許可schemaに絞ります。

// 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];
if (!file) {
  throw new Error("Usage: node tools/markdown-to-safe-html.mjs input.md");
}

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));

落とし穴は、allowDangerousHtmlという名前の通り、それだけでは安全にならないことです。Claude Codeに「HTMLも変換して」と頼むときは、「raw HTMLを許すならsanitizeを必須にする。許さないならallowDangerousHtmlを使わない」と明記します。

実装例3: 10言語localeファイルを監査する

多言語サイトでは、1ファイルだけ良くても公開品質には届きません。以下のスクリプトは、同じslugが10 localeに存在すること、heroImageが揃っていること、updatedDateが入っていること、descriptionが120文字以内であることを見ます。

// 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);
  if (!fs.existsSync(file)) {
    errors.push(`${lang}: missing ${file}`);
    continue;
  }

  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 official 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 article set is consistent");

よくある失敗例と落とし穴

失敗何が起きるか防ぎ方
見出しを正規表現だけで拾うコードフェンス内の##まで目次に入るremarkheadingノードを見る
tags: Claude Code, Markdownと書く配列ではなく文字列として読まれるfrontmatter schemaで型を落とす
localeごとにslug生成が違う見出しリンクや目次が言語で崩れるgithub-sluggerなどで一貫生成する
raw HTMLをそのまま通すonclickや危険な属性が残るrehype-sanitizeを入れる
外部リンクを本文確認だけで済ませる公式URLの移動や404に気づかない公開前にHEAD/GETで確認する
Claude Codeに全体修正を許す競合作業者のslugや別記事まで触るwritable filesを依頼文に固定する

特にXSSは「Reactなら安全」という理解で止めると危険です。Reactは通常のテキスト埋め込みをエスケープしますが、dangerouslySetInnerHTMLやHTML変換後の文字列を入れる場合は別問題です。Markdown処理では「どの入力を信頼するか」を必ず書きます。

Claude Codeへの安全な依頼文

Claude Codeには、作業範囲、禁止事項、検証コマンド、失敗時の報告形式を先に渡します。以下のようなYAMLをissue本文やCLAUDE.mdに入れると、複数人が並行しているrepoでも事故が減ります。

task: "Refresh one published MDX article"
owned_files:
  - "site/src/content/blog/claude-code-markdown-processing.mdx"
rules:
  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"
    - "touching other slugs"
    - "publishing raw HTML without sanitization"
verification:
  - "node scripts/check-code-fences.mjs"
  - "node scripts/check-updated-article-quality.mjs"
report:
  - "changed files"
  - "checks run"
  - "residual risks"

この依頼文で重要なのは「良い記事にして」ではなく「何を壊してはいけないか」を書くことです。Claude Codeは強い実装者ですが、scopeが曖昧な作業では余計な親切をします。公開済み記事では、余計な親切は差分レビューを難しくします。

公開前チェックリスト

最後は手動レビューとローカルコマンドを分けます。AST監査、locale監査、コードフェンス検査、更新記事品質チェックを順番に通すと、本文の自然さと構造の壊れを両方見られます。

node tools/audit-mdx.mjs site/src/content/blog/claude-code-markdown-processing.mdx
node tools/check-locales.mjs
node scripts/check-code-fences.mjs
node scripts/check-updated-article-quality.mjs

手動では、スマホ幅でコードブロックが横スクロールできるか、表が崩れないか、CTAが本文と自然につながるかを見ます。比較記事なら比較表、コード例が3つ以上ある記事なら概念図、手順記事ならスクリーンショットや検証ログを入れるのが理想です。

収益化CTAと運用へのつなぎ方

Markdown/MDX処理を整える目的は、単にビルドを通すことではありません。記事、教材、商品ページ、研修ページを安定して更新できると、検索流入から学習、購入、相談までの導線を壊しにくくなります。

個人で始めるなら無料チートシートで日常コマンドを固定してください。記事品質レビューやプロンプトを繰り返し使うならClaude Codeプロンプトテンプレート集が近道です。チームで公開フロー、権限、CI、locale運用まで整えるならClaude Code研修・導入相談へ進むと、この記事のチェックを実プロジェクトに合わせて設計できます。

実際に試した結果

この記事の更新では、Masaの運用で起きがちな「日本語だけ厚く、他localeが薄い」「descriptionが長い」「コードフェンスの言語が抜ける」「公式リンクが古い」という4点を先に失敗条件として置きました。そのうえで、ASTで見出し・コード・リンクを拾う方針に固定すると、Claude Codeへの指示がかなり具体的になります。最終確認ではnode scripts/check-code-fences.mjsnode scripts/check-updated-article-quality.mjsを通し、本文の量、内部リンク、外部リンク、updatedDate、heroImageをまとめて確認する流れにしました。公開済み記事のリライトでは、本文を書く力よりも、壊してはいけない契約を先に機械で見える形にすることが効きます。

#Claude Code #Markdown #MDX #remark #content-ops
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。