 
   rendertronを用いてSSRに対応してないサイトでもSEOやOGP対策を行う
目次
Dynamic Rendering
この手法は Dynamic Rendering と呼ばれ、SSR に対応してないサイトに対しての SEO 対策として有効です。Dynamic Rendering とは一言でいうと、サーバーで Node 単体ではなく、ブラウザを動かすイメージです。 これは SSR みたいな Node.js のコードを書くことないため、導入コストは低いです。

詳しくは、以下の google の記事を読んでください。
استخدام العرض الديناميكي كحل بديل | مجموعة خدمات بحث Google | Documentation | Google for Developers
كان العرض الديناميكي حلاً بديلاً للمواقع الإلكترونية التي تواجه مشاكل بخصوص المحتوى المستند إلى JavaScript.
 
 この記事でも説明されている rendertron を今回は用います。
Rendertron
GitHub - GoogleChrome/rendertron: A Headless Chrome rendering solution
A Headless Chrome rendering solution. Contribute to GoogleChrome/rendertron development by creating an account on GitHub.
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 で動かすのはもっと簡単だったりします。
インフラ構成
GitHub - hiroppy/dynamic-rendering-sample: This repo introduces how to realize dynamic rendering.
This repo introduces how to realize dynamic rendering. - hiroppy/dynamic-rendering-sample
上記のリポジトリでは docker-compose で簡単な構成を作りました。

- https://foo.comへアクセスが来たとき、Nginx で bot かどうかを判断する- bot の場合は、rendertron(internal)のサーバーへ
- アクセスの url を rendertron の url の path につける
- e.g. http://rendertron/render/https://foo.com
 
- e.g. 
- rendetron が index.html へアクセスし、html をレンダリングし返す
 
- アクセスの url を rendertron の url の path につける
- ユーザーの場合は、index.html を取りに行く
 
- bot の場合は、rendertron(internal)のサーバーへ
前段
以下を参考にしました。
https://github.com/GoogleChrome/rendertron/blob/main/docs/server-setup/nginx.md
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 を書くのは少し大変なので、今回は、こちらのイメージを使いました。
https://hub.docker.com/r/ammobindotca/rendertron
SPA
index.html を持っているサーバーでtry_filesしてあげることにより、404 を回避させます。
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 を設定したいと思います。
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);
// ogpconst 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 も有効化したい!って人は検討してみてもいいんじゃないでしょうか。
リポジトリ
GitHub - hiroppy/dynamic-rendering-sample: This repo introduces how to realize dynamic rendering.
This repo introduces how to realize dynamic rendering. - hiroppy/dynamic-rendering-sample
 
   
  