AstroブログにQA機能を追加!FAQプラグイン実装

ブログを書く際に、以下のような QA コンテンツを使いたくなる場面はないでしょうか?
自分自身 Astro でブログを書いている中で、QA コンテンツを使いたくなる場面がありました。しかし、デフォルトの Markdown の記法の中で上記のような QA コンテンツを表現する記法がなかったため独自の QA 記法を追加するために Remark プラグインを作成しました
プラグインではなくコンポーネントで QA コンテンツを表示する方法も別途解説しているので、興味があれば読んでみてください!

Remarkとは
Remark とは Markdown を unist を拡張した mdast と呼ばれる抽象構文木として扱うプラグインのエコシステムのことです
unist や mdast などについて詳しくは別記事で触れているのでそちらをご覧ください!

今回は Markdown で QA コンテンツを表現するために Remark プラグインを実装していきます
Markdown の記法を決める
まず、Markdown の記法を決めます。今回は Remark プラグインである remark-directive でディレクティブ記法を導入して使っていきたいと思います。ディレクティブ記法については、公式の README でも紹介されている CommonMark のドキュメントが参考になりますが、簡単に説明すると主に以下3つの記法が使えるようになります
:textDirective
::leafDirective
:::containerDirective
今回は、ディレクティブ記法の中の containerDirective を利用して、以下の記法で QA コンテンツの HTML を表現できるようにしたいと思います
:::FAQ
Q. Question1
A. Answer1
Q. Question2
A. Answer2
:::
↓
<dl>
<dt> Question1
<dd> Answer1
<dt> Question2
<dd> Answer2
</dl>
remark-directive を導入する
Markdown の記法が決まったので、プロジェクトにディレクティブ記法を導入するために remark-directive プラグインをインストールします
$ yarn add remark-directive
プラグインがインストールできたら、Astro プロジェクトの astro.config.mjs ファイルでプラグインを読み込みます
import remarkDirective from "remark-directive";
import { defineConfig } from "astro/config";
// https://astro.build/config
export default defineConfig({
markdown: {
remarkPlugins: [remarkDirective],
},
})
導入ができたら、README の myRemarkPlugin を参考に試しにプラグインを作成してディレクティブ記法が使えるようになったか確かめてみます。今回は以下のような remarkFaqContent というプラグインを作成しました
import { visit } from "unist-util-visit";
import { h } from 'hastscript'
export default function remarkFaqContent() {
return function (tree) {
visit(tree, function (node) {
if (
node.type === 'containerDirective' ||
node.type === 'leafDirective' ||
node.type === 'textDirective'
) {
console.log(node)
const data = node.data || (node.data = {})
const hast = h(node.name, node.attributes || {})
data.hName = hast.tagName
data.hProperties = hast.properties
}
})
}
}
このプラグインを astro.config.mjs ファイルで読み込んで、mdx ファイルで以下のようにディレクティブ記法を使ってみます
:text
::leaf
:::container
containerDirectiveのテスト
:::
コンソール or ターミナルに以下のような mdast が表示されていたら、remark-directive の導入は完了です
{
type: 'textDirective',
name: 'text',
attributes: {},
children: [],
position: {
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 6, offset: 5 }
}
}
{
type: 'leafDirective',
name: 'leaf',
attributes: {},
children: [],
position: {
start: { line: 3, column: 1, offset: 7 },
end: { line: 3, column: 7, offset: 13 }
}
}
{
type: 'containerDirective',
name: 'container',
attributes: {},
children: [ { type: 'paragraph', children: [Array], position: [Object] } ],
position: {
start: { line: 5, column: 1, offset: 15 },
end: { line: 7, column: 4, offset: 54 }
}
}
Remark プラグインを実装する
ではここから実際に Remark プラグインを実装していきます。最終目標は以下のような Markdown 記法で QA コンテンツの HTML を出力することです
:::FAQ
Q. Question1
A. Answer1
Q. Question2
A. Answer2
:::
↓
<dl>
<dt> Question1
<dd> Answer1
<dt> Question2
<dd> Answer2
</dl>
まず、先ほどテストのために実装した remarkFaqContent で上記の記法を試すと以下のような mdast に変換されます
{
type: 'containerDirective',
name: 'FAQ',
attributes: {},
children: [
{ type: 'paragraph', children: [Array], position: [Object] },
{ type: 'paragraph', children: [Array], position: [Object] },
{ type: 'paragraph', children: [Array], position: [Object] },
{ type: 'paragraph', children: [Array], position: [Object] }
],
position: {
start: { line: 1, column: 1, offset: 0 },
end: { line: 9, column: 4, offset: 61 }
}
}
この mdast が目標となる HTML に変換されるように AST を変換させていきます
最終的な remarkFaqContent プラグインは以下のようになりました
import { visit } from "unist-util-visit";
export default function remarkFaqContent() {
return function (tree) {
visit(tree, function (node) {
if (
node.type === 'containerDirective' ||
node.type === 'leafDirective' ||
node.type === 'textDirective'
) {
if (node.name !== 'FAQ' && node.name !== 'faq') return;
const children = [];
node.children.map((child) => {
const text = child.children[0].value
if (child.type === 'paragraph' && text.match(/^Q/)) {
const questionTextArray = text.split(' ')
questionTextArray.shift()
const question = questionTextArray.join('')
children.push({
type: "element",
tagName: "dt",
properties: {
class: 'question',
},
children: [{type: 'text', value: question}],
})
}
if (child.type === 'paragraph' && text.match(/^A/)) {
const answerTextArray = text.split(' ')
answerTextArray.shift()
const answer = answerTextArray.join('')
children.push({
type: "element",
tagName: "dd",
properties: {
class: 'answer',
},
children: [{type: 'text', value: answer}],
})
}
})
node.data = {
hName: "dl",
hProperties: {
class: ['faq'],
},
hChildren: children,
};
}
})
}
}
各タグのスタイルについては各自でお好きなように実装してください
Astro にプラグインを導入する
上記で実装した remarkFaqContent プラグインを astro.config.mjs で読み込みます
import remarkDirective from "remark-directive";
import remarkFaqContent from "./src/plugins/remarkFaqContent";
import { defineConfig } from "astro/config";
// https://astro.build/config
export default defineConfig({
markdown: {
remarkPlugins: [remarkDirective, remarkFaqContent],
},
})
読み込みが完了したら再度ページを読み込みます。すると以下のように QA コンテンツが表示できます
- Question1
- Answer1
- Question2
- Answer2