1 月末にリニューアルした本サイトについて、今回は Markdown と、その拡張言語である MDX を取り上げます。
本サイトのフレームワークには Astro を使用しており、ブログコンテンツとプライバシーポリシーのコンテンツを MDX ファイルで管理しています。この記事では Markdown だけでは残る課題とその解決方法および、使用している Markdown のプラグインについて説明します。
Markdown
見出し「Markdown」Markdown は簡易的に文書構造を示すことができる軽量なマークアップ言語です。
Astro では標準で Markdown 形式のファイルに対応しており、/src/pages/
ディレクトリ以下に直接 .md
ファイルを置くか、Astro 2.0 以上で使用できる Content Collection の機能を使用することでページを構築することができます。
以下はブログ記事のコードの抜粋です。
先頭の ---
から ---
までは、Front Matter(フロントマター)と呼ばれるエリアで、こちらにページのメタデータを記述しています。それ以降が Markdown(本文)のエリアです。
Astro ではこのハイフン 3 つ(---
)の区切りをコードフェンスと呼びます。
見出しやリンク、段落テキスト、画像、リスト、コード、引用といった、ブログコンテンツに必要な基本的な要素は、標準の Markdown で表現できます。
また、Astro では Markdown のパーサ(構文解析)に remark を使用しており、あらかじめ GitHub Flavored Markdown(GFM) と Smartypants のプラグインが導入されています。
この GFM によって、以下のような Markdown の拡張機能が使用できます。
出力結果は以下のような見た目になります(一部スタイルを調整しています)。
このようにあらかじめ組み込まれている機能によって、ある程度自由にコンテンツを作成することができますが、独自の要素を使いたいケースまではカバーできません。
Markdown エリアには直接 HTML を記述することもできるので、その方法で独自要素を実装する方法も考えられますが、コードを一元管理できないのでメンテナンス性や拡張性の観点から推奨できません。
ただ、要素の折りたたみを表現する <details>
と <summary>
のように、最小限の HTML 要素のみで機能する場合には、直書きでも問題ないでしょう。
この課題を解決するために MDX を導入します。
MDX
見出し「MDX」Astro の @astrojs/mdx インテグレーションを導入し、Markdown の拡張言語である MDX を有効にすることで、コンポーネントを使用することができるようになり、独自に定義した要素が使えるようになります。
例えば、以下は「SVG の使い方」という記事で使用している SVG デモのコンポーネントです。クリックや Enter キーで三角形のオブジェクトがランダムでアニメーションします。
この 2 つのデモは同一のコンポーネントファイルを参照していますが、以下のように異なるプロパティ(Props)を指定することで見た目や内容を出し分けています。
また、コンポーネント内に Markdown 記法を含めることもできます。以下は補足情報のコンポーネント <Aside>
に、Markdown コンテンツを内包する例です。
コンポーネント内にコンポーネントを含めることもできます。以下は、キャプション付きでコードブロックを表示する <CodeBlock>
コンポーネントを追加した例です。
このように、1 つのコンポーネントで複数のバリエーションを表現できるようになるので、前述したメンテナンス性と拡張性を確保することができます。
そのほかにも、MDX によって以下の機能を使うことができます。
- エクスポートされた変数を使用する
- frontmatter 変数を使用する
- カスタムコンポーネントを HTML 要素に割り当てる
詳細は Astro のドキュメントを参照してください。
MDX を採用することにマイナス面がないわけではありません。例えば、RSS フィードで記事コンテンツを取得することができますが、MDX ではサポート外です。
Markdoc
見出し「Markdoc」Astro 2.1 より @astrojs/markdoc インテグレーションが追加されました。
これにより Markdoc の機能を使って、独自要素を定義することも可能です。
ただ、現時点では v0.0.1
で、試験的な機能(Experimental)として提供されているので、今後大幅な仕様変更が発生する可能性がある前提で使う必要があります。
Markdown プラグイン
見出し「Markdown プラグイン」シンタックスハイライト
見出し「シンタックスハイライト」Astro ではコードのシンタックスハイライトの機能として、標準で Shiki が有効になっており、設定により Prism も選択できます。
本サイトでのシンタックスハイライトの設定を以下に抜粋します。
見出しにアンカーリンクを付与
見出し「見出しにアンカーリンクを付与」2023/04/06 追記 VoiceOver での検証を踏まえて、アンカーリンクのマークアップを変更しました。
Astro で使用する Markdown や MDX には、見出し要素に自動的に固有の ID が振られます。
この見出しにホバーしたときに、アンカーリンクを表示したかったので rehype-autolink-headings をインストールし、カスタマイズすることで実現しています。
本サイトでの rehype プラグインの設定を以下に抜粋します。
若干込み入ったコードになっていますが、behavior: 'after'
で見出し要素の直後にアンカーリンクを追加し、class="anchor"
を付与しています。
content
でアンカーリンク内のコンテンツを指定しており、hastscript の h()
を使用して、ハッシュ(#)の形をしたアイコンの SVG を挿入しています。
VoiceOver での検証を踏まえて、この SVG は aria-hidden="true"
でアクセシビリティツリーから除外し、読み上げの対象外とし、スクリーンリーダー用のテキストを別途追加しました。
このテキストを、変数 headingText
に格納していますが、念のため、フォールバック用に「この見出しのリンク」というテキストも残しています。
content: (node) => {
const heading = node?.children[0]?.value;
const headingText = (typeof heading === 'string') ? `見出し「${heading}」` : 'この見出しのリンク';
出力される HTML は、以下のようなコードになります。
ちなみに、GitHub の README.md
で出力される見出しのアンカーリンクを見ると、<a>
要素に aria-hidden="true"
が指定されています。アクセシビリティツリーの構造を単純化するという考えであれば、これでもよいのかもしれません。
ただ、この場合、アクセシビリティツリーからアンカーリンクが完全に無視され、スクリーンリーダーを使用するユーザにはアンカーリンクが存在しないものとされてしまいます。その点を考慮して、このサイトではアンカーリンクに aria-hidden="true"
は指定しませんでした。
元の astro.config.mjs
のコードに戻ると、group
は見出し要素を囲う要素で、アンカーリンクを絶対配置(position: absolute
)にしたときの起点の役割をしています。
アンカーリンクを見出し要素の直後に配置(behavior: after
)としたのは、Astro のドキュメントサイトのコードで引用されている以下の記事を参考にしました。
これにより、スクリーンリーダーの見出し検索のようなナビゲーション機能の妨げになることを回避できると考えています。