Open6

Cloudflare Workersで「headタグだけSSR」を実現する

きよしろーきよしろー

headタグだけSSRって何

headタグ部分だけサーバーサイドで生成し、body部分はCSRする
これによりサーバーの負荷を最小限にしながら動的OGPが実現できる

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
    <script type="module" crossorigin src="/assets/index.08ff9814.js"></script>
    <link rel="stylesheet" href="/assets/index.3fce1f81.css">

    <!-- ここをサーバーサイドで作る -->
    <meta name="description" content="{dynamic description}" />
    <meta property="og:title" content="{dynamic title}" />
    <meta property="og:description" content="{dynamic description}" />

  </head>
  <body>
    <div id="root"></div>
    
  </body>
</html>

他の動的OGPを実現する方法と比較

headだけSSR SSR Dynamic Rendering
サーバー負荷
SEO ○※

※(レスポンスが遅くなってCore Web Vitals的に悪くなる可能性あり)

headだけSSRの実現方法は色々あるがなんとなく、Cloudflareが簡単そうだと思った。

きよしろーきよしろー

フロント側は適当にviteでinitしておく

npm create vite@latest
# React + TypeScriptの構成を選択
きよしろーきよしろー

ディレクトリはこんなかんじ

head-ssr/ # Cloudflare Workersのプロジェクト
web/         # Webフロント

Cloudflare Workersの設定はこんなかんじ。

head-ssr/wrangler.toml
name = "header-ssr"
main = "src/index.ts"
compatibility_date = "2022-10-11"

[site]
bucket = "../web/dist" # ここで、Web側のビルド結果のディレクトリを指定する

公式ドキュメントを参考にすると、どうやら@cloudflare/kv-asset-handlerというパッケージが必要になるそうなのでインストール

npm i -D @cloudflare/kv-asset-handler

https://developers.cloudflare.com/workers/platform/sites/start-from-existing/

コードはこんなかんじ

head-ssr/src/index.ts
import {
  getAssetFromKV,
  serveSinglePageApp,
} from "@cloudflare/kv-asset-handler"
import manifestJSON from "__STATIC_CONTENT_MANIFEST"

/**
 * Welcome to Cloudflare Workers! This is your first worker.
 *
 * - Run `wrangler dev src/index.ts` in your terminal to start a development server
 * - Open a browser tab at http://localhost:8787/ to see your worker in action
 * - Run `wrangler publish src/index.ts --name my-worker` to publish your worker
 *
 * Learn more at https://developers.cloudflare.com/workers/
 */

export interface Env {
  // Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/
  // MY_KV_NAMESPACE: KVNamespace;
  //
  // Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/
  // MY_DURABLE_OBJECT: DurableObjectNamespace;
  //
  // Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/
  // MY_BUCKET: R2Bucket;
  __STATIC_CONTENT: KVNamespace
}

const assetFileExtensions = [
  ".css",
  ".js",
  ".png",
  ".jpg",
  ".jpeg",
  ".svg",
  ".gif",
]

const isAssetFileRequest = (request: Request): boolean =>
  assetFileExtensions.some((ext) => request.url.endsWith(ext))

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext
  ): Promise<Response> {
    const asset = await getAssetFromKV(
      {
        request,
        waitUntil(promise) {
          return ctx.waitUntil(promise)
        },
      },
      {
        // ここのASSET_NAMESPACEやASSET_MANIFESTの指定を忘れないこと(2敗)
        ASSET_NAMESPACE: env.__STATIC_CONTENT,
        ASSET_MANIFEST: JSON.parse(manifestJSON),
        mapRequestToAsset: serveSinglePageApp,
      }
    )
    if (isAssetFileRequest(request)) return asset

    // e.g. https://example.workers.dev/?id=1
    const id = new URL(request.url).searchParams.get('id')
    if (id === undefined) return asset

    const html = await asset.text()
    const ogp = generateOgpMetaTags(id)

    // ここでogp関係のメタタグを注入する
    return new Response(html.replace("</head>", `${ogp}</head>`), {
      headers: {
        "content-type": "text/html;charset=UTF-8",
      },
    })
  },
}

function generateOgpMetaTags(id: string): string {
  const description = `記事:${id}の説明`
  return `
    <meta name="description" content="${description}" />
    <meta property="og:title" content="記事:${id}のタイトル" />
    <meta property="og:description" content="${description}" />
  `
}

きよしろーきよしろー

とりあえずローカルで確認

cd web/
npm run build # フロント側を忘れずにビルドしておく
cd ../head-ssr/
wrangler dev # http://0.0.0.0:8787にアクセス

devツールのネットワークを見てみるとうまくいってるっぽい 🎉