fly.ioでSPA&OGP芸を最速かつゼロ依存でやる（package.jsonすら不要）

個人開発界隈ではOGP芸というものが流行っているみたいですね。

SNSにシェアする際に表示されるOGPの設定や画像をいい感じにするみたいなことのことですが、SPAではこれを生成するのが割と面倒だったりします。


自分が作っている[g4](https://www.g-g-g-g.games)でもOGPを使ったシェアを実装していますが、SSRを使って配信していて、割と設定が面倒だったりしています。

こんなの
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51402/c5deac6a-36cd-6a14-dd40-193060f6572a.png)

今回、なるべくSSRをしないでSPAのままやってみることはできないかというのをfly.ioで試してみました。

# 作ったものの概要

- 実際にOGP画像として表示される画像は`/image.png?i=0`で配信する。
- `/`に`?i=0`のようなパラメータをアクセスすると、入力した↑を含むOGPが反映されたhtmlを表示する
- fly.ioが日本語に対応してないので、titleに日本語は使えない
- せっかくfly.ioを使ってますが、今回はOGPだけにフォーカスしたいのでキャッシュ周りは実装してません

# 作ったもの
https://og-image-sample.edgeapp.net?i=0

or

https://og-image-sample.edgeapp.net?i=1

これをtwitterやfacebookなどでシェアしてみてください。

# 作り方

事前にflyのcliをPCにインストールしておく
npmは必要ないです（あったほうが便利だけど最速なので）

必要なファイルは4つだけ
- index.js: なんか処理書くやつ
- template.html: テンプレートです。ここに情報を流し込む
- fly.yml: なんか設定書くやつ
- sample.svg: OGP芸したい適当なsvg。今回はURLによって内容を書き換えるため雑に{{title}}みたいなやつをここに書いて、テキスト置換でそこに内容を埋め込んでます。

``` html:template.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <meta name="theme-color" content="#000000" />
    <title>fly.ioで最速ogp芸</title>
    <meta name="description" content="by shwld" />
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:creator" content="@shwld" />
    <meta property="og:type" content="website" />
    <meta property="og:url" content="https://www.g-g-g-g.games" />
    <meta property="og:title" content="shwld" />
    <meta property="og:description" content="shwld" />
    <meta property="og:image" content="https://www.g-g-g-g.games/assets/image.png" />
    <meta property="og:image:alt" content="shwld" />
  </head>
  <body>
    <h1>fly.ioでogp芸をする場合のサンプルだよ</h1>
  </body>
</html>
```

templateはこんな感じになりました。
ogpのcontentは置き換えるので何でもいいです。とりあえずさり気なくg4のものを入れてアピールしておきます。

次に`index.js`です。こちらにすべての処理を書きます。

``` js:index.js
import { Image } from '@fly/image'
import { mount } from "@fly/fetch/mount"

// templateをテキストで取得するFunction
async function getTemplate() {
  const resp = await fetch("file://src/template.html")
  return await resp.text()
}

// svgをテキストで取得しつつ中身ちょっと埋め込めるFunction
async function getSvgText(title) {
  const resp = await fetch("file://src/sample.svg")
  const text = await resp.text()
  // g4(https://www.g-g-g-g.games)ではReactでテキスト化されたsvgを吐いてるが、とりあえず単純に置換する
  return text.replace('{{content}}', title)
}

// 画像形式のレスポンス作るFunction
async function responseImage(svgText) {
  const svgResp = new Response(Buffer.from(svgText))
  const buf = await svgResp.arrayBuffer()
  const png = new Image(buf).png()
  const result = await png.toBuffer()
  return new Response(result.data, {
    headers: {
      'Content-Type': 'image/png',
      'Content-Length': result.data.byteLength.toString(),
    }
  })
}

// 出力データのパターン
const TITLES = [
  'g4 is pomodoro rpg!',
  'fly.io de OGP!!',
]

// fly.ioのrouterみたいなやつ。ここに処理を書いてく。
const mounts = mount({

  // このパスでogpの画像を生成する
  '/image.png': async (req, init) => {
    const url = new URL(req.url)

    // URLからQueryStringを取得
    const index = url.searchParams.get('i')
    if (index !== '0' && index !== '1') {
      return new Response('not found', { status: 404 })
    }

    // 対応したタイトルを取得
    const title = TITLES[index]

    // タイトルをsvgに埋め込んだテキストを作る
    const svgText = await getSvgText(title)

    // svgからpng画像を生成する
    return responseImage(svgText)
  },

  // このパスをシェアする
  '/': async (req, init) => {
    const url = new URL(req.url)

    // URLからQueryStringを取得
    const index = url.searchParams.get('i')
    if (index !== '0' && index !== '1') {
      return new Response('not found', { status: 404 })
    }

    // 対応したタイトルを取得
    const title = TITLES[index]

    // テンプレートをparseして編集できるようにする
    const doc = Document.parse(await getTemplate())

    // テンプレートにOGPを埋め込む
    doc.querySelector('meta[name="description"]').setAttribute('content', title)
    doc.querySelector('meta[property="og:url"]').setAttribute('content', url.href)
    doc.querySelector('meta[property="og:title"]').setAttribute('content', title)
    doc.querySelector('meta[property="og:description"]').setAttribute('content', title)
    doc.querySelector('meta[property="og:image"]').setAttribute('content', `${url.origin}/image.png?i=${index}`)
    doc.querySelector('meta[property="og:image:alt"]').setAttribute('content', title)

    // htmlを返す
    return new Response(doc.documentElement.outerHTML, {
      headers: { 'Content-Type': 'text/html' },
      status: 200,
    })
  },
})

// リクエストをmountsの定義を使って処理するように設定する
fly.http.respondWith(mounts)
```

index.jsはこんな感じ。
`/` は`template.html` のogpを書き換えて出すだけ。
`/image` はsvgファイルを読み込んで文字列を置換したものをpngに変換して返してます。

fly.ymlやsvgファイルは特に特別な設定はないのでドキュメントや最後にソースを貼るのでそちらを見ていただければ。

あとは、fly.ioのコンソールでアプリを作って、`fly deploy` するだけ。

再度になりますが、

https://og-image-sample.edgeapp.net?i=0

or

https://og-image-sample.edgeapp.net?i=1

これをtwitterやfacebookなどでシェアしてみてください。

以上になります。ソースは[こちら](https://github.com/shwld/fly.io-og-image)にあります。