目次
AIチャットに「詳しくはサービス一覧をご覧ください」と返させると、リンクとして表示されず、Markdown文字列がそのまま残ることがあります。
Acecoreのサイトでも、問い合わせAIチャットの回答にURL前後の空白を含むMarkdownリンクが混ざったため、AIチャットのMarkdownリンク描画を修正したPR でレンダラーを調整しました。
この記事では、その小さな修正を入口に、AI回答を安全にDOMへ変換するための考え方をまとめます。
AI回答は信頼済みHTMLではない
まず前提として、AI回答はHTMLではなくテキストとして扱います。
チャットUIではリンク、太字、箇条書きくらいは使いたくなります。しかし、回答をそのまま innerHTML に入れると、モデルが出した文字列をブラウザに解釈させることになります。
必要なのは、Markdownを全部実装することではありません。チャットで必要な表現だけを小さく拾い、安全なDOMだけを作ることです。
問題は空白だけではない
今回の直接の不具合は、次のようなリンクでした。
[サービス一覧](/services/)
Markdownとしては人間に意味が通りますが、URL部分を「空白を含まない文字列」として正規表現で拾っているとマッチしません。
修正前はこういう考え方でした。
;/\[([^\]]+)\]\(([^)\s]+)\)/
[^)\s]+ は空白を許さないため、( /services/ ) をリンクとして拾えません。そこで、括弧の内側では前後空白を許容し、実際に使うURLは後でtrimします。
;/\[([^\]]+)\]\(\s*([^)]+?)\s*\)/
ここで大事なのは、正規表現を緩めて終わりにしないことです。緩めたあとは、必ず正規化と安全判定を入れます。
hrefはtrimしてから検証する
リンク生成の順番は固定します。
- Markdownからlabelとraw hrefを取り出す
- raw hrefを
trim()する - trim済みhrefを許可リストで検証する
- 許可できる場合だけ
<a>を作る
const href = String(rawHref || '').trim()
if (label && isSafeMarkdownHref(href)) {
const link = document.createElement('a')
link.href = href
link.rel = 'noopener noreferrer'
if (/^https?:\/\//i.test(href)) {
link.target = '_blank'
}
link.textContent = label
parent.appendChild(link)
}
trim() 前の値ではなく、実際にDOMへ入れる値で検証します。検証した値と描画する値がずれると、安全判定の意味が弱くなります。
許可リストはプロダクトごとに決める
AIチャットが案内してよいURLは、サイトごとに違います。
Acecoreの問い合わせAIでは、おおむね次のような範囲に絞っています。
| 種類 | 例 | 判断 |
|---|---|---|
| 内部パス | /services/ | 許可する |
| 同一origin | https://acecore.net/... | 許可する |
| 公式LINE | https://lin.ee/... | 目的が明確なので許可する |
| mailto | mailto:[email protected] | 必要な固定宛先だけ許可する |
| tel | tel:05088902788 | 必要な固定番号だけ許可する |
| その他外部 | 任意のURL | 原則リンク化しない |
実装例はこうです。
function isSafeMarkdownHref(href) {
if (href.startsWith('/')) return true
try {
const url = new URL(href, window.location.origin)
if (url.origin === window.location.origin) return true
if (url.hostname === 'acecore.net') return true
if (url.hostname === 'lin.ee') return true
} catch {
return false
}
return href === 'mailto:[email protected]' || href === 'tel:05088902788'
}
この関数はサイトごとに変えるべきです。採用サイトなら求人媒体、ECなら決済や配送追跡、SaaSならドキュメントやステータスページを許可するかもしれません。
許可できないリンクはテキストに戻す
安全判定に失敗したリンクをどう扱うかも設計です。
問い合わせAIの場合は、リンク化できないMarkdownを完全に消すより、元の文字列として残すほうが扱いやすいです。ユーザーには文脈が残り、開発者は「AIが何を出したか」を確認できます。
つまり、パーサーは「安全なリンクを作る」だけでなく、「安全に作れない場合は通常テキストとして出す」責務も持ちます。
テストケースを最初から用意する
この種のレンダラーは、正常系だけ見ると見落とします。
最低限、次のケースを確認します。
| 入力 | 期待結果 |
|---|---|
[サービス一覧](/services/) | 内部リンクになる |
[サービス一覧]( /services/ ) | trimされて内部リンクになる |
[LINE]( https://lin.ee/example ) | 外部リンクとして開く |
[危険](javascript:alert(1)) | リンク化しない |
[外部](https://example.com/) | 許可しないならリンク化しない |
[壊れたリンク](/services/ | テキストとして表示する |
`code` と [link]( /contact/ ) | codeとlinkがそれぞれ描画される |
PR #99 では、[サービス一覧]( /services/ )、[サービス一覧](/services/)、[LINE]( https://lin.ee/DjIrdqj ) が同じURLとして扱われることを確認しました。
Markdown全体を実装しない判断
AIチャットで使うMarkdownは限定できます。
- 段落
- 箇条書き
- 太字
- インラインコード
- リンク
テーブル、画像、HTML、脚注、見出しの深い階層まで対応すると、レンダラーの責務が急に大きくなります。チャットUIでは、読みやすく案内できれば十分です。
必要になったら実績のあるMarkdownライブラリを使う選択もあります。ただし、その場合でも「HTMLを許すか」「URLをどう絞るか」「外部リンクに何を付けるか」は別に決めます。
まとめ
AIチャットのMarkdownリンク描画は、見た目の小さな修正に見えます。しかし実際には、AI出力をどこまで信頼するかという境界設計です。
実装で重要なのは次の点です。
- AI回答はHTMLではなくテキストとして扱う
- 必要なMarkdownだけを小さくDOM化する
- MarkdownリンクのURL前後空白を許容する
- hrefはtrimしてから安全判定する
- 許可リストで内部URLと必要な外部URLだけを通す
- 許可できないリンクはテキストとして残す
- 壊れたMarkdownや危険なURLをテストする
AIをサイト導線に入れるほど、回答テキストをどう描画するかが重要になります。便利なMarkdown対応と安全なリンク制御は、同じ実装の中で一緒に扱うべきです。
AI回答のリンク描画フロー
Text
モデルからの回答はまずプレーンテキストとして扱う。
Parse
必要なMarkdown表現だけを小さなパーサーで拾う。
Validate
hrefをtrimし、内部URLや許可ドメインだけを通す。
Render
innerHTMLではなくDOM APIで安全な要素だけを作る。
雑に描画する場合
- AI回答をinnerHTMLへ直接入れてしまう
- Markdown仕様全体を一気に実装しようとする
- URL前後の空白でリンク化に失敗する
- 外部URLやjavascript:を同じ扱いにしてしまう
小さく安全に描画する場合
- 回答はテキストとして受け取り、必要な表現だけDOM化する
- チャットで使うMarkdownだけに対応する
- URLはtrim後に安全判定する
- 許可できないURLはリンク化せず文字列として表示する
- 完了: AI回答をHTMLとして信頼しない
- 完了: MarkdownリンクのURL前後空白を許容する
- 完了: hrefは必ずtrimしてから検証する
- 完了: 内部パス、同一origin、必要な外部ドメインだけを許可する
- 完了: 外部リンクにはtargetとrelを明示する
- 完了: 許可できないリンクはテキストとして残す
- 完了: 正常系だけでなく危険なURLと壊れたMarkdownもテストする
markdown-itやmarkedを使えば十分ですか?
URL前後の空白を許容すると危険になりませんか?
許可できないURLは削除すべきですか?
コメント
Gui
Acecore 代表。業務システム、Web、DB/インフラ、品質保証、AI活用を、事業課題の整理から設計・導入後の改善までつなげて推進している。 C#/.NET を軸にした実装力を土台に、PHP/JavaScript、SQL Server/PostgreSQL/MySQL、Linux/Windows Server なども踏まえ、要件整理、技術選定、品質基準、GitHubベースの開発運用まで一体で設計する。 生成AIは、開発・検証・情報整理などの業務プロセスに取り入れ、小規模チームでも速く確かな成果を出すための実務基盤として活用している。