キュアップ・ラパパ!
Gatsby の記事に外部から取得してきたリソースを API経由で埋め込んでみたいと思うようになりました。
仕様を決めよう
とりあえず外部リソースとして Qiita を利用しましょう。最初にかんたんな仕様を考えておきます。
- Qiita の任意の 1 記事が画面に埋め込める
- Qiita の記事詳細ページの URL を使用して、記事を Gatsby の記事に埋め込める
- gatsby-transformer-remark の plugin として markdown に inlineCode として埋め込んで利用できる
- スタイルシートはユーザーが自由に指定できる。(よって、プラグイン側はとりあえず何もしない)
下準備
まずはローカルで開発していきましょう。Gatsby はプロジェクトルートにある plugins
ディレクトリ配下へプラグインファイルを格納しておきます。
plugins
配下に定義したスクリプトは、わざわざ npm library でインストールしなくとも plugins として認識されます。
プラグイン名は仮に gatsby-remark-cure-miracle
にしておきます。
※ ここでプラグインディレクトリ plugins/gatsby-remark-cure-miracle/
に package.json がないと動かないのにさんざんハマりました。エラーメッセージにも出てるのになぜかプラグインディレクトリのことだと気が付かつきませんでした。バカかな 🥺
/
└── gatsby-config.js
└── /src
└── /plugins
└── /gatsby-remark-cure-miracle
├── index.js
└── package.json
これで、 gatsby-config.js
でプラグイン名を定義してあげるとプラグインとして認識してもらえるようになります。
今回は gatsby-transformer-remark
のプラグインとして作成するので、以下のように定義しておきます。
また、プラグインのオプションとして、Qiita の個人アクセストークンを設定しておきます。アクセストークンを設定せず使うと、対象記事の数によってはすぐにリクエスト制現地を超えてしまいます。
素直にアクセストークンを取得して渡しましょう 😘
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 のインスタンスを生成しています。
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 からざっくりと推測しました。間違ってたらごめんなさい。
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 を格納しておいて、別途、非同期処理を実行します。
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 にクラスを付与しておきましょう。ついでに記事のタイトルも表示しておくと、どんな記事が埋め込まれているのかわかって良さそうですね。
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 名に対応したスタイルシートを書いていきましょう。プラグインの機能としてデフォルトのスタイルを定義しても良さそうです。
.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 に読み込んでおきます。
import './all.css';
さて、ここまでできたらコードの全体像を見てみましょう。
"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;
};
使い方
こんな感じで任意の記事の中でインラインコードを使って qiita:記事詳細URL
を貼り付けると、 Qiita の記事がページへ埋め込まれます。
# タイトル
`qiita:https://qiita.com/Qiita/items/c686397e4a0f4f11683d`
するとこんな感じになるはず。これから記事のスタイルは自分で書かないといけないので大変ですね。
実際に埋め込んでみたページはこちら
今回は実際に公式ドキュメントを読みながら実験的なプラグインを作ってみました。参考になれば幸いです。
おわり/(^o^)\