技術探し

JavaScriptを中心に記事を書いていきます :;(∩´﹏`∩);:

rendertronを用いてSSRに対応してないサイトでもSEOやOGP対策を行う

Dynamic Rendering

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

f:id:about_hiroppy:20200901005741p:plain

詳しくは、以下のgoogleの記事を読んでください。

developers.google.com

この記事でも説明されているrendertronを今回は用います。

Rendertron

github.com

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.com

上記のリポジトリではdocker-composeで簡単な構成を作りました。

f:id:about_hiroppy:20200901083956p:plain

  • https://foo.comへアクセスが来たとき、Nginxでbotかどうかを判断する
    • botの場合は、rendertron(internal)のサーバーへ
      • アクセスのurlをrendertronのurlのpathにつける
        • e.g. http://rendertron/render/https://foo.com
      • rendetronがindex.htmlへアクセスし、htmlをレンダリングし返す
    • ユーザーの場合は、index.htmlを取りに行く

前段

以下を参考にしました。

github.com

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:8080http://rendertron/render/http://localhost:8080と飛ばすようにし、htmlを返すようにします。

Rendertron

特に何もしなくていいですが、puppeteerを導入するために自前でDockerfileを書くのは少し大変なので、今回は、こちらのイメージを使いました。

hub.docker.com

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);

結果

ユーザーがアクセスした場合

f:id:about_hiroppy:20200901085149p:plain

まんま上記のhtmlが出力されただけとなり、CSRです。

botがアクセスした場合

f:id:about_hiroppy:20200901085214p:plain

metaにogや bodyの中にd3の結果出力コードが出ていてSSRが成功しました。

また、画面でみてもユーザーのアクセスと同様の画面となります。

f:id:about_hiroppy:20200901085423p:plain

これでgoogleボットやTwitterなどのogpにも対応することが可能です。

問題点

体感的に、SSRよりは遅く感じます。SSRは最適化しやすいのもあると思いますが。

f:id:about_hiroppy:20200901085934p:plain

SSRよりは楽な分、効率が悪いようにみえますが、今後ssr-serverとrendertronで同じ鯖スペックでどれぐらい捌けるのかも含め実験してみたいなーって思ったりします。

いずれにせよこういうのは、大規模サービスで実験しないとわからないことが多いので今後に期待です。

さいごに

昨日の夜、突然やりたくなって記事にしました。

導入コストは低いので、今SPAなサイトだけどSSRしてないからSEOが不安とかogpも有効化したい!って人は検討してみてもいいんじゃないでしょうか。

リポジトリ

github.com