26
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

AngularJSのSEO対策について

Last updated at Posted at 2015-05-07

Scotch.ioで取り上げられていたprerender-node#1.2.1では、angular-ui-router#0.2.13のレンダリング結果をうまく取得できなかったので、DIYで解決する手順を紹介します。
忙しい人はexpress-turnout#0.0.4-alphaを参照ください。

Phantomjs 2.0.0 のインストール

$ phantomjs -v2.0.0が返る環境を用意します。

Win (cmder)

λ curl -OL https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.0.0-windows.zip
λ unzip phantomjs-2.0.0-windows.zip
λ mv %cd%\phantomjs-2.0.0-windows\bin\phantomjs.exe %CMDER_ROOT%\bin\phantomjs.exe
λ phantomjs -v
2.0.0

Mac (Terminal/Homebrew)

$ brew install phantomjs
$ phantomjs -v
2.0.0

Linux

こちらの記事を参考に、CentOS 5.9でビルドに成功しました(終了まで2時間超掛かります)。

Compiling phantomJS 2.0 for AWS EC2 linux image

nodejsからphantomjsを実行する

同様に、node.js経由でphantomjsが実行できることを確認します。

$ node
var exec= require('child_process').exec;
exec('phantomjs -v',function(error,stdout){
  console.log(stdout);// 2.0.0
});

レンダリング用の実行ファイルを作成する

$ phantomjs render.js urlコマンドで、クローラーの代わりにページを訪問し、任意のタイミングまで待ってから、レンダリング結果を取得するプログラムを作成します。

./render.js
// Dependencies
var system= require('system');
var webpage= require('webpage');

// Environment
var url= system.args[1];

// Setup webpage
var page= webpage.create();
page.open(url);
page.onCallback= function(html){
  system.stdout.write(html);
  phantom.exit(0);
};

このプログラムは、ローカルファイルで動作を確認できます。

./index.html
<script>
if(window.callPhantom){
  window.callPhantom(document.documentElement.outerHTML);
}
</script>
$ phantomjs render.js index.html
# <html><head><script>if(window.callPhantom){window.callPhantom(document.documentElement.outerHTML);}</script></head></html>

webpage内には、window.callPhantomメソッドが用意されており、render.jsのpage.onCallbackで結果を受け取ります。

これを利用して、任意のタイミングで、クローラーにレンダリング結果を渡すことが可能です。

./index.html
<!DOCTYPE html>
<html lang="en" ng-app="myApp">
<head>
  <meta charset="UTF-8">
  <title>{{meta['og:site_name'] + ' | ' + title}}</title>
  <meta content="{{value}}" property="{{key}}" ng-repeat="(key,value) in meta">

  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.15/angular.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.14/angular-ui-router.js"></script>
  
  <script>
angular.module('myApp',['ui.router'])
.run(function($rootScope,$window,$location,$state){
  $rootScope.$on('$viewContentLoaded',function(){
    var renderedTemplate= $window.document.body.innerHTML.trim().length>0
    if(renderedTemplate){
      $rootScope.title= 'Welcome clawlers!';
      $rootScope.meta= {
        'og:site_name':$state.current.name,
        'og:title':$rootScope.title,
        'og:url':$location.absUrl(),
        'og:type':'article',
        'og:description':angular.element($window.document.body).text(),
      }

      if(window.callPhantom){
        setTimeout(function(){
          window.callPhantom(document.documentElement.outerHTML);
        },0);
      }
    }
  });
})
.config(function($stateProvider){
  $stateProvider.state('private',{
    url:'/private',
    template:'<h1>private</h1>',
  });
  $stateProvider.state('second',{
    url:'/second',
    template:'<h1>second</h1><a ui-sref="first">first</a>',
  });
  $stateProvider.state('first',{
    url:'*path',
    template:'<h1>first</h1><a ui-sref="second">second</a>',
  });
})
;
  </script>
</head>
<body ui-view></body>
</html>
$ phantomjs render.js index.html
# <html lang="en" ng-app="myApp" class="ng-scope"><head><style type="text/css">@charset "UTF-8";[ng\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\:form{display:block;}</style>
#   <meta charset="UTF-8">
#   <title class="ng-binding">first | Welcome clawlers!</title>
#   <!-- ngRepeat: (key,value) in meta --><meta content="firstsecond" property="og:description" ng-repeat="(key,value) in meta" class="ng-scope"><!-- end ngRepeat: (key,value) in meta --><meta content="first" property="og:site_name" ng-repeat="(key,value) in meta" class="ng-scope"><!-- end ngRepeat: (key,value) in meta --><meta content="Welcome clawlers!" property="og:title" ng-repeat="(key,value) in meta" class="ng-scope"><!-- end ngRepeat: (key,value) in meta --><meta content="article" property="og:type" ng-repeat="(key,value) in meta" class="ng-scope"><!-- end ngRepeat: (key,value) in meta --><meta content="file:///Users/59naga/Downloads/hoge.html" property="og:url" ng-repeat="(key,value) in meta" class="ng-scope"><!-- end ngRepeat: (key,value) in meta -->
# 
#   <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.15/angular.js"></script>
#   <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.14/angular-ui-router.js"></script>
# 
#   <script>
# angular.module('myApp',['ui.router'])
# //...
#   </script>
# </head>
# <!-- uiView:  --><body ui-view="" class="ng-scope"><h1 class="ng-scope">first</h1><a ui-sref="second" class="ng-scope" href="#/second">second</a></body></html>

<meta>, <title>, <body>をレンダリングした状態で、ソースコードを取得できました。

サーバーから実行する

上記をExpress4から使用する場合、以下のように実装できます。

$ npm install express
$ node app
# listening at 59798
$ curl -A Googlebot http://localhost:59798/
# Prerendered source-code
./app.js
// Environment
var port= 59798;

// Dependencies
var express= require('express');
var exec= require('child_process').exec;

// Setup prerender
var prerender= express.Router();
prerender.use(function(req,res,next){
  var ua= req.headers['user-agent'] || '';
  if(ua.match('Googlebot')==null){
    return next();
  }

  var scriptFile= require.resolve('./render.js');
  var uri= req.protocol+'://'+req.get('host')+req.originalUrl;
  var script= 'phantomjs '+scriptFile+' '+uri;
  exec(script,function(error,stdout,stderr){
    if(error){
      res.status(403);
      res.end(stderr);
      return;
    }
    res.set('Content-Type','text/html');
    res.end(stdout);
  });
});

// Setup express
var app= express();
app.use(prerender);
app.use(function(req, res) {
  res.sendFile(__dirname + '/index.html');
});

// Boot
app.listen(port, function() {
  console.log('listening at %s', port);
});

注意

  • 記事内のコードは、例外処理を省いた最低限のコードです。
  • 実行コストが高く、レスポンスに1秒以上掛かることも少なくありません。実際に使用する場合は、タイムアウト、ブラックリスト・ホワイトリスト、キャッシュが必要になるでしょう。
  • 前2機能はexpress-turnoutというライブラリに実装しました。ui-routerを使ったページに使用して、Googleが訪問した後の検索結果がこんな感じです

参考

26
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?