Gatsby のサイトに API で取得した記事を埋め込みたい

キュアップ・ラパパ！

Gatsby の記事に外部から取得してきたリソースを API経由で埋め込んでみたいと思うようになりました。

### 仕様を決めよう

とりあえず外部リソースとして Qiita を利用しましょう。最初にかんたんな仕様を考えておきます。

- Qiita の任意の 1 記事が画面に埋め込める
- Qiita の記事詳細ページの URL を使用して、記事を Gatsby の記事に埋め込める
- [gatsby-transformer-remark](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-transformer-remark) の plugin として markdown に inlineCode として埋め込んで利用できる
- スタイルシートはユーザーが自由に指定できる。（よって、プラグイン側はとりあえず何もしない）

### 下準備

まずはローカルで開発していきましょう。Gatsby はプロジェクトルートにある `plugins` ディレクトリ配下へプラグインファイルを格納しておきます。

`plugins` 配下に定義したスクリプトは、わざわざ npm library でインストールしなくとも plugins として認識されます。

プラグイン名は仮に `gatsby-remark-cure-miracle` にしておきます。

※ ここでプラグインディレクトリ `plugins/gatsby-remark-cure-miracle/` に package.json がないと動かないのにさんざんハマりました。エラーメッセージにも出てるのになぜかプラグインディレクトリのことだと気が付かつきませんでした。バカかな 🥺

```txt
/
└── gatsby-config.js
└── /src
└── /plugins
    └── /gatsby-remark-cure-miracle
        ├── index.js
        └── package.json
```

これで、 `gatsby-config.js` でプラグイン名を定義してあげるとプラグインとして認識してもらえるようになります。
今回は `gatsby-transformer-remark` のプラグインとして作成するので、以下のように定義しておきます。

また、プラグインのオプションとして、Qiita の個人アクセストークンを設定しておきます。アクセストークンを設定せず使うと、対象記事の数によってはすぐにリクエスト制現地を超えてしまいます。

素直にアクセストークンを取得して渡しましょう 😘

```js:title=gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `gatsby-transformer-remark`,
      options: {
        plugins: [
          {
            resolve: `gatsby-remark-cure-miracle`,
            options: {
              accessToken: 'YOUR_PERSONAL_ACCESS_TOKEN',
            },
          },
        ],
      },
    },
  ],
};
```

### 実装

Qiita の API を叩いて、json データから記事情報本文の `rendered_body` と記事タイトルを表す `title` を html に流します。

#### 記事情報の取得

今回は axios を使って http リクエストを行います。オプションでアクセストークンを定義している場合、アクセストークン情報が含まれる axios のインスタンスを生成しています。

```js
const axios = require(`axios`);
const API_BASE = `https://qiita.com/api/v2/`;

let headers = {
  'Content-Type': `application/json`,
};
if (accessToken) {
  headers = {
    ...headers,
    Authorization: `Bearer ${accessToken}`,
  };
}

const http = axios.create({
  baseURL: API_BASE,
  headers,
});
```

次に `unist-util-visit` を使用して `markdownAST` を調べて、 `inlineCode` type のノードの値を変換していきます。

また、Qiita の URL が valid なものかをチェックして、処理するべき `inlineCode` かどうかを判定しています。
URL の仕様がわからなかったので、存在している Qiita の URL からざっくりと推測しました。間違ってたらごめんなさい。

```js
const visit = require(`unist-util-visit`);

const isUrlValid = url => {
  const mached = url.match(
    /https:\/\/qiita\.com\/(?!-)[0-9a-zA-Z_-]+\/items\/[0-9a-z]+/
  );
  return mached === null ? false : true;
};

const getItemId = url => {
  return url.match(
    /https:\/\/qiita\.com\/(?!-)[0-9a-zA-Z_-]+\/items\/([0-9a-z]+)/
  )[1];
};

module.exports = async ({ markdownAST }) => {
  visit(markdownAST, `inlineCode`, async node => {
    // qiita:で始まる inlineCode かどうかを調べます。
    if (node.value.startsWith(`qiita:`)) {
      // inlineCode から記事URLを取得します。
      // `qiita:https://qiita.com/Qiita/items/c686397e4a0f4f11683d` であれば
      // `https://qiita.com/Qiita/items/c686397e4a0f4f11683d` の部分のみを取り出します。
      const postUrl = node.value.substr(6);

      // 取り出した postUrl が vali なものかどうかをチェックします。
      if (isUrlValid(postUrl)) {
        // valid な URL の場合の処理
      }
    }
  });

  return markdownAST;
};
```

ただし、visit method の中で非同期作業を行うと、そこで処理が skip されてしまいます 🥺

一旦 `nodes` という変数を用意して、処理対象の node を格納しておいて、別途、非同期処理を実行します。

```js
module.exports = async ({ markdownAST }) => {
  const nodes = [];

  visit(markdownAST, `inlineCode`, async node => {
    if (node.value.startsWith(`qiita:`)) {
      const postUrl = node.value.substr(6);

      if (isUrlValid(postUrl)) {
        nodes.push(node);
      }
    }
  });

  await Promise.all(
    nodes.map(async node => {
      const postUrl = node.value.substr(6);
      const itemId = getItemId(postUrl);
      const response = await http.get(`/items/${itemId}/`);


      node.type = `html`;
      node.value = `<div>${response.data.rendered_body}</div>`;
    })
  );

  return markdownAST;
};
```

これでだいたい準備が整いました。ただ、これだとサイトになにも装飾されない Qiita の記事が展開されて都合が悪そうですね 🥺

ユーザーが任意のスタイルシートを定義可能なように、`node.value` に値をわたす時に div にクラスを付与しておきましょう。ついでに記事のタイトルも表示しておくと、どんな記事が埋め込まれているのかわかって良さそうですね。

```js
node.type = `html`;
node.value = `
  <div class="gatsby-qiita-cure-miracle-wrapper">
    <div class="gatsby-qiita-cure-miracle-body">
      ${response.data.rendered_body}
    </div>

    <div class="gatsby-qiita-cure-miracle-foot">
      Qiita: <a href="${response.data.url}" target="_blank" rel="noopener noreferrer">${response.data.title}</a>
    </div>
  </div>
`;
```

定義した class 名に対応したスタイルシートを書いていきましょう。プラグインの機能としてデフォルトのスタイルを定義しても良さそうです。

```css:title=src/components/all.css
.gatsby-qiita-cure-miracle-body {
  border: 1px solid #1b262c;
  box-shadow: 4px 4px #55c500;
  height: 420px;
  margin: 1.6rem 0;
  overflow: auto;
  padding: 1.6rem 2.4rem;
  width: 100%;
}

.gatsby-qiita-cure-miracle-foot {
  margin-bottom: 2.4rem;
  text-align: right;
}
```

作成したスタイルシートは、任意の component に読み込んでおきます。

```tsx:title=src/components/Layout.tsx
import './all.css';
```

さて、ここまでできたらコードの全体像を見てみましょう。

```tsx
"use strict";
const visit = require(`unist-util-visit`);
const axios = require(`axios`);

const API_BASE = `https://qiita.com/api/v2/`;

module.exports = async ({ markdownAST }, { accessToken }) => {
  let headers = {
    "Content-Type": `application/json`
  };
  if (accessToken) {
    headers = {
      ...headers,
      Authorization: `Bearer ${accessToken}`
    };
  }

  const http = axios.create({
    baseURL: API_BASE,
    headers
  });

  const isUrlValid = url => {
    const mached = url.match(
      /https:\/\/qiita\.com\/(?!-)[0-9a-zA-Z_-]+\/items\/[0-9a-z]+/
    );
    return mached === null ? false : true;
  };

  const getItemId = url => {
    return url.match(
      /https:\/\/qiita\.com\/(?!-)[0-9a-zA-Z_-]+\/items\/([0-9a-z]+)/
    )[1];
  };

  const nodes = [];

  visit(markdownAST, `inlineCode`, async node => {
    if (node.value.startsWith(`qiita:`)) {
      const postUrl = node.value.substr(6);

      if (isUrlValid(postUrl)) {
        nodes.push(node);
      }
    }
  });

  await Promise.all(
    nodes.map(async node => {
      const postUrl = node.value.substr(6);
      const itemId = getItemId(postUrl);
      const response = await http.get(`/items/${itemId}/`);

      node.type = `html`;
      node.value = `
        <div class="gatsby-qiita-inside-wrapper">
          <div class="gatsby-qiita-inside-body">
            ${response.data.rendered_body}
          </div>

          <div class="gatsby-qiita-inside-foot">
            Qiita: <a href="${response.data.url}" target="_blank" rel="noopener noreferrer">${response.data.title}</a>
          </div>
        </div>
      `;
    })
  );

  return markdownAST;
};
```

[Github にあげたコードはこちら](https://github.com/octonyat/gatsby-remark-qiita-inside)。

### 使い方

こんな感じで任意の記事の中でインラインコードを使って `qiita:記事詳細URL` を貼り付けると、 Qiita の記事がページへ埋め込まれます。

```markdown
# タイトル

`qiita:https://qiita.com/Qiita/items/c686397e4a0f4f11683d`
```

するとこんな感じになるはず。これから記事のスタイルは自分で書かないといけないので大変ですね。

<img width="839" alt="スクリーンショット 2020-03-08 12.18.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102819/544e3aa6-12ef-a6ad-13d1-0977a211b9ca.png">

実際に埋め込んでみたページは[こちら](https://dotgirl.net/2020/03/08/gatsby-remark-qiita-inside/)

今回は実際に公式ドキュメントを読みながら実験的なプラグインを作ってみました。参考になれば幸いです。

おわり／(^o^)＼