Reactをまともに使ったことないエンジニアがBun+HonoでSSGを試してみた

Bun+Honoは今日初めて触ったので備忘録を残します。
Reactはほとんど触ってないのでちょっとしたことに躓いて牛歩状態です。
この記事はチュートリアルなコードと、参照したドキュメントへのリンクを含めて後で辿れるように書きました。

## 試した環境

- MacOS Sonoma 14.3
- Bun 1.0.6
- Hono 4.0.4

## Bun インストール

コマンドラインからインストールしました。
`sudo`は使わないのでたぶん怖くない筈です。
```
$ curl -fsSL https://bun.sh/install | bash
```
削除したい時はホームフォルダの`.bun`を削除すれば良いそうです。
```
$ rm -rf ~/.bun
```

https://bun.sh/docs/installation

## Project作成
https://hono.dev/getting-started/basic

ドキュメントに習ってHonoのプロジェクトを作成
```bash
$ bunx create-hono my-app
```

何も知らずにコマンドを叩くと**12からあるTemplate**を選べと言われてドン引きしました。
今回は`bun`を選択。
```bash
$ bunx create-hono my-app

create-hono version 0.4.0
✔ Using target directory … my-app
✔ Which template do you want to use? › bun
cloned honojs/starter#main to /Users/o2/work/tmp/my-app
✔ Copied project files
```

VSCodeでプロジェクトを開くとあちこちアンダーラインが引かれます。
最初にライブラリ導入しないとね。
無事`Hello Hono!`と表示できました。👏
```bash
$ code my-app
$ cd my-app
$ bun i
$ bun run dev

$ bun run --hot src/index.ts
Started server http://localhost:3000
```

![スクリーンショット 2024-02-21 0.48.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35973/530d4a4a-62a0-d367-41aa-48987d909add.png)

## SSGを試す

https://hono.dev/helpers/ssg

ドキュメントにあるようにSSGを設定。
`import app from "./src/index";`と設定。
(なお、この書き方だとindex.\*, index/\* と様々なファイルを参照するそうです。)

```build.ts
import { toSSG } from "hono/bun";
import app from "./src/index";

toSSG(app)
```

実行するとstaticフォルダにファイルが出力できました。
```bash
$ bun build.ts
```

```bash
$ tree --gitignore 
.
├── README.md
├── build.ts
├── bun.lockb
├── package.json
├── src
│   └── index.ts
├── static
│   └── index.txt
└── tsconfig.json

3 directories, 7 files
```

## ssgParamsで複数のページを出力する

同ページのドキュメントの`ssgParams`を試していきます。
ここでは静的ページの作成にはコードに直書きのコンテンツデータを利用します。

最初にjsxを利用しますので現在の`index.ts`のファイルの拡張子を変更します。

```bash
$ mv index.ts index.tsx
```

https://hono.dev/guides/jsx

```index.tsx
import { Hono } from 'hono'
import { ssgParams } from 'hono/ssg'

const app = new Hono()

const ShopsData = [
  { id: "1", name: "hoge",}, 
  { id: "2", name: "fuga" }
]

function getShop(id: string){
  return ShopsData.filter(shop => id == shop.id)
}

app.get(
  '/shops/:id',
  ssgParams(async () => {
    return ShopsData.map((shop) => ({ id: shop.id }))
  }),
  async (c) => {
    const shops = await getShop(c.req.param('id'))
    if (shops.length < 1) {
      return c.notFound()
    }
    const shop = shops[0]
    return c.render(
      <div>
        <h1>{shop.name}</h1>
      </div>
    )
  }
)

export default app
```

`package.json`の`.ts`を`.tsx`に修正して実行
```package.json
{
  "scripts": {
    "dev": "bun run --hot src/index.ts"
    ↓
    "dev": "bun run --hot src/index.tsx"
  },
...
```

```
$ bun run dev
```
`localhost:3000/shops/2`にアクセス
`shops/[id]`にIDを指定してページにアクセスできることが確認できます。

![スクリーンショット 2024-02-21 1.04.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35973/b0430723-e927-9002-8123-af3eef16f686.png)

再度SSGを出力しなおします。
```
$ bun build.ts 
$ tree  --gitignore
.
├── README.md
├── build.ts
├── bun.lockb
├── package.json
├── src
│   └── index.tsx
├── static
│   ├── index.txt
│   └── shops
│       ├── 1.html
│       └── 2.html
└── tsconfig.json
```

staticフォルダに設定した`[id].html`のファイルが出力できていることが確認できました。
上記の出力まで見れば何を行う処理なのか解りやすいですね。

```typescript

const ShopsData = [
  { id: "1", name: "hoge",}, 
  { id: "2", name: "fuga" }
]

app.get(
  '/shops/:id',
  ssgParams(async () => {
    return ShopsData.map((shop) => ({ id: shop.id }))
  }),
```
ここでの肝ですが、

- `/shops/:id`では`app.get`にパラメータ`id`を取得するように指定
- `ssgParams`に生成するページのIDを`ShopsData`を展開して与えています

これによりstaticフォルダに出力するファイルを制御できます。


## 静的ファイルへのリンクに対応する

```index.tsx
import { Hono } from 'hono'
import { ssgParams } from 'hono/ssg'

const app = new Hono()

const ShopsData = [
  { id: "1", name: "hoge",}, 
  { id: "2", name: "fuga" }
]

function getShop(id: string){
  return ShopsData.filter(shop => id == shop.id)
}

app.get(
  '/shops/:id',
  ssgParams(async () => {
    return ShopsData.map((ShopsData) => ({ id: ShopsData.id }))
  }),
  async (c) => {
    const shops = getShop(c.req.param('id'))
    if (shops.length < 1) {
      return c.notFound()
    }
    const shop = shops[0]

    const contents = (
      <ul>
        { ShopsData.map( shop => (<li><a href={
          Bun.env.NODE_ENV === "development" ? `/shops/${shop.id}` : `/shops/${shop.id}.html`
        }>{shop.name}</a></li> )) }
      </ul>
    )
    
    return c.html(`
      <div>
        <h1>${shop.name}</h1>
      </div>
      ${contents}
    `)
  }
)

export default app
```

`package.json`に`build`コマンドを追記
```package.json
{
  "scripts": {
    "dev": "bun run --hot src/index.tsx",
    "build": "NODE_ENV='production' bun build.ts"
  },
...
```

ここではコマンドを`build.ts`ではなく`package.json`に追記した`build`コマンドを実行します。
この修正により出力するファイルと`bun run dev`で実行時と静的ページを閲覧してもリンクが機能します。
```
$ bun run build
$ bun run dev
```

![スクリーンショット 2024-02-21 1.17.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35973/82e1ae5c-2dc7-b951-a784-98865c85cf4b.png)

## これでSSGマスター

まだまだ機能は沢山ありますが基本のループとIF文が書けるようになりました。
Bun+Honoを試した感想としては、随分簡単に静的ファイルの出力処理が書けるのだなと思いました。
これでとっかかりが出来たのであとはドキュメントを読み進めるだけです。

初学者ですのでもっと良い方法等あれば是非コメントを残していただければ幸いです。