Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

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

自分が作っているg4でもOGPを使ったシェアを実装していますが、SSRを使って配信していて、割と設定が面倒だったりしています。

こんなの
image.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}}みたいなやつをここに書いて、テキスト置換でそこに内容を埋め込んでます。

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です。こちらにすべての処理を書きます。

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などでシェアしてみてください。

以上になります。ソースはこちらにあります。

shwld
主に開発で得られた知見を記録していきます。 記載された内容は、所属する企業や団体の公式見解ではありません。
https://shwld.net
mof-mof
「つくって人をしあわせにする」をビジョンにAIチャットボットや開発チームレンタル等のサービスを提供している渋谷のIT企業です
https://www.mof-mof.co.jp/about
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away