無銘雑記

これ書けばいいの?

サークルのHPリニューアルしました

f:id:igara1119:20190303145348p:plain
平成の終わりが近いですね

先にサークル現状の報告

この場を借りて先に今のサークル(shin・DO・meeeee)がどうしているのかという報告をさせていただきます。

結論言ってしまうと 先月の2月5日に 技術書典6 のサークル当落通知というのがあって落選してしまったので技術書典6には参加しません。

技術書典5以降の技術書典6に向けたサークル内部の動きとして

  • GitLabのサブプロジェクト機能・issues board機能使っていい感じに細かいタスクの可視化 f:id:igara1119:20190303145554p:plain
  • Hangoutで打ち合わせするようになった

とかあって前回とは違った問題解決できてきたなぁと思ったんですけどね。

HPリニューアルしました

こちらになります。

shindomeeee.github.io

リニューアルをおこなった理由としては先ほどのissues boardの画像で目移りしたかもしれませんが

ホームページメンテしんどい問題

があって告知用とかマルチに使おうとして結果的に放置されてしまったというのがありました。 HPの作成段階では実績もなく なんとなくなデザイン作成 とかの負債があったなぁと感じてあのHPの立ち位置を考え直し、

  • みんなブログなら記載する
  • 実績のみ載せるLP的なページの認識であった方がライトで良さそう

というのに気づいて思い切って今までのホームページを捨てることにしました。

HPにあるリンクとかもGoogleのスプレットシートで管理するようにしたのでよりサークル内部の情報をまとめやすくなったんじゃないかなと思っています。 f:id:igara1119:20190303152113p:plain

あとはイラストなど素材も充実してきたのでHPにも導入するようになったのも大きな変更です。 (あのイラストは汎用性高いので気に入ってます。

ここから玄人バイニン向け

あのHPを僕1人でメンテするのもあれで、やったこと結構エグいのが多かったのでナレッジ残す意味あいで色々記載します。 ※記載している内容はほとんど僕の趣味によるものが強いです。

Blogs一覧取得API作成

  • API・DBはFirebaseのCloud Firestore(最近GAになったらしいですね
  • マスタ管理としてSpreadSheet
  • SpreadSheet -> Cloud Firestoreにデータ反映する仕組みとしてGoogle Apps Scriptを使用しています。

図にするとこんな感じです

f:id:igara1119:20190303162325p:plain

なぜの構成にしたかというとサークルメンバーのGoogleアカウントわかっていたのでIAM管理もGoogleにさせてしまった方が楽だったからです。

Cloud Firestoreの設定

ルール

SpreadSheetに入力できてFirebaseのロールを持っている人への書込み権限 一覧のデータを取得するための読み込み権限 を下記のようなので設定

service cloud.firestore {
  match /databases/{database}/documents {
    match /blogs/{document=**} {
      allow read;
      allow write: if request.auth;
      allow delete: if request.auth;
    }
    match /events/{document=**} {
      allow read;
      allow write: if request.auth;
      allow delete: if request.auth;
    }
  }
}

SpreadSheetの設定

f:id:igara1119:20190303170609p:plain

CSVにすると

id,title,url,tags,created_at,document_id
1,技術書典5当選しました!!!,https://ultrabirdtech.hatenablog.com/entry/2018/08/02/065033,["技術書典", "技術書典5"],2018-08-02,hogehoge

な構成にし、列の説明として

  • id -> 順番
  • title -> ブログタイトル
  • tags -> 現在使用してないけど絞り込み検索とかで使用する想定
  • created_at -> ブログの公開日
  • document_id -> Firestoreのdocument_id

な感じで

f:id:igara1119:20190303171445p:plain

SpreadSheet上に描画ツールで作成した更新ボタンにGoogle Apps Scriptのスクリプトを割り当てできるようにします。

Google Apps Script

Google Apps Script経由でSpreadSheetの内容取得、Firestoreに書込みができるように マニフェストファイルを編集します

appsscript.json

{
  "timeZone": "Asia/Tokyo",
  "dependencies": {
    "libraries": []
  },
  "exceptionLogging": "STACKDRIVER",
  "oauthScopes": [
    "https://www.googleapis.com/auth/firebase.database",
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/auth/spreadsheets.currentonly",
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/datastore"
  ]
}

肝心なのがoauthScopesで

で追加してます。

あとはスクリプトの追加で

blogs.gs

function setBlogs() {
  // 列の扱うデータの配置
  const columnNumbers = {
    id: 0,
    title: 1,
    url: 2,
    tags: 3,
    created_at: 4,
    document_id: 5
  }

  const rowNumbers = {
    // 列名がある箇所の配置
    scheme: 0
  }
  const apiUrl = "https://firestore.googleapis.com/v1/projects/(project id)/databases/(default)/documents/blogs"
  // 後にdocument_idを取得するために使用
  const removeString = "projects/(project id)/databases/(default)/documents/blogs/"

  // マニフェストファイル(appsscript.json)にあるoauthScopesで許可されたoauth tokenを取得
  const token = ScriptApp.getOAuthToken()
  const headers = {
    authorization: "Bearer " + token
  }

  // 対象のSpreadSheetのURL
  const url = "https://docs.google.com/spreadsheets/d/(SpreadSheet ID)"
  const spreadsheet = SpreadsheetApp.openByUrl(url)
  // SpreadSheetの読み込みたいシート
  const blogsSheet = spreadsheet.getSheetByName('blogs')
  const blogsData = blogsSheet.getDataRange().getValues()
  // IDの逆の順番にする(最新のものを先に登録させるため Firestore REST APIのorderBy asc があれば...)
  blogsData.reverse().pop()

  blogsData.forEach(function(blog, rowIndex) {
    const id = blog[columnNumbers.id]
    const title = blog[columnNumbers.title]
    const url = blog[columnNumbers.url]
    const tags = JSON.parse(blog[columnNumbers.tags])
    const created_at = new Date(blog[columnNumbers.created_at]).toISOString()

    if (blog[columnNumbers.document_id]) {
      const targetDocumentId = blog[columnNumbers.document_id]
      const deleteOptions = {
        method: "delete",
        contentType: "application/json",
        headers: headers,
        muteHttpExceptions: true
      }
      const deleteResponse = UrlFetchApp.fetch(apiUrl + "/" + targetDocumentId , deleteOptions)
      const deleteResponseCode = deleteResponse.getResponseCode()
      const deleteResponseBody = deleteResponse.getContentText()

      if (deleteResponseCode === 200) {
        const deleteResponseJson = JSON.parse(deleteResponseBody)
      } else {
        Logger.log(Utilities.formatString("Request failed. Expected 200, got %d: %s", deleteResponseCode, deleteResponseBody))
      }
    }

    if (id && title && url && tags && created_at) {
      const createPayload = {
        fields: {
          id: {
            integerValue: rowIndex + 1
          },
          title: {
            stringValue: title
          },
          url: {
            stringValue: url
          },
          tags: {
            arrayValue: {
              values: tags.map(function(tag) {
                return {stringValue: tag}
              })
            }
          },
          created_at: {
            timestampValue: created_at
          }
        }
      }

      const createOptions = {
        method: "post",
        contentType: "application/json",
        payload: JSON.stringify(createPayload),
        headers: headers,
        muteHttpExceptions: true
      }
      const createResponse = UrlFetchApp.fetch(apiUrl, createOptions)
      const createResponseCode = createResponse.getResponseCode()
      const createResponseBody = createResponse.getContentText()

      if (createResponseCode === 200) {
        const createResponseJson = JSON.parse(createResponseBody)
        const documentId = createResponseJson.name.replace(removeString, "")
        blogsSheet.getRange(id + 1, columnNumbers.document_id + 1).setValue(documentId)
      } else {
        Logger.log(Utilities.formatString("Request failed. Expected 200, got %d: %s", createResponseCode, createResponseBody))
      }
    }
  })
}

な感じで雑にSpreadSheetにあるデータを正とするためにFirestore上のデータ全消しした後で再度追加するようにしてます。

このスクリプトに先ほどSpreadSheetのボタンにスクリプトの割り当てをすることでボタンから更新することが可能になります。

フロント実装

github.com

がっつり変更入れてるので現在(2018/03/03時点)でもPR中にしてます。

昔ながらのpublic_html に 自身で編集したindex.html 置くような運用辞めた

どう言ったことというと以前はindex.html, css, jsを直接変更していたのを辞めたということです。 いわゆるジェネレータやbundlerのようなものを用いていなかったため新しいページを作成するときはディレクトリ切ってindex.htmlを設置し、共通で変更必要なものを都度都度index.htmlを直すようなことを辞めました。 なぜ今までこのような運用していたのかというとサークルのみんながどのようなの得意としているのか理解していなかったのもあり一番共通な認識でできるだろうと当時思っていたのですが、結果としていじる側にしんどみが出てしまっていたという風に感じています。

ジェネレータとしてGatsbyJSにした

GatsbyJSのベースとなるReactJSを用いるため別の学習コストが発生してしまっているのですがここは僕が率先してできるので無理やりに選定しました。 Vueベースなものを扱うというのもありましたがTypeScriptで補完が効くようにすぐに作れるのが僕の中でReactだったのでGatsbyJSにしたというのがあります。 知り合いでよくJekyllで作ったGitHub Pagesを見てたりしてたのですが僕個人としてフロントで動的に見させたい需要が強かったのでNodeJSよりのものを選びました。

デプロイ周りの変更

GitHub Pagesのホスティングの仕組みとしてmasterブランチのものが静的サイトとして公開されるようになっていて 以前は様々な対応したものをPR出してmasterブランチに反映されて初めて公開されるような運用していました。

しかし今はmasterブランチとは別でdevelopブランチをデフォルトブランチとし、 yarn deployを実行することでmasterブランチ以外でもすぐにmasterブランチにあげて GitHub Pagesに反映されたものを確認するようにしました。 yarn deploy と言っても厳密には gatsby build と NodeJSライブラリのgh-pagesによるmasterブランチ反映をさせています。

型システム & コンポーネント思考 & できるだけすぐに捨てられるように対応

CSSの運用はCSS Modulesでやるようにしました。 Reactならstyled-componentsでやる手段もありますがelementとstyleが密結合すぎると感じて基本的なhtmlとcssで運用した方が良いと感じてCSS Modulesでやるようにしてます。 CSSのやつもNodeJSライブラリのtyped-css-modulesを用いれば型定義化され補完が効くようにできるのでそうしてます。 あと型導入によってAPIとして使用しているFirestoreが返してくるJSONの形(これとか)であったり、 よく困惑するコンポーネントが持つ独自の状態(これとかやってることとして背景画像が読み込まれたことを保証して背景画像のアニメーションをするようにするためのstate変更をさせる実装してます) というのも型化されて見通しいいように?しました。

jest導入

Reduxによる状態変更テスト以外に独自のコンポーネントのテストも行うようにして作成したテストファイルに対して必ずカバレッジが100%になるように頑張ってます。

f:id:igara1119:20190303183634p:plain

API周りはモック使用したり(これとか)、 デフォルトの値をモックしたりとか(これとか)、 画像の読み込み完了時にstate変更されるようなテストをjest.spyOn使用したとか(これとか)あります。

UI周りはBlogs一覧でリンク何個あるかのテストとかはやってますがどういったスタイル当たるかというかまでやってません。 (そこに関してはjestによるテストというか自動でスクリーンショットとるかstorybookによるもので担保するのが疲弊しないんじゃないかと個人的に思っています

しんどみの技術書典5 中間報告的な何か

この記事の内容

宣伝とかはバードくんやかんずさんが行っているので

ultrabirdtech.hatenablog.com

k-anz.hatenablog.com

僕からは入稿までにどういったことを行ったのかという報告をしたいと思います。

行ったこと

サークル名の決定

サークル名の候補としてメンバーの共通点から決めようというのがありました。
色々あったのですが身内ネタが濃すぎ・喧嘩を売り過ぎてるネーミングだったという自粛の点があり、生き残った候補のshin・DO・meeeeにしたというのがあります。

みんな なんらかの業(カルマ)とかしんどみあるでしょ?

意味とかは宣伝のブログに記載しているので割愛します。

気付かれている方々もいるかもしれませんが、
技術書典5のサークルリスト上でshinじゃなくてsinになってるあれは
スペルまで明確に決めていない時に先に登録してしまった名残で、
間違えるのもsin(罪)だなというの感じて残したまましております。

サークルカットの作成

techbookfest.org

技術書典5の詳細にある画像は別のプロのもきちくんに作成していだたきました。
なんでああなったのかの図にするとこれしかない案からデフォルメされた画像を作ってくれました。

f:id:igara1119:20180923174130p:plain
爆誕

PSDデータのレイヤー見ると細かい下書きの非表示レイヤーが残ってたり、
画像作成してと依頼したのも直近なのにも関わらずに対応していただいて
本当にありがとうございました。

先駆者の意見を聞いてみること

初めて出展する人向けの勉強会があったので参加しました。

techbookfest.connpass.com

その中で知ったこととして

  • 確定申告方法
  • 印刷所
  • 作成方法
  • ロイヤリティフリー素材の扱いについて (レビューの時にいらすとやの絵があるなぁと思ったけど規約内だったのでOKそうとか気付きがあった
  • 過去の現場の声

など他のサークルの人から聞けたというのがあるので
感謝だなぁと思っています。

本の作成手順的なこと

本のタイトルの決定

みんな別々のテーマで書いていたので汎用性あるようなタイトルにならざるを得なかったです。

執筆内容

モチベーション保って書けることを重視し各自自由にテーマを決めて書くようにしました。
途中でテーマ変えたりとかもありました。

執筆環境

これ完全に僕の圧でRe:VIEWで作成しましょうという風にしてしまったなぁと負い目を感じてますが、Re:VIEWで作成しました。
ただRe:VIEWにしたことによるCIとの連携や、
GitLab上でのMerge Requestのレビューが上手くいきましたので結果的によかったと思います。

今回の環境のサンプルとしてこんなものを作成しました。

gitlab.com

CI上で行わせたこととして

  • textlintによる簡単な文章構文の静的解析
  • 電子書籍用のPDF作成
  • 印刷用のPDF作成(本文のみのPDFにして隠し(通し)ノンブルを追加、ページ総数が奇数の時は空白ページをうめる
  • ビルド結果をDiscordに通知

Merge Requestのレビューついては
今までこのメンバーで議論をするということがあまりなかったので楽しかったです。
(途中、議論したがり欲でようわからん難癖議論をしてすみませんでした。

勉強会でも議論になっていたことですが、
レビューしてくれる人がいなくてどうしようというのがある中で
僕達のサークルでは相互にレビューしてくれる人がいるというのはありがたいことだと思いました。

しんどみあったとこ

  • マージン調整とかでLaTexの知見が必要になったところ
    CSSやっていき組だったらまた別の世界があったのかもしれない
  • textlintででたerrorをignoreするか議論
    (固有名詞として存在するけど長い漢字の単語なのでerrorが出るとか
  • PDF出力すると半角の_(アンダースコア)がなんかでかくみえる (未解決

入稿手順的なこと

印刷所の決定

日光企画さんにしました。
理由としてはhttp://www.nikko-pc.com/only-event/backup.htmlとかのページを見ると入稿から本が納品される日の目安が見れて良いというのと
技術書典のバックアップ印刷所でもあるので本の受け取りが会場でできて楽そうだったからです。

入稿日の決定

早めに印刷所に入稿すると割引が適応されるので当初は50%引きの日に入稿しようとしていました。

入稿の仕方・添削をしてもらう

日光企画さんにお伺いし、いただいた指摘として

  • 出力された本文がB5サイズのものではない
    (今回はB5想定ではない本文の中心から無理やりB5サイズに適応した
    Re:VIEWの設定でtexdocumentclass: ["jsbook", "uplatex,oneside,b5j"] を忘れていた
  • 本文全体に通しノンブルがない
  • 本の厚さに適応した表紙の設置がされていない
    (もきちくんに依頼してたけど細かい指示内容なしで 日光企画さんのテンプレートに表紙載っけてとしか言わなかったので反省してます。 あとB5で入稿できそうな雰囲気がなかったので別のフォーマットの提示とか かわいそうな事させたなぁと思っています。

入稿申し込み・入金

申し込みした結果

  • B5
  • 34ページ(表紙4ページ + 本文30ページ
  • 50冊
  • 左綴じ
  • 平綴じ
  • クリアPP(表紙の加工
  • 本文用紙 上質90
  • オンデマンド スミ
  • 割引適応40%

計15,180円 になりました。

さいごに

様々な方の知見を借りる事で進める事ができたなぁと思います。

当日寝坊しないようにがんばろう