なにこれ

GatsbyにQiitaの記事を取り込めるプラグイン(gatsby-source-qiita)を作りました QiitaからGatsbyに乗り換えようと考えている方で、Qiitaの記事を引き継ぎたい場合に便利なプラグインです。サンプル(gatsby-starter-qiita)も用意してます。 今回はこのプラグインとサンプルについて紹介します。

デモイメージ(コメント付き)

概要

  • プラグイン作成の経緯と最近のQiita
  • gatsby-source-qiita

    • 使い方
    • 実装ポイント

      • プラグイン公開手順
      • gatsby/graphql not found
      • 目次情報作成
  • gatsby-starter-qiita

    • 概要
    • 実装ポイント

      • シンタックスハイライトのスタイル
      • オリジナル記事とQiita記事のスキーマ統一
      • Github-rebbons便利
  • まとめ

プラグイン作成の経緯と最近のQiita

最近Qiitaが微妙なので、Qiitaとブログを別々に管理するより、ブログにまとめたほうが良さそうと思ったのと、 1つぐらいGatsby公式サイトで公開してみたいという思いから作りました。 大枠はmottox2さんのgatsby-source-esaを参考にして、 gatsby-transform-remarkQiita APIドキュメントを見ながら実装してます。

余談ですが、最近のQiitaはポエムが乱立していて、変な方向に向かっていますね。
かつては有用な技術記事がたくさん投稿されているイメージでしたが、トレンド機能を導入してからでしょうか?
昔に戻ってもらえるとうれしいです。来月のQiita Advent Calendarに期待です!!
とはいえ、今でも良記事もたくさんあって、Angualrとかで絞ると結構見つかります。 最近、これは!と思った記事は、「Angularで、Angular Materialのテーマに対応するライブラリを作る」などです。このような記事こそQiitaにあるべきだと思います。

gatsby-source-qiita

使い方

インストールします。

npm install --save gatsby-source-qiita

アクセストークやユーザ名を設定します。

gatsby-config.jsの一部
module.exports = {
  plugins: [
    {
      resolve: `gatsby-source-qiita`,
      options: {
        accessToken: `YOUR_PERSONAL_ACCESS_TOKEN`,
        userName: `YOUR_UAWE_NAME`,
        fetchPrivate: false,
        excludedPostIds: ['da8347f81a9f021b637f']
      }
    }
  ]
}
  • accessToken

    • 【必須】API認証用のQiitaアクセストークンです。Qiitaの設定ページ > アプリケーション > 新しくトークンを発行するで発行してください。
  • userName

    • 【必須】Qiitaアカウント名です。ここで指定したアカウントに紐付く記事を引っ張ってきます。
  • fetchPrivate

    • 【任意】限定公開記事も取得するかを設定します。デフォルトはfalseです。
  • excludedPostIds

    • 【任意】除外したい記事をid配列で指定します。デフォルトは何も除外しません。

これでQiitaの記事をGraphQLで取得できるようになります。 取得情報はほぼQiita APIのまんまですが、目次情報(headings)を追加しています。

Qiita記事取得用のクエリ
{
  allQiitaPost {
    edges {
      node {
        id
        title
        headings {
          value
          id
          depth
          parents {
            value
            id
            depth
          }
        }
        rendered_body
        body
        comments_count
        created_at
        likes_count
        reactions_count
        tags {
          name
        }
        updated_at
        url
        user {
          id
        }
      }
    }
  }
}

実装のポイント

プラグイン公開手順

いきなしnpmパッケージで開発を始めるより、下記のように段階を経て動作確認していく方が楽でした。

  1. (Gatsby APIを使わない処理について)コンポーネントで実装
  2. アプリケーション自体のgatsby-node.jsgatsby-browser.jsで実装
  3. ローカルプラグインに切り出してgatsby-node.jsgatsby-browser.jsで実装
  4. 別プロジェクトに切り出してnpmパッケージとして実装、 アプリケーション側からはnpm install ../gatsby-source-qiitaのようにローカルnpmインストール
  5. npm公開
  6. アプリケーション側でnpm install gatsby-source-qiitaのようにnpmインストール

プラグインをローカルインストールするとgatsby/graphql not found

Gatsbyではプラグインでgatsby/graphqlを使う場合でも依存ライブラリには追加しません。 ビルド時にアプリ側のgatsby/graphqlを使ってプラグインも含め全体をコントロールするからです。

のはずなんですが、プラグインをnpmでローカルインストールする(npm install ../gatsby-source-qiitaする)と ビルド時に「gatsby/graphql not found」と怒られました。 しょうがないので動作確認時だけローカルプラグインとして扱う(アプリのplugins配下に置く)ことで回避しました。 npm公開版だと問題なく動いたのですが... 最後まで謎でした。

目次情報作成

QiitaAPIで取得するHTMLは既にリンクが埋め込まれていたので、HTMLから目次を抽出しています。 HTML構文解析にはunifiedrehypeを使いました。

HTMLから目次情報抽出する処理
import rehype from 'rehype'
import visit from 'unist-util-visit'
import hastToString from 'hast-util-to-string'

const MIN_HEADER_DEPTH = 1
const HEADER_TYPE_IN_HAST = 'element'
const HEADER_TAG_NAMES_IN_HAST = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']

function _extractHeadingDetails(htmlStr) {
  // HAST形式の抽象構文木に変換
  const htmlAst = rehype.parse(htmlStr)

  // ヘッダータグ(h1〜h6タグ)を探す
  const result = []
  visit(htmlAst, HEADER_TYPE_IN_HAST, node => {
    if (!HEADER_TAG_NAMES_IN_HAST.includes(node.tagName)) {
      return
    }

    // ヘッダータグから深さとタイトルを抽出
    const heading = {
      depth: Number(node.tagName[1]),
      value: hastToString(node).replace('\n', '')
    }
    // aタグのうちhref属性が#始まりのものを探しidを抽出
    node.children.filter(c => _isHeaderIdLink(c)).forEach(c => {
      heading.id = decodeURI(c.properties.href.split('#')[1])
    });

    result.push(heading)
  })

  return result
}

function _isHeaderIdLink(node) {
  return node.tagName === 'a'
          && node.properties.href
          && node.properties.href.startsWith('#')
}

gatsby-starter-qiita

概要

Gatsbyの一番ベーシックなサンプル(gatsby-starter-blog)をベースに、gatsby-source-qiitaを導入し、オリジナルの記事とQiitaの記事を一緒くたに扱えるサンプルです。 デモもあります。

デモイメージ(コメント付き)

実装ポイント

シンタックスハイライトのスタイル

Qiita APIで取得するHMTLのソースコードブロックは、Qiitaで想定してるシンタックスハイライト用クラスが付与されています。 そのためスタイルはQiitaのサイトで開発者ツールで該当のCSS開いてほぼ流用しました。

オリジナル記事とQiita記事のスキーマを統一したい

オリジナル記事とQiita記事でスキーマが違うため、ソースコードで一緒くたに扱おうとするとif文だらけになって扱いづらい状態でした。 対策としてスキーマ統一用ローカルプラグインを作りました。ローカルプラグインなので名前はテキトーですが...
Gatsbyプラグインでは、createNodeFieldというAPIでノードのfieldsに自由にプロパティが追加できます。 今回はonCreateNode(ノード生成時に呼ばれるAPI)時にcreateNodeFieldを使ってfieldsにタイトルや作成日など共通プロパティを追加することでスキーマの差異を吸収しています。

exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions

  if (node.internal.type !== `MarkdownRemark` && node.internal.type !== `QiitaPost`) {
    return
  }

  // ここで差異を吸収
  const [
    slug,
    title,
    date,
    excerpt,
    tags,
  ] =
    node.internal.type === `MarkdownRemark`
      ? [
        createFilePath({ node, getNode }),
        node.frontmatter.title,
        node.frontmatter.date,
        _excerptMarkdown(node.rawMarkdownBody, 120),
        node.frontmatter.tags
      ]
      :[
        `/${node.id}/`,
        node.title,
        node.created_at,
        _excerptHtml(node.rendered_body, 120),
        [...(node.tags.map(tag => tag.name) || []), 'Qiita'] // Qiitaタグを追加
      ]

  // ノードのfieldsにプロパティを追加
  createNodeField({ name: `slug`,     node,   value: slug     })
  createNodeField({ name: `title`,    node,   value: title    })
  createNodeField({ name: `date`,     node,   value: date     })
  createNodeField({ name: `excerpt`,  node,   value: excerpt  })
  createNodeField({ name: `tags`,     node,   value: tags     })
}

Github-ribbons便利

GitHub Pagesなどでサンプル公開したときに、ソースコードのリンクを貼り付けたくなりますが、 その時に便利なのがGithub-ribbonsです。 いろんなところで見かけるこのリボン、実はコピペで簡単に実装できたんですね。知らなかった。

GitHub Ribbons

Github-rebbonsで紹介されているコードを少しReactベースに修正して貼り付けました。

<a href="https://github.com/Takumon/gatsby-starter-qiita">
  <img
    style={{
      position: 'absolute',
      top: 0,
      right: 0,
      border: 0
    }}
    src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_png"
    alt="Fork me on GitHub" />
</a>

まとめ

今回はQiitaから記事を取得するGatsbyプラグインを作りました。 自分のプラグインがGatsbyのサイトで検索できるようになったときは少し感動しました。 また自分のブログにgatsby-source-qiitaを適用したことにより記事数とバリエーションが増えてので、なんとなく満足してます。