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 -v
で2.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時間超掛かります)。
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
コマンドで、クローラーの代わりにページを訪問し、任意のタイミングまで待ってから、レンダリング結果を取得するプログラムを作成します。
// 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);
};
このプログラムは、ローカルファイルで動作を確認できます。
<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
で結果を受け取ります。
これを利用して、任意のタイミングで、クローラーにレンダリング結果を渡すことが可能です。
<!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
// 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が訪問した後の検索結果がこんな感じです。