Markdownの対話セクションをチャット風UIに自動変換する実装

2026.02.14
Share:

はじめに — 「AIとの対話」を記事でどう見せるか

ZERONOVA LAB は「AIネイティブ実験スタジオ」を掲げています。Journal の記事には、PMである Zeronova と AI である Claude Code の対話セクションを必ず含めるというルールがあります。

しかし、プレーンテキストの対話セクションは読みにくい。「Zeronova: ○○」「Claude Code: △△」という文字列が並んでいるだけでは、会話の臨場感が伝わりません。

Journal の PM-AI 対話セクションをチャット風UIで自動描画。react-markdown のカスタムコンポーネントで Zeronova: / Claude Code: パターンを検出し、Slack風のチャットメッセージに変換

2026年2月13日、react-markdown のカスタムコンポーネントを活用して、Markdown の対話セクションをSlack風のチャットUIに自動変換する仕組みをClaude Codeで実装しました。

設計の要件

まず、対話セクションがどのような Markdown で書かれるかを確認します。

**Zeronova**: RLS first の設計方針でいきたい。

**Claude Code**: 各テーブルに POLICY を定義する
アプローチが安全です。

**Zeronova**: それで進めよう。

要件は以下の通りです。

  1. **Zeronova**: で始まる段落を「人間側のメッセージ」として描画
  2. **Claude Code**: で始まる段落を「AI側のメッセージ」として描画
  3. 後方互換として **PM**: も Zeronova として扱う
  4. Markdown の記法変更は不要(既存の記事がそのまま動く)
  5. 対話以外の段落は通常のテキストとして描画

スピーカー検出の実装

react-markdown では、Markdown の各要素(段落、見出し、リスト等)をカスタムコンポーネントで差し替えることができます。<p> タグの描画をカスタマイズすることで、対話セクションを検出します。

ZeronovaZeronova
対話の検出はどのレベルでやるべきか。Markdown のプリプロセスで変換するか、react-markdown のレンダリング時に検出するか。
Claude Code
react-markdown のカスタムコンポーネントでレンダリング時に検出する方が保守性が高いです。Markdown のテキスト自体を変換するプリプロセス方式だと、正規表現が複雑になり、意図しないマッチのリスクがあります。カスタムコンポーネント方式なら、React の children から太字テキストを取得して、パターンマッチするだけで済みます。
ZeronovaZeronova
カスタムコンポーネント方式で行こう。

detectSpeaker() 関数が、段落の先頭にあるスピーカーラベルを検出します。

function detectSpeaker(children: React.ReactNode): {
  speaker: "zeronova" | "claude" | null;
  content: React.ReactNode;
} {
  // children の最初の要素が <strong> で
  // テキストが "Zeronova" / "Claude Code" / "PM" なら検出
  // ...
}

react-markdown は **Zeronova**<strong>Zeronova</strong> に変換します。カスタム <p> コンポーネントの children から最初の <strong> 要素のテキストを取得し、ZeronovaClaude CodePM のいずれかにマッチするかを判定します。

PM → Zeronova へのスピーカーラベル変更

チャット風UI の実装と同時に、スピーカーラベルの表記も見直しました。

それまでの Journal 記事では **PM**: というスピーカーラベルを使っていました。しかし、チャットUIで表示名として「PM」を見たときに、誰の発言なのかが分かりにくい。

ZeronovaZeronova
チャットUIを入れるなら、スピーカーラベルも見直したい。「PM」では一般的すぎて個人ブランドの認知につながらない。
Claude Code
スピーカーラベルを **Zeronova**: に変更することを提案します。AuthorCard・About ページ・JSON-LD の著者名と一致させることで、サイト全体で名前の一貫性が保てます。既存6記事の **PM**:**Zeronova**: に一括変更し、後方互換として **PM**: の検出も残します。
ZeronovaZeronova
それで行こう。表示名は「Zeronova」だけでいいか?サイト全体では「Zeronova(ゼロノバ)」で統一しているが。
Claude Code
チャットUIのスピーカーラベルは「表示名」です。Slack や Discord でフルネーム+読み仮名を表示しないのと同じです。SEO 効果は JSON-LD の author.namealternateName で担保されているので、UI は「Zeronova」のみが最適です。

この判断の結果、チャットUIのスピーカーラベル(Zeronova)、AuthorCard(Zeronova(ゼロノバ))、JSON-LD(name + alternateName)の3層で、UIの自然さ・ブランディング・SEO を同時に満たす設計になりました。

Claude Codeで既存6記事のスピーカーラベルを一括変更し、合計17箇所の **PM**:**Zeronova**: に置換しました。

チャット風UIの描画

スピーカーが検出された段落は、チャットメッセージとして描画されます。

Zeronova のメッセージ

  • アバター画像(/avatar.png、28x28px、next/image で最適化)
  • 白テキストの名前ラベル「Zeronova」
  • グレーの左ボーダー(border-gray-400
  • 薄い白背景(bg-white/5

Claude Code のメッセージ

  • TerminalIcon(neon green の丸いアイコン)
  • primary カラーの名前ラベル「Claude Code」
  • neon green の左ボーダー(border-primary
  • 薄い green 背景(bg-primary/5

この差異によって、人間と AI の発言が視覚的に即座に区別でき、対話の流れが直感的に把握できます。

実装上のポイント

next/image でのアバター最適化

Zeronova のアバター画像は next/imageImage コンポーネントで描画しています。Markdown レンダラーはクライアントコンポーネントのため、next/image をそのまま使用できます。

後方互換の維持

**PM**: という旧形式のスピーカーラベルも Zeronova として描画します。Journal の初期記事では **PM**: を使っていたため、後方互換を維持しないと既存記事の表示が崩れます。

新規記事では **Zeronova**: を使うことをガイドラインで規定しています。

順序付きリストのスタイル修正

対話UI の実装中に、別のレンダリング問題も発見しました。

順序付きリスト <ol><li> がカスタムの緑ドット(番号なし)で表示されていた。react-markdown の li コンポーネントに ordered: boolean が渡されるため、順序付き/なしでスタイルを出し分け

react-markdown の <li> カスタムコンポーネントに ordered プロパティが渡されることを利用して、順序付きリスト(<ol>)では数字、順序なしリスト(<ul>)ではカスタムのneon greenドットを表示するように修正しました。

内部リンクの target 属性

もう一つの修正として、内部リンクの target 属性の問題がありました。

内部リンクがすべて target="_blank" になっていた。href?.startsWith("/") でサイト内リンクを判定し、内部リンクは新しいタブで開かないように修正

全てのリンクに target="_blank" を設定していたため、/journal/another-article のような内部リンクも新しいタブで開かれていました。href/ で始まるかどうかで内部/外部を判定し、内部リンクは target なし(同一タブでのページ遷移)にしました。

著者情報の統一(E-E-A-T対応)

対話UIの実装と同じ日に、サイト全体の著者情報の統一もClaude Codeで行いました。

AuthorCard role を全バリアント統一: "Product Manager / AI-Native Builder"。PM → PdM 表記統一。全76ツールの JSON-LD に jobTitle: "Product Manager" を追加

AuthorCard コンポーネントの role 表記が Journal / Focus Blog / Tools でバラバラだったのを、すべて「Product Manager / AI-Native Builder」に統一。PM と PdM の表記揺れも PdM(Product Manager の日本語略称としてより正確)に統一しました。日本のIT業界では PM がプロジェクトマネージャーを指すことが多いため、PdM(プロダクトマネージャー)と明記することで職種の誤解を防ぐ効果もあります。

合わせて、ツールページの JSON-LD(getToolAuthorJsonLd())に jobTitle: "Product Manager" を追加し、全76ツールの構造化データに反映しました。Google は jobTitle を使って著者の専門性を判定するため、E-E-A-T の Expertise シグナル強化に寄与します。

著者ペルソナの定義を docs/author-persona.md に Single Source of Truth として集約し、各ガイドライン(journal-spec.md、x-posting-guidelines.md、note-checklist.md、kdp-publishing-guide.md)からの参照に切り替えた点も、コンテンツの一貫性を保つ上で重要な改善でした。

author-persona.md を Single Source of Truth として新規作成。6ファイルに分散していたペルソナ定義を統合し、約160行の重複を削除

まとめ — ブランドコンセプトをUIで体現する

PM-AI 対話のチャット風UIの実装を通じて得た学びをまとめます。

  1. react-markdown のカスタムコンポーネントは強力: Markdown の記法を変えずに、レンダリング時に特定パターンを検出して UI を差し替えられる。プリプロセス方式より保守性が高い
  2. 視覚的な差異が対話の読みやすさを左右する: アバター、ボーダーカラー、背景色の組み合わせで、人間とAIの発言を即座に区別できる
  3. 後方互換は初期から考慮する: **PM**:**Zeronova**: の移行で、後方互換を維持したことで既存記事への影響をゼロにできた
  4. ブランドコンセプトはUIで体現する: 「AIネイティブ実験スタジオ」を掲げる以上、AIとの対話が視覚的に「対等な議論」として表現される必要がある。プレーンテキストの **Claude Code**: ではその意図が伝わらない
  5. Single Source of Truth は運用コストを下げる: 著者ペルソナ情報が6ファイルに分散していた状態を、1ファイルに統合したことで更新漏れのリスクが減った

「AIとの対話をどう見せるか」は単なるUI実装の話ではなく、ブランドの信頼性に直結する問題です。チャット風UIによって「Zeronova と Claude Code が対等に議論して意思決定している」というリアリティが視覚的に伝わるようになりました。

関連記事:

Zeronova avatar

Zeronovaゼロノバ

Product Manager / AI-Native Builder

Web/IT業界19年以上・20以上のWebサービスを担当したPdM。東証プライム上場企業の子会社代表として事業経営を経験。現在はAIを駆使して企画から実装まで完結させる個人開発を実践中。