Yewで動的Metaタグやプリレンダー実装してみた
Section: Technology

Yewの現状

Yewは一般的なSPAと同様に動作します。そのような多くのフロントエンドフレームワークでは、SSRや動的にMetaタグを変更してくれるライブラリ等が実装されています。

しかし新参者のYewにはそのようなものはありません。そのため、自ら実装する必要がありました。

動的Metaタグの実装

まずはRouteが変化した際に、TitleやDescription等のheadタグ内の要素を変更するようにしました。

具体的には以下のコードのように、Document::query_selectorメソッドでmeta[name='?????']のような引数を与え、特定のメタタグを取得します。後は状況に応じて、そのメタタグのcontentアトリビュートを変更したり、生成・削除したりしています。

let document = yew::utils::document();

let selectors: String = format!("meta[{}='{}']", name, value);

if let Ok(Some(meta)) = document.query_selector(&selectors) {
    meta.set_attribute("content", content)?;
} else {
    document.create_meta(name, value, content)?;

    let meta = document.create_element("meta")?;
    meta.set_attribute(name, value)?;
    meta.set_attribute("content", content)?;

    document.head().unwrap().append_child(meta.as_ref())?;
}

OGPやMetaタグを読み込んでくれない

実際にデベロッパーツールでElementsを確認すると、ページが変わる際に変更されているのを確認できました。これでOGPやその他のメタタグが動的に変更されるようになりました。

OGP

Twitter等のSNSでは、自身で投稿した情報に含まれているURLの移行先のOGPを設定しておくと、URLの見栄えをよくしてくれるカードの機能などがあります。

以下の画像が実装して、期待する結果です。

しかし先程の実装方法では、カードなどが表示されません。どうやらサイト内のmetaタグ等の情報を取得する多くのボットでは、最初に読み込まれるHTMLファイルのheadタグからのみ取得するらしいです。

このような仕様により、後のjavaScriptファイル等による動的に生成・変更されたmetaタグは読み取ってくれません。

検索クローラー

Googleの検索クローラーでは、JavaScriptやWASM等を実行し、その情報も読み取ってくれるらしいです。このことがあったため、最悪OGPの機能が使えなくても、あまり問題ないと思っていました。

しかし、Googleコンソールで確認してみると、上手く読み込んでいないことが判明しました。ある記事では時間が経たないと、JavaScriptファイルを取得し切れていないとあったが、数日経っても変わりませんでした。

このままだと検索結果に表示されず、サイトが見られないので、流石に対処しなければ、ということになりました。

SSR

サーバー側でJavaScript等の実行を行い、実行後の結果をクライアント側に送るのが、SSRです。主なメリットとして、クライアント側での初期実行の処理時間が減少し、サイトを開いてから表示される時間が減少します。

SSRにより、metaタグもクライアントに送られた段階で生成・変更されるので、どのクローラーもmeta情報を取得することができます。

YewでもSSRを実装する試みがあるのですが、あまり活発ではなく、現状ではありません。

Prerendering

SSRと似たように、サーバー側で事前にJavaScript等の実行を行います。違いとしては、Prerenderingの場合、HTMLのような静的ファイルを作成します。(認識が間違っているかもしれないので、そのときはご連絡していただけたらと思います)

Prerenderingの場合は、Yewのようなフレームワークに依存しません。そのためwebpack系のライブラリを探していると、PrerenderSPAPluginというものを見つけました。他にもPrerenderingするライブラリ等があったのですが、実装のしやすさ的に、このライブラリにすることを決めました。

実装

PrerenderSPAPlugin

webpack.jsonの内容をいじると、指定したroutesのHTMLファイルをそれぞれ生成してくれます。

具体的には、このような感じにしました。

new PrerenderSPAPlugin({
    staticDir: distPath,
    routes: [
        '/',
        // ...
        '/tumple',
    ],
    postProcessHtml: function (context) {
        return context.html.replace(
            /http:\/\/localhost:8000/gi, 'https://bkbkb.net'
        )
    },
    renderer: new Renderer({
        renderAfterDocumentEvent: 'prerender-trigger',
    })
})

生成する際は、ローカル環境のドメインになるので、URLを置換するようにしました。(自分は失敗しましたが、pathか何かで上手く設定すると、ドメインを変更してくれるかもしれません)

何も指定しないと、プリレンダリングの際にJavaScript等がまだ実行していないときに、HTMLが生成されてしまいました。そのためrendererの設定で、イベントをトリガーとして、metaタグを変更した後に発生させて、そのあとにHTMLが生成されるようにしました。

Nginx

これでプリレンダリングが完了したのですが、生成されたHTMLは静的なためJavaScript等を含んでおらず、動的な処理はできず、本来のサイトの動作をしなくなりました。

動的な処理はしたいため、ボットの場合プリレンダリングで生成されたHTMLを送信し、一般ユーザーの場合JavaScript等を含む元のファイルを送信するようにできないかと考えました。

Nginxで$http_user_agentごとに送信するファイルを分ければ、できそうであることがわかりました。慣れてない.confファイルの設定を変更し、ボットである場合、プリレンダリングで生成されたHTMLを送信するようにしました。

set $html_file "/main.html";
if ($http_user_agent ~* "bot|crawler") {
    set $html_file "$uri/";
}

try_files $uri $html_file;

まとめ

発展途上のフレームワークを使用すると、成熟したフレームワークで実装されているものも自ら用意しなければならないことを、身をもって感じました。