odiak.net

動的なOGPが欲しいけどSSRするほどでもない場合にやったこと

SPAのリンクをTwitterやFacebookでシェアした際に、OGPによるプレビューを表示させたいが、
SSRするほどでもない場合のやり方を考えてみた。

前提

  • 完全に静的なHTML, JavaScript, CSSだけで構成されたシングルページアプリケーション。
    • Next.jsのSSRなどを使っていない。
  • それらの静的なファイルはNginxで配信されている。

Nginxの設定はこんな感じ。(リクエストに該当するファイルが存在しない場合は/index.htmlを返す)

server {
    server_name example.com;

    root /path-to-static-files;

    if (!-f $request_filename) {
        rewrite (.*) /index.html;
    }
}

やり方

1. HTMLファイルにOGPのタグを付けて返すアプリケーションをNode.jsとExpressで作る

次のコードのような感じ。

元のアプリケーションのパスに/ogpというプレフィックスをつけたものでリクエストを受けると、
HTMLにそのページのOGPタグを付けて返す。
(プレフィックスはなんでもよい。付ける理由は後述する。)

HTMLは、どこかのファイルから読んでくる。
ここではシンプルに決まったファイルを読んでいるが、疎結合にするためにパスをヘッダーで指定したり、ネットワーク経由で読み込んでも良いと思う。

OGPタグを付ける処理は、HTMLの</head><meta ... />...</head>で置き換えるという雑な実装。

import express from 'express'
import { createServer } from 'http'
import { readFile } from 'fs/promises'

const app = express()

async function getPost(slug: string): Promise<{title: string}> { /* ... */ }

// OGP for post pages
app.get('/ogp/posts/:slug', async (req, res) => {
    // OGP
    const slug = req.params.slug
    const html = await readFile('/path-to-static-files/index.html', 'utf-8')
    const post = await getPost(slug)
    res.send(html.replace('</head>', `
        <meta property="og:title" content="${post.title}"/>
        <meta property="og:type" content="article"/>
        <meta property="og:image" content="https://example.com/image.png"/>
        <meta property="og:url" content="https://example.com/posts/${slug}"/>
        <meta property="og:site_name" content="My Website"/>
        <meta name="twitter:card" content="summary"
    </head>`))
})

// OGP for category pages
app.get('/ogp/categories/:page', async (req, res) => {    
    // ...
})

// default OGP
app.use('/ogp/:anything*', async (req, res) => {
    // ...
})

createServer(app).listen(8000)

2. Nginxの設定を変更する

こんな感じ。
/foo/barでリクエストが来たときに、/foo/barというファイルが存在しなければ/ogp/foo/barでlocalhost:8000にプロキシしたものを返す。

server {
    server_name example.com;

    root /path-to-static-files;

    if (!-f $request_filename) {
        rewrite (.*) /ogp$1;
    }

    location /ogp {
        proxy_pass http://localhost:8000;
    }
}

これでリクエストのパスに応じてOGPのタグがついたHTMLが返ってくるようになった。万歳!


まあこのままでも問題はないが、もう一工夫してみる。

OGPの生成に多少の時間がかかる場合、このやり方では読み込みの時間が長くなり、アプリケーションのパフォーマンスに影響してしまう。
アプリケーションのユーザーにとってはOGPタグは必要ないわけだし、省略できる時は省略したい。

そこで考えたのが、クッキーを利用する方法。
フロントエンドであるクッキーに値を設定し、そのクッキーが設定されている場合は直接静的ファイルを返すのである。

3. フロントエンドでクッキーを設定する

アプリケーションのどこかに次のようなコードを入れておく。

document.cookie = `no_ogp=1; max-age=${60 * 60 * 24 * 365}`

4. Nginxの設定を再度変更する

次のように、location /ogp の下に、if文を追加する。
no_ogpというクッキーに空ではない値が設定されていれば、OGPタグを付ける処理をスキップして直接HTMLファイルを返す。
(rewriteの最後にlastを付けないと、rewriteしたものがproxy_passに渡ってしまう。)

server {
    server_name example.com;

    root /path-to-static-files;

    if (!-f $request_filename) {
        rewrite (.*) /ogp$1;
    }

    location /ogp {
        if ($cookie_no_ogp) {
            rewrite (.*) /index.html last;
        }
        proxy_pass http://localhost:8000;
    }
}

はい、これで初回のリクエスト以外はOGPタグを付ける処理をスキップすることができ、アプリケーションのパフォーマンスにほぼ影響がなくなりました。
めでたしめでたし。

まとめ

  • SPAにOGPタグをつけるためにNext.jsなどを使うのはかったるいので、専用のミニアプリケーションを作ってそちらでタグを付ける処理をしてみた。
  • クッキーを使ってタグを付けるかどうかを制御すると、アプリケーションのパフォーマンスにも影響しない。

お読みいただきありがとうございました。
こちらは、Web上のホワイトボードアプリ: Kakeruを開発中に思いついたアイデアで、実際にアプリケーションで使用しています。
よかったらKakeruの方も使ってみてください。