Libraryから取得した本文はMarkdownです。表示するだけならライブラリでHTMLに変換してdangerouslySetInnerHTMLに渡せば済みますが、このサイトでは対応する記法を限定した変換器を自前で書き、React要素として描画しています。
生HTMLを通さない
一番の理由は、本文に含まれるHTMLをそのまま出さないことです。Reactは文字列をテキストとして描画するので、<script>や<img onerror>が原稿に紛れ込んでもそのまま文字として表示されます。変換器はブロックごとに要素を組み立てるだけで、HTML文字列を生成しません。
リンク先を制限する
リンクはhttps:、http:、mailto:、tel:、ページ内の#、サイト内の/だけを許しています。javascript:のようなスキームは、リンクにせず書かれたままの文字列として出します。
const safeHrefPattern = /^(https?:\/\/|mailto:|tel:|#|\/)/i;画像も同じ考え方で、https:か同一サイトのパスだけを<img>にします。
対応している記法
必要になったものだけを足しています。
| 記法 | 対応 |
|---|---|
| 見出し、段落、箇条書き、番号付き | あり |
| 引用、区切り線、表 | あり |
| コードブロック(言語指定つき) | あり |
| 強調、インラインコード、リンク、画像 | あり |
| 生HTML、脚注、数式 | なし |
見出しは1段下げる
ページのタイトルがh1を持つので、原稿の#はh2、##はh3として描画します。Libraryのエディタで「見出し1」を付けた行が、記事の中では節の見出しになります。技術記事ではこの見出しにidを付け、目次からリンクしています。
コードブロックの言語
言語指定は<pre data-lang="ts">として持ち、CSSのattr()で右上に表示しています。シンタックスハイライトのライブラリは読み込んでいません。読みやすさより、ページの軽さと依存の少なさを優先しています。
足りなくなったら
対応する記法を増やすときは、変換器のブロック型を1つ足し、描画側にcaseを1つ足します。ここまでで200行ほどなので、必要になった時点で読み直せる大きさに収まっています。