Github issueをCMSとして使う

2021/10/28

JAMJAMJAMStack/Online #2

jiyuujin

  • 複雑なUIを作るのが好きです
    • React/Next/Vue/Nuxt/Scala/Swift
  • スタッフとして色々と参画中
    • FlutterKaigi 2021
    • JAWS DAYS 2021
    • Flutter Japan User Group
    • PWA night
    • v-kansai / kansai.ts
  • プロフィールサイト
    • https://yuma-kitamura.nekohack.me
  • 技術ブログ
    • https://webneko.dev

URL:

   https://ohayo.nekohack.me

Deploy to Vercel

Fetch issues

Vite

Vue

Twitter Spaces で

雑談する

こんな

イメージ

そもそも Spaces とは

  • 今年春、部分的に開始された音声のみでやりとりを行うコミュニケーションツール
    • 共同ホスト最大 2 名、スピーカー最大 10 名
    • スケジューリングも可能
  • 最近ホスト権が全開放された
  • なお、引き続き PC (PWA) 版の Web ブラウザにおいてはスピーカーとして参加できない
    • リスナーとしてのみ参加が許されている

いつやっているの?

  • 平日の毎朝、 09:00 から 45 分間程度
  • 扱うテーマは基本的にフリー
    • 前日に勉強会が開催されたらそれを振り返ったり、誰かがブログを書いたらその知見共有と宣伝を兼ね雑談したり、と様々
    • 私自身の職歴が iOS 開発より始まって Android 開発も経ながら、現在は React (Vue) を中心とする Web フロントエンド開発に携わっている
      • 自然とその辺りが中心になっている傾向
    • 最近は Flutter 開発にも取り組む

これまでは Notion を

  • 今年の GW 明けよりこの活動をスタートした
    • かれこれ半年が経過している
  • 参加できない方、これから回数を重ねて振り返ることを想定することを考慮した
    • 当時 API が Public Beta になったと話題の Notion に雑談内容を記録した
      • https://www.notion.so/nekohack/Quick-Note-c0a6b685fb524ca4823cc1dccbf2f9b8

なぜ Github issue に

  • 課金しなくても良い
    • Github issue を使う前は Notion を使っていた
      • チームプランで始めてしまったがゆえ、日に日に容量と相談
      • 個人プランに鞍替えすれば無料で行けそうだった
  • 誰でも issue を書ける
  • API としての完成度が高い
    • 良くも悪くもまだ Notion API はベータ版
      • タイトルやラベル付けの取得は問題無かった
      • 肝心となるコンテンツの部分 (ブロックの中身) を完全な形式で取得できなかった

具体的にやったことは

ざっくりこの 3 点

  • 平日は毎朝、自動で Github issue を作成する
    • Google Apps Script
      • AM8:00 (JST+0900) 台にトリガーを設定する
  • GraphQL を使えるよう設定する
    • @vue/apollo-composable
  • Vite (Vue 3) で SG 化
    • きっかけは仕事の場面でバンドラとして CRA 環境のビルド高速化に Vite を利用した経験から
      • antfu/vite-sse を参考にさせていただいた
      • 結果としては中々に面倒な印象でした
        • 後日ブログで詳しく

Vue 3 で GraphQL を使う

  • Github API に v3 (REST) と v4 (GraphQL) が存在する
    • 違いは REST と GraphQL の書き味
    • 気になるリクエスト制限
      • v3 は 5000 リクエスト数 / 1h まで、一方 v4 は 5000 ノード数 / 1h まで
  • @vue/apollo-composable を使えば Hooks ベースで issue データを fetch できる
export const searchQuery = gql`
  query {
    viewer {
      login
      repository(name: "ohayo-developers") {
        createdAt
        name
        issues(last: 100 orderBy: { field: CREATED_AT, direction: DESC }) {
          nodes {
            id
            body
            createdAt
            title
            url
            number
            labels(last: 10) {
              nodes {
                id
                name
              }
            }
            timelineItems(first: 10) {
              nodes {
                ... on IssueComment {
                  id
                  body
                }
              }
            }
            participants(last: 10) {
              nodes {
                id
                login
                name
                avatarUrl(size: 40)
              }
            }
          }
        }
      }
    }
  }
`

必要なものだけ取得する

  • タイトル
  • 作成日時
  • Label 一覧
  • コメント内容
  • 参加者 (コントリビュータ) 一覧
export const searchQuery = gql`
  query {
    viewer {
      login
      repository(name: "ohayo-developers") {
        issues(last: 100 orderBy: { field: CREATED_AT, direction: DESC }) {
          nodes {
            timelineItems(first: 10) {
              nodes {
                ... on IssueComment {
                  id
                  body
                }
              }
            }
          }
        }
      }
    }
  }
`

コメント内容を取得する際は、注意が必要

  • GraphQL の Union 型を扱う
    • 事前定義のスキーマに対応したフラグメントを読み込む必要がある
{
  "__schema": {
    "types": [
      {
        "kind": "UNION",
        "name": "IssueComment",
        "possibleTypes": [
          {
            "name": "IssueComment"
          }
        ]
      }
    ]
  }
}

トークンが無いとダメ

  • 事前に Github の管理画面でトークンを発行する
    • admin:repo_hook という scope を指定してトークン発行の手続きをとる

GraphQL 用 Client を準備

  • フロントエンドで以下の通り構築する
    • 必要な依存関係をインストールする
    • 事前定義のスキーマに対応したフラグメントを読み込む必要がある
      • ApolloClient のキャッシュで読み込む
        • これが無いと issue のタイトルまでしか取れずコメントの内容までは取れない
import fetch from 'isomorphic-unfetch'
import ApolloClient from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { ApolloLink, concat } from 'apollo-link'
import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'
import introspectionQueryResultData from './fragmentTypes.json'

const GITHUB_API_V4 = 'https://api.github.com/graphql'

const fragmentMatcher = new IntrospectionFragmentMatcher({
  introspectionQueryResultData
})

const authMiddleware = new ApolloLink((operation, forward) => {
  operation.setContext({
    headers: {
      Authorization: `bearer ${
        import.meta.env.VITE_APP_GITHUB_API_ACCESS_TOKEN
      }`,
      Accept: 'application/vnd.github.v4.idl'
    }
  })
  return forward(operation)
})

const httpLink = new HttpLink({
  uri: GITHUB_API_V4,
  fetch
})

export const apolloClient = new ApolloClient({
  link: concat(authMiddleware, httpLink),
  cache: new InMemoryCache({
    fragmentMatcher
  })
})

コンポーネントで使う

  • 先に作成したクエリと Plugin を合わせ、適宜呼び出したいコンポーネントで呼び出す
    • @vue/apollo-composable の useQuery でクエリを読み込む
    • useResult でレスポンスが吐き出される
      • ここから先はよしなりにデータを加工してページの描画に繋げる
import { useQuery, useResult } from '@vue/apollo-composable'
import { searchQuery } from '../graphql/issue'

export default {
  setup() {
    const { result, error, loading } = useQuery(searchQuery)
    const issues = useResult(
      result,
      null,
      (data) => data.viewer.repository?.issues?.nodes
    )
    return { loading, error, issues }
  }
}

そしてページ描画へ

  • データを取得できたら、あとはよしなりにそのデータを加工してページ描画に繋げる
    • 今回は grid レイアウトを使ってカレンダーを作った

最後にこれだけは!

  • API の使い心地を考える
    • Notion の書き味は申し分無し、しかし現時点では API が完全でない気がする
  • Vite を静的サイトで使ってみて
    • 設定するのはちょっと面倒臭かった
      • 詳しくは後日ブログ書く🤙🏻
  • 雑談という取り組みから
    • 「誰でも参加 (編集も) できる」を目指して
    • 雑談だけではなく日々の知識の蓄積などでも?
  • お時間ある方は 5 分間耳だけでも聴いてみて

各リポジトリはこちら

  • ドキュメント
    • どなたでもお書きいただけます!
      • https://github.com/jiyuujin/ohayo-developers/issues
  • ウェブサイト
    • コントリビュート歓迎!
      • https://github.com/jiyuujin/ohayo-website

Thank you..🙇‍♀️

Made with Slides.com