rendertronを用いてSSRに対応してないサイトでもSEOやOGP対策を行う
Dynamic Rendering
この手法はDynamic Renderingと呼ばれ、SSRに対応してないサイトに対してのSEO対策として有効です。Dynamic Renderingとは一言でいうと、サーバーでNode単体ではなく、ブラウザを動かすイメージです。 これはSSRみたいなNode.jsのコードを書くことないため、導入コストは低いです。
詳しくは、以下のgoogleの記事を読んでください。
この記事でも説明されているrendertronを今回は用います。
Rendertron
puppeteerをラップしたapi serverみたいなもので内部はkoaが使われています。これを起動し、/render
へurlをpathとして挿入するとそのページのhtmlが返されます。 例えば、/render/https://google.com
とアクセスすると、google.comのhtmlが返ってきます。 また、スクリーンショットも取れたりします。(/screenshot
)
返すhtmlは配信元とは一致はせず最適化されたものが返されます。例えば、console.log('hello')
やdocument.write('test')
だけ書かれたjsなどは、htmlに挿入された後そのスクリプトタグはhtml内からなくなったり、base
がついたりします。
ちなみにrendertronをGCPで動かすのはもっと簡単だったりします。
インフラ構成
上記のリポジトリではdocker-composeで簡単な構成を作りました。
https://foo.com
へアクセスが来たとき、Nginxでbotかどうかを判断する
前段
以下を参考にしました。
upstream rendertron { server rendertron:3000; } map $http_user_agent $is_bot { # default 1; # if you want to debug as a bot, you should comment out this '~*googlebot' 1; } server { listen 80; server_name localhost; if ($is_bot = 1) { rewrite ^(.*)$ /rendertron/$1; } location /rendertron/ { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_pass http://rendertron/render/$scheme://storage$request_uri; } location / { proxy_pass http://storage/; } }
botの場合、urlに/rendertron
を付け、location /rendertron/
の分岐へいれます。そして、proxy_pass http://rendertron/render/$scheme://storage$request_uri;
のリバースプロキシを設定します。 このように書くことにより、http://localhost:8080
をhttp://rendertron/render/http://localhost:8080
と飛ばすようにし、htmlを返すようにします。
Rendertron
特に何もしなくていいですが、puppeteerを導入するために自前でDockerfileを書くのは少し大変なので、今回は、こちらのイメージを使いました。
SPA
index.htmlを持っているサーバーでtry_files
してあげることにより、404を回避させます。
# nginx.conf server { listen 80; server_name localhost; location / { root /usr/share/nginx/sample; # for spa try_files $uri $uri/ /index.html; } }
HTML, JS
ここは例なので何でもよく、各サービスのアプリケーションコードとなります。
今回は、重いアプリケーションを動かしたかったのでここからd3のサンプルを借りました。 これがhtmlへレンダリングされていれば成功となります。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"/> </head> <body> <div id="observablehq-6b3f2a05"></div> <script type="module"> (async () => { const id = '#observablehq-6b3f2a05'; if (document.querySelector(id).children.length === 0) { const { Runtime, Inspector } = await import('https://cdn.jsdelivr.net/npm/@observablehq/runtime@4/dist/runtime.js'); const { default: define } = await import('https://api.observablehq.com/@d3/hierarchical-edge-bundling.js?v=3'); const inspect = Inspector.into(id); (new Runtime).module(define, name => name === 'chart' ? inspect() : undefined); } })(); </script> <script src="main.js"></script> </body> </html>
main.jsでは、ogpを設定したいと思います。
// main.js const a = document.createElement('a'); a.setAttribute('href', 'https://observablehq.com/@d3/hierarchical-edge-bundling'); a.text = 'This is hierarchical-edge-bundling, the code is here.'; document.body.append(a); // ogp const props = [ { type: 'og:url', content: 'http://localhost:8080' }, { type: 'og:type', content: 'website' }, ... ]; const fragment = document.createDocumentFragment(); props.forEach(({ type, content }) => { const meta = document.createElement('meta'); meta.setAttribute('property', type); meta.setAttribute('content', content); fragment.appendChild(meta); }); document.querySelector('head').appendChild(fragment);
結果
ユーザーがアクセスした場合
まんま上記のhtmlが出力されただけとなり、CSRです。
botがアクセスした場合
metaにog
や bodyの中にd3の結果出力コードが出ていてSSRが成功しました。
また、画面でみてもユーザーのアクセスと同様の画面となります。
これでgoogleボットやTwitterなどのogpにも対応することが可能です。
問題点
体感的に、SSRよりは遅く感じます。SSRは最適化しやすいのもあると思いますが。
SSRよりは楽な分、効率が悪いようにみえますが、今後ssr-serverとrendertronで同じ鯖スペックでどれぐらい捌けるのかも含め実験してみたいなーって思ったりします。
いずれにせよこういうのは、大規模サービスで実験しないとわからないことが多いので今後に期待です。
さいごに
昨日の夜、突然やりたくなって記事にしました。
導入コストは低いので、今SPAなサイトだけどSSRしてないからSEOが不安とかogpも有効化したい!って人は検討してみてもいいんじゃないでしょうか。