Github ActionsとIssuesでブログを書く

Github Issuesをブログとして使うアイデアは太古の昔からある。ここで言っているのはIssueを使ってブログ記事を書き、タグをつけ、フィルタや検索して管理しながら、そこからHTMLを生成するようなやつのことだ。ちょっと検索しただけでもいくつかの実装を見かける。

* [mateogianolio](https://github.com/mateogianolio)/**[issuance](https://github.com/mateogianolio/issuance)**
* [casualjavascript](https://github.com/casualjavascript)/**[blog](https://github.com/casualjavascript/blog)**
* [bufgix](https://github.com/bufgix)/**[github-blog](https://github.com/bufgix/github-blog)**

また、実装を解説したブログ[Simple GitHub Issues Powered Blog](https://egghead.io/blog/github-issues-powered-blog)もある。いずれも基本的にはGithub Issuesをバックエンドとして使い、フロントのJavaScriptがIssuesのAPIから情報をとってHTMLを生成している。例えば[これ](https://casualjavascript.com/)なんかはシンプルでいい感じ。


# Github Actionsを使う

今回書くのは、これのGithub Actions版だ。似たようなSPA版実装がたくさんあるからGithub Actions版を作ってバランスを取ることにする。Issuesの作成・更新をトリガーにGithub ActionsがサーバーサイドでHTMLをレンダリングしてWeb1.0な感じのサイトをつくってみる。Github Pagesでホストするので全てGithub内で完結することになる。



# 仕組み

Issueの作成からサイトのビルドまでの流れは以下のようになる[^2]。ビルドに関してはGithub Actionsがさまざまなコンテキスト（例えばIssue Id、Issue Title、Account Nameなど）を持っているのでそれをHTMLへ埋め込むだけの処理になる。こういう処理にはテンプレートエンジンが便利だ。今回は懐かしの[Mustache](https://mustache.github.io/)を使った。またIssuesに書き込まれた本文のMarkdownをHTMLに変換する部分は[GithubにAPI](https://docs.github.com/ja/rest/markdown)がある。変換のライブラリを探してきてActions内で動かすつもりでいたのでこれは助かる[^1]。

[^1]: いろいろと探して評価していたが必要なかった
[^2]: Github Pagesにホストするのであれば、マークダウンファイルを直接pushして、Jekyllでビルドする方法もある。Jekyllのやり方に合わせればこっちの方が簡単かもしれない。別途やってみたい。



```mermaid
sequenceDiagram
    actor User
    participant Github Pages
    participant Github Actions
    actor Author
    participant Github API
    Author->>Github Actions: Create issues
    Github Actions->>Github API: POST /markdown <body>
    Github API->>Github Actions: HTML
    loop Mustache
    	Github Actions->>Github Actions: Build index.html with ${context}
    	Github Actions->>Github Actions: Build post.html with ${context}
    end
    Github Actions->>Github Pages: git push index.html <post_id>.html
    User->>Github Pages: Read
```

# Github Actions

Github Actionsの中身はこんな感じになる。このYAMLはあまり処理をしていなくて実態は後述するJavascriptのコード `build-pages.js` である。YAMLは環境変数をjsに渡すのとGithub Pagesへのpushを行なっているだけ。

```yaml:build-pages.yaml
name: Build Pages
on: 
  issues:
    types: [opened, edited]

env:
  owner: ${{github.repository_owner}}
  repo: ${{github.event.repository.name}}
  target_issue_id: ${{github.event.issue.id}}

jobs:
  build-pages:
    runs-on: ubuntu-latest
    steps:
      - name: View the github context
        run: echo "$GITHUB_CONTEXT"
        env:
          GITHUB_CONTEXT: ${{ toJson(github) }}
          
      - name: View github-script context
        uses: actions/github-script@v6
        with:
          script: console.log(context)

      - name: Checkout blog repo
        uses: actions/checkout@v3

      - name: Install Octokit and Mustache
        run: npm install @octokit/rest mustache
      
      - name: Env test
        uses: actions/github-script@v6
        with:
          script: |
            console.log(process.env)
            
      - name: Build pages
        run: node src/build-pages.js

      - name: Git setting
        run: |
          git config --local user.email "your_email_address"
          git config --local user.name "${{github.actor}}"
          
      - name: Commit and push pages
        run: |
          git add index.html posts/${{github.event.issue.id}}.html
          git commit -m "update" -a
          git pull
          git push origin main
```


```js:build-pages.js
const { Octokit } = require("@octokit/rest");
const octokit = new Octokit({});
const Mustache = require('mustache');
const fs = require("fs");
const df = require("./date-format.js")


// Build
octokit.rest.issues.listForRepo({
	owner: process.env.owner,
	repo: process.env.repo,
})
.then(issues => {
	issues.owner = process.env.owner
	issues.repo = process.env.repo

	// Format issue object
	issues.data.map(issue => issue.updated_at_short = df.shortDate(issue.updated_at))
	console.log("issues: ", issues)

	// Build index
	const index_template = fs.readFileSync("template/index.template.html", "utf8").toString();
	const index_html = Mustache.render(index_template, issues)
	fs.writeFileSync("index.html", index_html, "utf8");

	// Build post
	target_issue = issues.data.filter((ti) => {
		return ti.id == process.env.target_issue_id
	});
	target_issue = target_issue[0]

	markdown = target_issue.body
	const issue_template = fs.readFileSync("template/post.template.html", "utf8").toString();
    octokit.rest.markdown.render({"text": markdown, "mode": "gfm"})
	.then(issue_html => {
		target_issue.issue_html = issue_html
		console.log("target_issue: ", target_issue)

        const issue_page = Mustache.render(issue_template, target_issue)
	    fs.writeFileSync("posts/" + process.env.target_issue_id + ".html", issue_page, "utf8");
	})
});
```

`build-pages.js` はHTMLのテンプレートを読み込んでそこにGithub Actionsのcontextの変数をMustacheを使って埋め込み、`index.html`と`<issue_id>.html` を作るだけのシンプルな処理。ローカルでデバッグする際には以下のように環境変数を渡して実施していた。

```sh
export owner=your_account
export repo=blog
export target_issue_id=1198469729
```


# Mustache

あとは好きな`index.html`と`<post_id>.html`のテンプレートファイルを用意してMustacheを使って`<h1>{{title}}</h1>`という感じで好きなHTMLに変数を埋め込むだけだ。APIから帰ってくるマークダウンから変換したHTMLをそのままHTML内に埋め込みたい場合は`{{{html}}}`と髭を3つにしてやればよい。


# まとめ

またもやGithub Issueからブログをつくるやつを増やしてしまった。今回のはサーバー側で静的サイトを作るのでこれまでのSPAなサイトと違って、軽快でシンプルなサイトを作るのに向くと思う。また静的サイトジェネレータと比べてもビルドが自動化されていているので、その点でも楽ができるのではないだろうか。次は誰が書く？

あ、あと偶然だがこんなキャンペーンやってたのでエントリーしておいた - [GitHub Actionsの自分流の使い方をシェアしよう](https://qiita.com/official-campaigns/engineer-festa/2022)