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

AstroブログにQA機能を追加!FAQプラグイン実装
記事内に商品プロモーションを含む場合があります

ブログを書く際に、以下のような QA コンテンツを使いたくなる場面はないでしょうか?

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つの記法が使えるようになります

Markdown
:textDirective

::leafDirective

:::containerDirective

今回は、ディレクティブ記法の中の containerDirective を利用して、以下の記法で QA コンテンツの HTML を表現できるようにしたいと思います

Markdown
:::FAQ
Q. Question1

A. Answer1

Q. Question2

A. Answer2
:::

HTML
<dl>
  <dt> Question1
  <dd> Answer1
  <dt> Question2
  <dd> Answer2
</dl>

remark-directive を導入する

Markdown の記法が決まったので、プロジェクトにディレクティブ記法を導入するために remark-directive プラグインをインストールします

terminal
$ yarn add remark-directive

プラグインがインストールできたら、Astro プロジェクトの astro.config.mjs ファイルでプラグインを読み込みます

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 というプラグインを作成しました

src/plugins/remarkFaqContent.js
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 ファイルで以下のようにディレクティブ記法を使ってみます

Markdown
:text

::leaf

:::container
containerDirectiveのテスト
:::

コンソール or ターミナルに以下のような mdast が表示されていたら、remark-directive の導入は完了です

Markdown
{
  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 を出力することです

Markdown
:::FAQ
Q. Question1

A. Answer1

Q. Question2

A. Answer2
:::

HTML
<dl>
  <dt> Question1
  <dd> Answer1
  <dt> Question2
  <dd> Answer2
</dl>

まず、先ほどテストのために実装した remarkFaqContent で上記の記法を試すと以下のような mdast に変換されます

Markdown
{
  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 プラグインは以下のようになりました

src/plugins/remarkFaqContent.js
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 で読み込みます

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

参考資料

Recommend
こんな記事も読まれています!