Hyperappでhead内のタグを書き換えるライブラリ、Hyperapp Helmetをリリースしました！

# はじめに

[Hyperapp Helmet](https://github.com/babie/hyperapp-helmet)という、[Hyperapp](https://github.com/jorgebucaran/hyperapp)フレームワーク用のライブラリをリリースしました。

Helmetとは、[React Helmet](https://github.com/nfl/react-helmet)が有名/はしりですが、&lt;head&gt;タグの中を書き換えるコンポーネントのことを言います。

昨今の検索エンジンbotはJavaScriptを解釈しますので、SSRはOGP/Twitter Card対応のためにあると言っても過言ではないと思います。その時に、ページ内容に合わせた&lt;head&gt;内タグの書き換えが必要になりますが、Hyperappにはそれに対応するライブラリがありませんでした。

# 特長

Hyperapp Helmetは以下の特長を持ちます。

- [@hyperapp/render](https://github.com/kriasoft/hyperapp-render)対応。`renderToString()`だけじゃなく、`renderToStream()`でSSRする時も使えます。
- [@hyperapp/router](https://github.com/hyperapp/router)対応。[JSDOM](https://github.com/jsdom/jsdom)と併用することにより、サーバサイドでも、ルーティングに合った&lt;head&gt;内タグの書き換えができます。
- ネスト対応。親・子・孫……コンポーネントそれぞれで定義した&lt;Helmet&gt;が全て&lt;head&gt;内タグに反映されます。
- TypeScript対応。

# インストール

```shell-session
$ npm install hyperapp hyperapp-helmet
or
$ yarn add hyperapp hyperapp-helmet
```

# 使い方

公開メソッドは2つしかないので、README.md読めばわかると思いますが、軽く紹介しておきます。

## &lt;Helmet&gt;

任意のコンポーネントで使える。&lt;Helmet&gt;タグで囲まれた部分が&lt;head&gt;タグの中に挿入される。`key`アトリビュートが必須。

```tsx
import { h } from 'hyperapp'
import { Link } from '@hyperapp/router'
import { App as A } from '../App'
import { Helmet } from 'hyperapp-helmet'

export const Counter = (): any => (state: A.State, actions: A.Actions) => (
  <div key="counter">
    <Helmet key="counter-helmet">
      <title>Counter: {state.count}</title>
      <meta name="description" content="Number Counting Page" />
    </Helmet>
    <h1>{state.count}</h1>
    <button onclick={() => actions.down(1)}>-</button>
    <button onclick={() => actions.up(1)}>+</button>
    <div>
      <Link to="/about">About</Link>
    </div>
  </div>
)
```

## getHelmetNodes(view, state, actions) => VNode[]

アプリケーションのビューを探索し、該当の&lt;head&gt;内タグのVNodeの配列を返す。

```tsx
import { h, View } from 'hyperapp'
import { App as A } from './App'
import { getHelmetNodes } from 'hyperapp-helmet'

const Fragment = ''
export const Html: View<A.State, A.Actions> = (
  state: A.State,
  actions: A.Actions
) => {
  const helmetNodes = getHelmetNodes(A.view, state, actions)
  return (
    <Fragment>
      <Fragment innerHTML="<!doctype html>" />
      <html lang="ja">
        <head>
          <meta charset="utf-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1" />
          <script src="/index.js" defer />
          {helmetNodes}
        </head>
        <body>
          <div id="app">{A.view}</div>
        </body>
      </html>
    </Fragment>
  )
}
```

こんだけ！　[サンプルプロジェクトを公開してある](https://github.com/babie/hyperapp-typescript-sample)ので、動かして見てください。

# 開発

せっかくなので、小話をば。

## 経緯

- 小さくて速いウェブアプリが作りたいな〜
- Reactはエコシステムが大きいから開発が早いけど、サイズがデカいしな〜
- おっしゃ、Preact挑戦してみたろ
- この`preact-compat`っての入れるとReact用のライブラリが使えるが、サイズがデカくなるぞ？　う〜ん……
- そういえばHyperappってのがあったな？
- `@hyperapp/router`でルーティングもできるし、`@hyperapp/render`ってやつでSSRもできる。全部小さい！　ええやんけ！
- （素振りリポジトリを作ってみる）
- えっ、HyperappにHelmetないの？　マ？　今時SSRはOGP/Twitter Card対応だから意味ないやんけ！　う〜ん……
- せっかくだし、作ってみるか！

## 実装

そんな難しいことはしてないのですが、迷いながら実装したところがあるので、説明させてください。

具体的には２点あり、
1. &lt;Helmet&gt;コンポーネントが&lt;template&gt;タグを返し、それがレンダリングされる。
2. &lt;head&gt;内タグの更新のとき、一旦全削除して、全追加している。

まず、1についてですが、Reactコンポーネントと違って、Hyperappのライフサイクルメソッドは外挿なんですよね。なので、なんらかのノードを返す必要がありました。

最初は`<div style={{ display: 'none' }}>{children}</div>`を返していたのですが、&lt;div&gt;の中に&lt;title&gt;とは入ってるのかっこ悪い……となって一度は`children`を書き出さず、空の&lt;div&gt;だけ返してました。しかし、SSRをするに当たってノードを探索・解釈してみたんですが、実際にはマウントされないため、`oncreate`ライフサイクルメソッドが実行されず、タグを格納するグローバル変数に入らないという問題が発生しました。

というわけで、また`children`を書き出す実装に戻したんですが、上記の通り気味が悪いので、実際には表示されないエレメントは何かないかと探したところ、&lt;template&gt;が見つかりました。これなら&lt;title&gt;等があっても違和感が少ないと考え、これを使うという選択をしました。

現在の実装ではテンプレートとしては使ってないのですが、将来実際にテンプレートとして使って、&lt;head&gt;に流し込むというのもありうるのではないかなと思います。

次に、2についてですが、最初は実際に書き出したエレメントと架空のノードとの差分をとってパッチを当てる実装にしてたんですが、これだと、props/state変数をタグに使用したとき、同定が難しいという問題が発生しました。

ユーザーに逐一`key`を設定してもらえば同定できるのですが、それは手間だろうということで、現在のように、&lt;Helmet&gt;毎に全削除＆全追加という実装になってます。


# おわりに

このライブラリを使うことにより、HyperappでSSRするに当たって、最後の障害がなくなったと思います。ぜひ、使ってやってください。

このライブラリを作るにあたり、@r7kamura 氏の[katatema](https://github.com/r7kamura/katatema)を参考にしました。氏にはTwitterで助言を頂いたりして大変お世話になりました。ここにお礼を申し述べます。

よし、これで次はアプリに取りかかれるぞ〜