記事作成から公開までをGitHubで完結できる技術ブログ基盤作り

キャリトレのプロダクト開発をしている髙宮です。

本エンジニアブログはWordPressやはてなブログではなく、独自に選定した技術スタックで構築運用しています。 ここでは、GitHubのPullRequestでMarkdownの記事の作成・レビューを行い、マージされたら自動的に公開されます。

今回は全社のエンジニアが使っているGitHubを入り口に置くことで、かなり気軽な投稿フローが構築できました。 PullRequestでの編集作業もオンラインでかなりの所まで完了するので、高効率な編集ができている実感があります。

本記事では、構築に使用した技術スタックと得られた運用フローに関してご紹介いたします。

TL;DR

  • 記事はMarkdownで記述しGitHubで管理するとエンジニアネイティブ
  • HTMLはHugoで生成すると、自由なカスタマイズや書きながらのローカルプレビューが便利
  • ホスティングはNetlifyで行うと、GitHub連携・Hugoビルド・独自ドメインでのSSL化と配信に必要な全てをやってくれて最高

エンジニアブログの要件定義

エンジニアが書くのだから、エンジニアネイティブな色々を使って好き勝手やりたい。
この思いをもとに要件を決めました。

エンジニアネイティブなブログ運用

Web画面ポチポチで運用するのはエンジニアっぽくない。

  • Markdownで書きたい!
  • Gitでバージョン管理したい!
  • GitHubのIssueでネタを受け付け、PullRequestで編集や校正したい!

ブログのネタ集めのハードルを下げたい

エンジニア数が増大し、多くの事業が展開されていると、会社のどこでどんな技術を使っているのか?といった情報は見えにくくなってしまいます。 各エンジニアが記事にできそうなネタを持っていても、投稿までのハードルが高いとせっかくのネタが拾えません。

運用作業を無くしたい

草の根的に有志が集って準備を始めたため、WordPressのサーバ管理などで人の仕事を増やしたくありません。

その他

  • 独自ドメインでやりたい
  • HTTPSでホスティングしたい

エンジニアブログの技術スタック

これらの要件を満たすブログ技術を色々と試行錯誤したどり着いた技術スタックがこちらです。 エンジニアブログの技術スタック

  • Github: ブログの機能・デザイン・記事のコード管理
  • Hugo: 静的サイトジェネレータ
  • Netlify: 静的サイトビルド・ホスティング・独自ドメインSSL化

Hugoとは、オープンソースの静的サイトジェネレータで、高速性と柔軟性を特徴としています。 Go言語のhtml/templateの記法でサイトのテーマを作成し、Markdownで作成したコンテンツと結合させHTMLを作成します。類似の機能を持つ静的サイトジェネレータにはJekyllHexoなどがあります。

Netlifyについては一言では説明しづらいのですが、非常に多機能な静的サイトのホスティングサービスです。 トップページには以下のような文言でサービス説明がなされており、モダンなWebプロジェクトの管理のためのオールインワンなワークフローを謳っています。

Build, deploy, and manage modern web projects

An all-in-one workflow that combines global deployment, continuous integration, and automatic HTTPS. And that’s just the beginning.

NetlifyはGitHubで管理されているHugoプロジェクトをビルドし、指定した独自ドメインでHTTPで公開してくれます。 ただこれだけでは語りつくせません。以降で説明するエンジニアブログの運用フローで詳細に触れます。

エンジニアブログの運用フロー

この技術スタックにより、ネタ集めから記事公開までの運用フローが全て連携できるようになりました。 ここでは、次の4つに分けて実施しているブログの運用フローを紹介します。

  • GitHubのIssueでネタ集め
  • Markdownで記事の執筆
  • PullRequestでレビュー
  • masterマージで自動的に公開

GitHubのIssueでネタ集め: 社内のエンジニアに広く寄稿を募ることができる

ブログの機能・デザイン・記事は全てGitで管理しGitHubにホストしています。 リポジトリは社内のエンジニア全員がアクセス可能で、 master 以外のブランチは自由に扱えます。 また、 README.md には下記抜粋のように、社内のエンジニア向けの執筆ガイドを準備しており、寄稿のハードルを下げています。

# [BizReach Tech Blog](https://tech.bizreach.co.jp/)
![BizReach Tech Blog](https://tech.bizreach.co.jp/img/default_600x314.jpg)

## このリポジトリを見ている人へ
記事ネタのIssueを立てて貰えれば、編集部から記事化のお声掛けします。

## 執筆ガイド

1. 記事ネタの骨子となるIssue作成  
   [Issues](https://github.com/bizreach/engineers-blog/issues/new) を作成して、テンプレートにあわせて記事ネタを書きます。

1. 編集部確認と担当割り当て
   Issueを編集部でレビューし、問題なければ担当編集を割り当てます。  
   参考: [レビューフロー](https://github.com/bizreach/engineers-blog/wiki/レビューフロー)

1. 記事ファイル作成  
   まずはこのリポジトリをcloneします。  
   執筆する記事のタイトルに対応するURLを決め、骨子のIssue番号と合わせて以下のパスで `index.md` を作成します。
   `content/posts/{骨子のIssue番号}/{記事のタイトル}/index.md`  
   画像は `index.md` と同じディレクトリに入れます。  
   記事のURLは `https://tech.bizreach.co.jp/posts/{骨子のIssue番号}/{記事のタイトル}/` になります。

1. 記事を執筆
   作成した `index.md` にMarkdown形式で記事を書きます。  
   Front Matter と呼ばれる下記メタデータをファイルの先頭に記載してください。
...

これを読んでGitHubにIssue登録してもらい、編集部から記事化の声掛けをしています。

Markdownで記事の執筆: ブログの完成像をローカルでプレビューできる

記事をMarkdownで書きますが、HugoがWebサーバ機能を持っており、書いている記事のブログ上での完成像をプレビューすることができます。 この情報も以下のように README.md に執筆者向けのガイドとして記載しています。

1. (Optional) PC上で記事をプレビュー

    1. `brew install hugo` でHugo(静的サイトジェネレータ)をインストールします。
    2. `hugo server` でWebサーバを起動し <http://localhost:1313/> でブログをプレビューできます。
    3. Hugoのインストール以外でも、下記Dockerコマンドでも同様にプレビューできます。  
       `docker build -t engineers-blog/hugo .; docker run -v $(pwd):/usr/share/blog -p 1313:1313 -it engineers-blog/hugo`

実際、今この記事を書いている自分も hugo server を実行し確認しながら書いています。 他ページや画像へのリンクを書いている時点で確認できるため、リンク切れやレイアウト崩れに早めに気が付くことができます。

なお、同様の運用をWordPressで実現する場合、プレビュー作成のためアカウントを執筆者用に準備する必要があります。 これは権限管理のための人の作業が発生することになります。

PullRequestでレビュー: Netlifyがプレビュー生成してくれる

PullRequestが作成・更新されると、それをトリガーとしてビルドが走り、下図のように deploy/netlify がプレビューを作成してくれます。 これが本当に便利で、このPRをマージしたらどのように本番ブログに反映されるかを予め確認できます。 また、デザイナーさんとブログのデザインを変更する際にも、作成中のデザインを実際のWeb画面で確認できるためコミュニケーションしやすくなります。

deploy/netlify — Deploy preview ready!

なお、PullRequestでのプレビュー生成を行うためには、リポジトリのルートディレクトリに以下のようにNetlify用のファイルを定義しておく必要があります。
参考: hugo/netlify.toml · gohugoio/hugo

[build]
publish = "public"
command = "hugo"

[context.production.environment]
HUGO_VERSION = "0.46"
HUGO_ENV = "production"
HUGO_ENABLEGITINFO = "true"

[context.split1]
command = "hugo --enableGitInfo"

[context.split1.environment]
HUGO_VERSION = "0.46"
HUGO_ENV = "production"

[context.deploy-preview]
command = "hugo --buildFuture -b $DEPLOY_PRIME_URL"

[context.deploy-preview.environment]
HUGO_VERSION = "0.46"

[context.branch-deploy]
command = "hugo -b $DEPLOY_PRIME_URL"

[context.branch-deploy.environment]
HUGO_VERSION = "0.46"

[context.next.environment]
HUGO_ENABLEGITINFO = "true"

masterマージで自動的に公開

PullRequestでのレビューを完了し master へマージすると、これをトリガーとしてNetlifyによるビルドが走りブログに公開してくれます。 レビューが完了しない限りマージできませんし、レビューが完了していれば公開作業がマージボタン1つで完了します。 Netlifyに設定していれば、マージ後のビルド・デプロイ結果をSlack通知で受け取ることもできます。

NetlifyによるSlackへのデプロイ通知

感想

GitHubは毎日仕事でお世話になっていますが、HugoとNetlifyはエンジニアブログを作り始めるまで知りませんでした。

Hugoは多くの部分で自由度高くカスタマイズできるため、ブログ機能の作り込みの強い味方になりました。 Webサーバ機能によるプレビューは、ブログ機能を作り込む際にも大変役立ち、効率よく開発することができました。

Netlifyは本当に凄い。今回構築したブログの運用フローはNetlify無しにはありえません。 一定のトラフィックまでは無料で使用可能なため、エンジニアブログとは別に個人のブログもNetlifyに引越ししました。

今後は、このブログの記事をどしどし増やして、自分自身も本記事の執筆を契機にアウトプット力を上げ、会社と自分自身のエンジニアとしてのブランディングにつなげて行きたいと思います。

Appendix: Hugoによるブログ機能の作り込み実装

会社に所属する多数のエンジニアでブログを書くため、執筆者が複数人という要件のためHugo用のテーマ側で作り込んだ部分について紹介します。

TaxonomyとしてAuthorを追加

Hugoでは、デフォルトでカテゴリとタグの2つの分類(Taxonomy)を持っていますが、 config.toml への設定により執筆者(Author)を追加しました。

[taxonomies]
  category = "categories"
  tag = "tags"
  author = "authors"

これにより、記事のFrontMatterにauthorsとして以下のようにIDを指定することで、執筆者一覧ページ、執筆者個別ページが作成されます。

authors: ["hashigo"] # 執筆者のID(URLに使用可能な文字のみ) 1名のみ設定

Hugo上の扱いでは、 [taxonomies] に登録されているものは全てリスト形式となっています。 このため、追加した執筆者もリスト形式となっているため複数入力できてしまいます。 現状では1記事の執筆者は1名と定めているため、リストの先頭要素のみを表示するようにしています。

執筆者プロフィールを別ファイルで管理

Markdownとは別ファイルで執筆者プロフィールを管理し、記事の末尾に埋め込む仕様としました。

authors: ["hashigo"] # 執筆者のID(URLに使用可能な文字のみ) 1名のみ設定

FrontMatterで上記のように執筆者を設定している場合、 data/authors/hashigo.toml にTOML形式のデータファイルを記述します。 TOMLには以下のような内容を執筆者に書いてもらいます。

name     = "本名 or ニックネーム"
twitter  = "twitter-id" # Optional
facebook = "facebook.id" # Optional
github   = "github-id" # Optional
bio      = "自己紹介文"

Hugo用のテーマでは以下のようなコードを実装しています。

{{ range $key, $value := first 1 $.Params.authors }}
    {{/* 執筆者プロフィール取得 */}}
    {{ $profile := index $.Site.Data.authors . }}
    {{ if not $profile }}
        {{ errorf "執筆者情報がありません: %s" . }}
    {{ end }}

    ...執筆者プロフィール埋め込み...
{{ end }}

first 1 $.Params.authors でauthorsの先頭キー hashigo を取得。 取得した値が . で参照されるようになり、 index $.Site.Data.authors . でデータファイルを読み込む。 「…執筆者プロフィール埋め込み…」部分では、次の指定で各プロフィール項目を埋め込み可能になります。

  • 名前: {{ $profile.name }}
  • FacebookID: {{ $profile.facebook }}
  • TwitterID: {{ $profile.twitter }}
  • GitHubID: {{ $profile.github }}
  • 紹介文: {{ $profile.bio }}

執筆者画像を別ファイルで管理

執筆者プロフィールと同様に、執筆者画像も記事の末尾に埋め込む仕様としました。

ブログデザインの変更で執筆者画像のサイズが変更になる可能性があるため、少し大きめのサイズで集めたい。 しかし、現時点のブログデザインでは80x80のサイズ指定のため、Hugoでリサイズして埋め込みたい。 Hugoにこれに対応するImage Processing機能はあるのですが、対象となる”Image”はPage Resourcesに限られており、これは各記事と同じディレクトリに置かれたファイルのみが対象でした。 これでは、記事とは別に配置した執筆者画像をリサイズできません。 この制限を回避して執筆者画像を”Image Processing”で処理できる形で取得する必要があります。

事前準備として、 content/authors/index.md に空ファイルを準備します。 執筆者画像はこの空ファイルと同じディレクトリに content/authors/*.(jpg|png|gif) のパスで作成します。 作成した空ファイルはHugoからは記事として扱われますので、同じディレクトリの執筆者画像は、この空ファイル記事の”Page Resources”として扱えるため、”Image Processing”で処理できる形で取得できます。

ちなみに、この空ファイル記事はURLがTaxonomyに追加したauthorsとURLが同一のため、記事一覧には表示されません。

Hugo用のテーマでは以下のようなコードを実装しています。

{{ range $key, $value := first 1 $.Params.authors }}
    {{/* 執筆者画像取得 */}}
    {{ $originalImage := printf "%s.*" . | ($.Site.GetPage "page" "authors").Resources.GetMatch }}
    {{ if not $originalImage }}
        {{ errorf "執筆者画像がありません: %s" . }}
    {{ end }}

    {{/* 執筆者画像リサイズ */}}
    {{ $image := $originalImage.Fit "80x80 Lanczos q90" }}

    <img src="{{ $image.Permalink }}" alt="{{ $profile.name }}" width="{{ $image.Width }}" height="{{ $image.Height }}">
{{ end }}

authorsの先頭キーを hashigo とすると、 printf "%s.*" . | ($.Site.GetPage "page" "authors").Resources.GetMatch にて空ファイル記事と同一ディレクトリの hashigo.* を取得します。

HugoのImage Processingでは、画像に対してリサイズや切り取りが可能です。 $originalImage.Fit "80x80 Lanczos q90" では、Fitにより画像を正方形に切り取り、Lanczos関数で80x80にリサイズし、画質90で保存しています。 “Image Processing”で変換した画像を <img src="{{ $image.Permalink }}" alt="{{ $profile.name }}" width="{{ $image.Width }}" height="{{ $image.Height }}"> にて埋め込むことで、変換後の画像を表示できます。