Gatsby.jsの翻訳を手伝ってみたら(肩書だけ)OSSのメンテナーになった話

お世話になっているGatsbyのドキュメントを和訳するプロジェクトが進行していたので、一部を手伝うことにしました。 こちらのリポジトリをフォークしてIssueで宣言した上で翻訳・マージすることになっています

github.com

準備までの手順

  1. Issueで翻訳する場所を宣言する、反映を確認
  2. リポジトリをフォークする(Github上でボタンポチ)
  3. git clone [my-folked-repository]
  4. cd gatsby-ja
  5. git switch -c docs/accessibility-statement(docs/accessibility-statement.mdを翻訳する場合)
  6. yarn install

環境構築でハマったとこ

gitのバージョンが古くてswitchが使えなかったのでアップデート

sudo add-apt-repository ppa:git-core/ppa
sudo apt install git`

yarn使わずnpmで生きてきたのでyarnを新規インストール

公式の通り

classic.yarnpkg.com

curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt update && sudo apt install yarn

翻訳

  1. ブランチを切る
  2. スタイルガイドに従って翻訳する
  3. pull requestを出してレビュー対応する

Githubを用いたチーム開発している人には慣れたものだと思いますが、自分は初めてだったのでかなり緊張しました。

報酬

github.com

いくつか翻訳したページの一つが無事Mergeされました!
PRのマージに連動して、メンテナーのGatsby.jsのOrganizationに追加され、公式グッズのバウチャーも頂けることになりました。

まさかこんな形でOSSメンテナーの肩書(ただし翻訳のみ)を手に入れられるとは…5つPRが採用されると更にグッズが貰えるようなので、意欲が刺激されますね。

Gatsby.jsでGraphQLのスキーマから型を生成して利用する

TypeScript導入時はコンポーネント内に自分でPropsの型を書いていましたが、全てのコンポーネントでこれを行うと非常に面倒かつ、GraphQLのスキーマとオリジナルの型で二重に型を生成することになるので、自動生成するプラグインを導入してコンポーネントから利用します。

gatsby-plugin-graphql-codegenの導入

npmでインストールした後、生成するtypesファイルの宛先と監視するディレクトリを指定します。 これらはGraphQLのクエリを生成する部分が指定してあればよいので、特に目立った構成変更をしていない自分の場合はREADMEの指定をそのまま利用しました。

{
      resolve: "gatsby-plugin-graphql-codegen",
      options: {
        fileName: "types/graphql-types.ts",
        documentPaths: [
          "./src/**/*.{ts,tsx}",
          "./node_modules/gatsby-*/**/*.js"
        ],
        codegenDelay: 200
      }
    },

出力先fileNameは監視するディレクトdocumentPathsと同じ場所にしてしまうと自分を参照して無限ループになるので注意。

これで、ローカルサーバーを起動した後にクエリに変更を行うとプロジェクト内で使える型データがtypes/graphql-types.tsに吐き出されます。

コンポーネント内で型を利用する

前回までは自分で型を書いていました。

interface StaticQueryProps {
  allMarkdownRemark: {
    edges: Edge[]
  }
}

interface Edge {
  node: {
    frontmatter: {
      date: string
      title: string
    }
    id: string
  }
}

const recentPost: React.FC = () => (
  <StaticQuery
    query={graphql`
      query RecentPostQuery {
        allMarkdownRemark(limit: 4, sort: { fields: frontmatter___date, order: DESC }) {
          edges {
            node {
              frontmatter {
                date(formatString: "YYYY/MM/DD")
                title
              }
              id
            }
          }
        }
      }
    `}
 // render

自動生成した型を使うために、import文を1行書いて型情報を読み込みます。 この場合、GraphQLのクエリに名称をつけておくと[Query名]Queryという名称で型情報が出力されます。

~Queryという名前をクエリにつけていたので、~QueryQueryという型にならないよう全て書き直しました…

//...import
import { RecentPostQuery } from "../../types/graphql-types"

const sportsPost: React.FC = () => {
  const data: RecentPostQuery = useStaticQuery(
    graphql`
      query RecentPost {
        allMarkdownRemark(
          sort: { fields: frontmatter___date, order: DESC }
          limit: 4
        ) {
          edges {
            node {
              frontmatter {
                date(formatString: "YYYY/MM/DD")
                title
                cover {
                  childImageSharp {
                    fluid {
                      src
                    }
                  }
                }
              }
              id
              fields {
                slug
              }
            }
          }
        }
      }
    `
  )
//render

f:id:gensobunya:20200204173243p:plain
vscode capture

VScodeの補完と参照もバッチリ効くので、もうPropsの中身を見る度にあっちこっちのコンポーネントを見る必要なし!

欠点

各Functionの返り値を明確に意識する必要が出てきます。本来なら当たり前ですが、Vanilla JSだとよしなに書いても問題なく動いてしまうので、最初は苦労しました…

Gatsby.jsでTypescriptのコンポーネントを作ってみた

サークルサイトの再構築をするにあたり、Typescriptを導入してみました。

「最近の記事」コンポーネント

GraphQLで取得したデータに型を指定する

interface StaticQueryProps {
  allMarkdownRemark: {
    edges: Edge[]
  }
}

interface Edge {
  node: {
    frontmatter: {
      date: string
      title: string
    }
    id: string
  }
}

const recentPost: React.FC = () => (
  <StaticQuery
    query={graphql`
      query RecentPostQuery {
        allMarkdownRemark(limit: 4, sort: { fields: frontmatter___date, order: DESC }) {
          edges {
            node {
              frontmatter {
                date(formatString: "YYYY/MM/DD")
                title
              }
              id
            }
          }
        }
      }
    `}
    render={(data: StaticQueryProps) =>
      data.allMarkdownRemark.edges.map(({ node }) => (
        <div key={node.id}>{node.frontmatter.title}</div>
        //  component
      ))
    }
  />
)

メリット

  • Propsとして下位コンポーネントに引数を渡す際、「あれ?このProps何入ってたっけ」といちいち上位コンポーネントのコード読む必要がなくなる(このコンポーネントだとそうなってない)(PropsTypesでもいい)
  • MarkdownRemarkあたりのデータ構造、適当にえいやで動くように書いていたものの構造をしっかり理解してコードを書けるので手戻りが少ない

なぜJAMstackを採用するのか?

はじめに

この記事はJAMstack Advent Calender20日目の記事です。

qiita.com

フレームワーク類の実装や特定のCMSが~~といった内容はこの記事には皆無です。
個人開発、企業のWEBサイト・ブログでそれぞれJAMstackを採用するメリット・デメリットについて考察します。

なお、筆者のJAMstack経験は趣味の自転車に関するブログ及び、同人サークルサイトをそれぞれGatsby.js, Hugoで運用しており、所属企業のコーポレートサイトのアーキテクチャ刷新に検討している程度の経験です。

個人開発(自分)編

当初、個人ブログはBloggerを使っていたり、GCE上にWordpressを立てて運用していたりしましたが、どうしてもデザイン自由度や広告問題、アクセス性能問題がついてまわるため、Hugoに行き着き現在ではGatsby.jsで運用されています。

運用(費用・人的)コスト

WEBサイト運営で重視したい点は(個人的に)下記三つです。

非常に残念なことに、これらをブログサービスや、ペライチさんのようなSaaSで満たそうとすると大抵有料になります。当然ですね。

しかし、Gatsby.jsやHugoで作ると上記の要件を満たすために必要なサービスは以下の2つです。

  • Github無料
  • Netlify… 無料

なんと!完全無料で思いのままにWEBサイトが作れる!

しかも、WordPressなどのCMSをセルフホストして使った場合はアップデートを随時行っていないとあっという間に悪い大人の餌食になってしまう一方、JAMstackは静的配信なので脆弱性対応は最小限で済みます。

ただし、本当に労力を割ききれない場合は(もしくは力を入れたいプロダクト・仕事が別にある場合)、はてなブログやnoteのような完全なマネージドサービスを使ったほうがよいでしょう。
自分も、フルタイムで別の仕事をしつつ、運用するサイトは2つが限界だと判断したため、この技術ブログははてなブログを使っています。

コンテンツ移植性

WordPress <=> Blogger など、サービス間のマイグレーションも、基本的にMarkdown形式で記事が残っているため、遥かに簡単に別のフレームワークに変更できます。
自分も、今度サークルサイトをVue.jsのGatsby.jsことGridsomeに変更してみる予定です。コンテンツは特に手に入れなくとも、ビルド環境とテーマを組み直せば完了です。

移行ツールなどをわざわざ用意してデータフォーマットを変更する必要はありません。

エンタープライズ

さて、企業においてJAMstackのメリットを活かせるユースケースを考えてみます。

CMS上でコンテンツを編集し、IaaS+LB+CDNで公開する王道な構成と、Gitリポジトリ+CI+静的ホスティングのJAMStack構成を比較してみます。

セキュリティ

前述の通り、静的ホスティングサービスを利用することで意識しなければならない点がだいぶ減ります。最終的にReactアプリを配信する場合などはアプリレイヤーのみ、企業側でコントロールすればよくなります。

定期的にパブリッククラウドの監査、ミドルウェアのバージョン確認、セキュリティパッチの適用作業などをこなしていく必要はなくなります。

コンテンツベンダーのいる業務運用

Qiita的にはあまり聞かない話題ですね

そこそこ大きい企業では部門ごとにコンテンツの発注先が違うことはよくあることかと思います。
CMS上でコンテンツ作成するタイプのCMSを使っている場合、コンテンツの作成お作法や運用ルールなどを新規ベンダー追加の度に伝える必要があり、情シスは死にます。

さらに、プロモーションページなど継続的に取引が発生しないコンテンツに関して1回だけのためにCMS運用教育を行うことは最高に非効率です。

代替案として、特定ディレクトリ下で機能することを前提にした静的なHTML+CSS+JSを納品してもらい、最も取引の大きいコンテンツベンダーに依頼してCMSで使えるコンテンツ形式に変換してもらうことも考えますが、責任の所在が曖昧になって…よくないことになります。

JAMStackのフレームワークの場合、納品された静的ファイルを/publicに配置してCIを回すだけで新しいコンテンツを追加できます。

プルリクエスト単位でステージングサイトを作ることができ、Gitリポジトリでコンフリクトを管理できることも大きいです。特定企業の中でしか通用しないCMSの運用ルールを覚えてもらうより、Gitの使い方に沿ってもらったほうが生産的です。

部門間

CIのHTTP hookさえキックできるのであれば、部門ごとにHeadless CMSを用意して完全にコンテンツ作成を分離しつつ、最終的に一つのWEBサイトとして公開することも可能になると妄想中です。

製品ページはContentful、プロモーションページは静的ファイルで作成、コーポレート関連の情報は今までの試算を活かすためにDrupalをHeadless化してビルド時にコンテンツ取得…なんてこともできます。

まとめ

ステージングサイトのアクセス制限をどうする?結局Gatsbyなどのフレームワークでテーマを作る部門に要望が集中するのでは?などの課題もありますが、大規模にも使える可能性を秘めているアーキテクチャだと考えています。

もっと事例が増えて、一般的に使われる技術に成長してほしいですね!

Google Tag Managerでパスごとに読み込むscriptを変更してパフォーマンス劣化を防ぐ+Adsenseタグの配信方法

eBayのスマートタグを記事ページのみ配信してみる

eBayパートナーネットワークのスマートリンク機能は、Scriptタグを配置することでページ内のebayリンクを全てアフィリンクに置き換えてくれるスグレモノ。ただ、Lighthouseのスコアを悪化させるので不要なページには置きたくない。

そこでタグマネージャーを使って記事ページのみにこのスクリプトを配信する。

f:id:gensobunya:20191109112329p:plain
trigger

トリガー設定でPage Pathを/postに指定することで、トップページやタグ記事一覧のページを対象外にして配信できる。 これまでなら面倒なJavaScriptを書かなければならなかったことを考えると大変にかんたん。素晴らしい。

2019/10からのAdsenseタグと相性悪くてクソ

<head>タグ内に配置することが必須なタグはGTMで配信しづらい。

代表例はアドセンスの自動広告タグ(2019/10最新版)。こいつは<head>内配置指定されている。

<script data-ad-client="/*your adsense id*/" async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>

回避策は古いタグを使って「HTMLタグ」を使って配信すること。機能に変化はない。

<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
  (adsbygoogle = window.adsbygoogle || []).push({
    google_ad_client: "/*your adsense id*/",
    enable_page_level_ads: true
  });
</script>

自転車ブログをHugoからGatsby.jsで書き直した

Gatsbyに変更した理由

Hugoは元々サイトビルドが爆速という理由で採用しました。事実、ローカルサーバを作る場合でも300ページ近いブログも一瞬(0.2秒程度)でビルドが終わりlocalhostで接続できるのでこの点については全く不満なし。

ただし、主に実際にWEBサイトとして運用するにあたって下記の点が課題になっていました。

  • レスポンシブな画像配信がテーマ任せで、デバイスごとに最適な配信がしづらい
  • 上記の影響で結局shortcodeというHugo特有の記法をMarkdownに書かねばならず、可搬性が落ちている
  • 画像サイズが最適でない影響でSEO悪化
  • 初期のバージョンで構築したので、git submoduleを使ったテーマのアップデートができなかった(そもそもテーマも結構いじってしまったので単純アップデートができない)
  • テーマをいじるのにGolangでフロントエンドを書くという苦行をしなければならない

特にきついのは画像配信周りとテーマの弄り難さでした。

そんなわけもあってReact.jsでフロント周りを自分で1から書きながらGatsby

採用したフレームワークなど

ほぼ自分でデザインを書くと決めていたので、採用するフレームワークも選び放題なのでなるべくモダンで、かつ最小で済むようにしました。
Gatsby-starter-blogをベースに使わないと思われるプラグインを削除して、下記のサービスやフレームワークに必要なものをインストール。
今はgatsby-starter-blog-mdxで始めるのがいいと思います

www.gatsbyjs.org

npmの文脈で全てが完結するので、新たに勉強しなければならないことが少なく割と楽ちんでした。
プログラミングではないけど、Google Tag Managerはかなり便利だったので別途記事書きます。

特に工夫という工夫はなく、各プラグインフレームワークの記載に従って実装しました。

というよりも、3ヶ月ほどかかってそれなりにハマったはずなのに、逐一メモらなかったために終わったら綺麗サッパリ忘れてしまった。どこかでハマってたどり着いた人はリポジトリ参照してください。

github.com

強いて言えば、Hugo時代の遺産であるdraftフラグを使うためにGraphQLに1行追加しました

....
    allMarkdownRemark(
      sort: { fields: [frontmatter___date], order: DESC }
      skip: $skip
      limit: $limit
      filter: {frontmatter: {draft: {eq: false}}}
...
...

悲しい作業

一番不毛だと感じたのがHugo用に書いたShortcodeと、Markdownのfrontmaterを正規表現と手動Grepで置き換える作業。
Markdownでコンテンツ管理する以上は、標準的な(最低でもGithub flavorな)Markdown記法だけに利用をとどめましょう…

Markdown以外から画像を利用するのはGatsby.jsにおけるクソ実装レベルがかなり高い行為なのでなるべくMarkdownからページ生成したほうがいいということはわかった。

移行結果

f:id:gensobunya:20191109104458p:plain
lighthouse

出来上がったものがこちら

blog.gensobunya.net

記事書いといてLighthouseスコア100じゃないのかよとツッコミたくなりますが、以前のブログは画像サイズのせいで70台などと全体的に低迷していたのでかなり改善されました。Performanceは毎回数字が変わるので飾りです。AccessibilityはもうちょいUIの実装力や色の知識を上げれば90後半になりそう。

テストに関して

ホスティングに使っているNetlifyのDeploy-preview機能を利用しました。

リンクしているGithubリポジトリのプルリクから自動的にステージングサイトを生成してくれるので、CI/CDをほぼ書かなくても実装できます。
今回、本番サイトはhugoでビルドしているため、ビルドコマンドが変更となりますがNetlifyはこんな要件にも対応しています。

プロジェクトrootのnetlify.tomlに下記のように環境毎のビルドコマンドと公開ディレクトリを指定できるので本番と違うコマンドで自動ビルド可能です。

[context.production]
  command = "npm run build"
  publish = "/public"

[context.deploy-preview]
  command = "npm run build"
  publish = "/public"

[context.branch-deploy]
  command = "npm run build"
  publish = "/public"

JavaScript同士ならnpm run buildの中身を書き換えておけばいいんですが言語ごと変わるとそれもできないのでありがたい。 管理画面でBranch deployをONにすると特定のブランチが更新されるたびにステージングサイトを作ることもできます。Netlifyマジですごい、神。

Withingsの体組成計を買って、GASでSlackの減量チャンネルにポストする

f:id:gensobunya:20191108222621j:plain
withings

ものぐさレコーディングダイエット

減量できたことないけど、いろんな事を記録して可視化するのはいいこと。
そして、体重の崩壊を防ぐ仲間内Slackでは、このようにpostした体重と体脂肪率をグラフ化してくれるスクリプトがすでに動いています。

f:id:gensobunya:20191108222638p:plain
weight graph

f:id:gensobunya:20191108222655p:plain
fatrate graph

postしたusernameを自動的に取得して、それぞれの人の記録をトレンド含め表示してくれています。
今までは、愛用していたTANITAの体組成計から手動でメモしていたのですが、いかんせん朝にメモ作業するのは結構面倒。目も疲れるし。

そこで、Wi-Fi対応の体組成計を購入して、乗ったら自動的にSlackに体重と体脂肪率を飛ばすシステムを作りました。
白羽の矢がたった機種がこちら。

体重の計測は200g刻みと、21世紀の体重計とは思えない精度ですが計測結果が100g程度変動していたところで一喜一憂するものでもなし、インターネット直結という点を重視しました。

とりあえずIFTTT

計測した内容はWithings Health MateというWEBサービスにアップロードされ、モバイルアプリでも閲覧できます。
本体の設定はすべてモバイルアプリ経由なので楽ちん。このサービス自体がIFTTTに対応しているので、まずは直接IFTTTを使ってSlackに連携してみます。

f:id:gensobunya:20191108222807p:plain
ifttt to slack

骨量や筋肉量、水分量も計測できるのですがAPIで取得できるのは「体重」「前回体重差」「体脂肪量」「前回体脂肪量差」「体脂肪率」「計測日時」の6点のみでした。
こちらのIFTTTアプレットの起動結果がこちら。

f:id:gensobunya:20191108222822p:plain
slack post

連携はできましたが、一つ思い出してください。

postしたusernameを自動的に取得して、それぞれの人の記録をトレンド含め表示してくれています。

IFTTTのbotアカウントがpostするだけだとグラフ化の方のbotが正常に動いてくれません。自分のアカウントがpostしたことにしないと…
ちなみにコードを書かなくてもMyfitnessPal経由でGARMIN CONNECTやSTRAVAに体重を連携できるので単体でも結構便利です。

Slack APIを直接叩こう

Slack APIの仕様によると、chat.postMessageas_userオプションをtrueにしてPOSTすればいいようです
IFTTTのオプションにはないので直接何かしらのスクリプトを起動することにします。

f:id:gensobunya:20191108222834p:plain
overview

どこで実行するかが問題ですが、データの受け皿を用意しつつ無料でスクリプトを動かせるとなると、やっぱりGoogle SpreadSheetとApps Scriptがシンプルで簡単です。 tokenの取得やアプリの登録に関しては割愛します。

一回、IFTTTでスプレッドシートにデータを飛ばしてカラムを確認した後に以下のスクリプトを、編集をトリガーに設定して実行します。 Date,Weight,Weight w/o fat,Fat weight,Fat Percentの順に4カラムIFTTTで入力しています。

function getLatestWeightData(){
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const lastRawNum = sheet.getLastRow();
  const dataRange = sheet.getRange(lastRawNum, 1, 1, 5) //Date,Weight,Weight w/o fat,Fat weight,Fat Percent
  const postData = {
    "weightKg": dataRange.getValues()[0][1], 
    "fatPercent": dataRange.getValues()[0][4]
  }
  Logger.log(postData)
  return postData
}


function postMeasureDataToSlack() {
  //Get latest row data
  const data = getLatestWeightData();
  const postMessage = data.weightKg+"kg "+data.fatPercent+"%";
    
  //POST to Slack channel
  const baseURL = "https://slack.com/api/chat.postMessage";
  const postData = {
    "channel": "<your channel name>",
    "as_user": true,
    "text": postMessage
  };
  const options = {
    "method": "post",
    "contentType": "application/json; charset=utf-8",
    "headers":{
      "Authorization": "Bearer <!!your access token!!>"
    },
    "payload": JSON.stringify(postData)
  }
  
  const response = UrlFetchApp.fetch(baseURL, options);
  Logger.log(response)
  return
}

Slackグループの設定がありませんが、トークンがグループごとに発行されるのでそこで認識されています。
"as_user": trueを設定したので、あたかも自分が発言したかのようにチャンネルにポストされます。

f:id:gensobunya:20191108222850p:plain
users post

仲間内で牽制し合うコミュニティではIFTTTよりこちらのほうがより便利かと思います。
他にも自動で有給連絡したり、as_userオプションの使い道は結構ありそうですね。

アプリ連携体重計だとその後の広がりがあまり良くないのですが、WEBサービスだと連携も簡単です。タニタオムロンも、WEBサービス連携する体重計は結構高いので1万円程度で買えるWithingsはかなりコスパ良い買い物でした。