The Problem:
AngularJS freamwork is awesome, the two-way data binding is one of the main features and power of AngularJS. On the first day i started working on my AngularJS project - I couldn't go back.
This technic of Java Script rending HTML is great, if you compare to another freamworks like ASP.Net MVC - it's simpler and faster.
But, one question was always on my mind - what happens when we need to get the final rendered page (inculding all content and meta tags) without browser ? using, let's say, a web request ? or some crawler ? What will be the result ?
In the following P.O.C exmple application, I created an angular application with simple service, which update the MetaTitle and MetaDescription values on the rootScope, and bind them to the title and description metatags :
angular.module('MyApp',[]).service('updatemymetaplease',function($rootScope){
var $rootScope = $rootScope;
retrun {
UpdateNow:
function(title,description) {
this.$rootScope.MetaTitle = title;
this.$rootScope.MetaDescription = description;
}
}})
.controller('MainController',function(updatemymetaplease){
updatemymetaplease.UpdateNow('My App Title','Awsome Application');
});
and the page.html file :
<html ng-app="MyApp">
<head>
<title>{{MetaTitle}}</title>
<meta name="description" content="{{ MetaDescription }}">
</head>
<body ng-controller="MainController">
<p>Exemple App</p>
<script src="app.js"></script>
</body>
</html>
After the digest cycle of angular the output will be as the following :
<html>
<head>
<title>My App Title</title>
<meta name="description" content="Awsome Application">
</head>
<body></body>
<script src="app.js"></script>
</html>
isn't it's great ? no static meta tags, my HTML file is fully dynamic, clean and readable, without any unreadable JS files and JQuery opertions on the global scope, and especially - i don't need any server side framework (asp.net,php,java, etc.), I can bind my meta tags dynamiclly on client side.
Let's test our awesome site the Facebook Sharing preview box :
- Facebook preview display
Why ?
Well, Javascript is a client side language (except NodeJS family), I meen the UI can run and render only by the browser - so what happen if your client not using a browser ? in this case Facebook scraper - the crawler expect to get a fully rendered HTML page , actully the caller seems to do GET request, which returns the following HTML page :
<html>
<head>
<title>{{MetaTitle}}</title>
<meta name="description" content="{{ MetaDescription }}">
</head>
<body>
<p>Exemple App</p>
</body>
</html>
It's not a Bug !
We all know that google started rendering heavy JS application, which provide a solution to the google SEO problem spcificlly, but what about Bing crawler ? Facebook scraper? Twitter and many more. at this point a pure angularJS app will be showing as the output html does, without JS rendering.
Do I really have to wrap my app with server side freamwork to solve this issue ? - NO
Soultion Description :
We will use the awsome PhantomJS (http://phantomjs.org/) to render the AngularJS scripts before sending the responed back, that way we can be sure that the meta tags and content will be avalible to the engines, and the output of the html page will be the fully renderd html page. You can also set up a snapshot creator - that way you can respond quickly than ever.
This will be the final life cycel of the application:
Once IIS get's an incoming request, the Rewrite module decides:
- If it's Facebook,Twitter,Bing or anything you will define - then the request will be transferd to PhantomJS windows service - and the fully rendered HTML will be the output.
- Else the regular website behavior.
Make sure you define the rewrite as expected, do not redirect all HTTP call's to the PhantomJS, only the ones you need.
Download the code : https://github.com/benmizrahi/AngularJS-Social-Sharing-And-SEO
Setup and Settings :
Tested Envierment : Window Server 2012 R2 ,IIS 8.5
Step 1 - Install's :
1) Download and install Url-Rewite Module :
http://www.iis.net/downloads/microsoft/url-rewrite
2) Download and install Application-Request-Routing :
http://www.iis.net/learn/extensions/installing-application-request-routing-arr/install-application-request-routing
Step 2 - Phantom Windows Service :
1) Download the git project.
2) Go to PhantomService.exe.config and open with notepad - config the following :
a) PathToPhantom - local path to phantom.exe (from zip).
b) PhantomScript - local path to phantom script (Components\phantomWS.js).
c) PhantomLogLocation - phantom logging location
d) PhantomPort - local port to listen
e) LocalHost - you're local app host (usually http://yourdnsname).
f) CreateSnapshots - enable snapshots (1/0).
g) SanpshootLocation - location to save snapshots.
3) Open cmd and navigate to "C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319"
4) Run the following command : InstallUtil.exe "\path\to\phantom\service\PhantomService.exe" - give the installation an admin user.
5) Before stating the windows service go to "%systemroot%\system32\drivers\etc\"
open hosts file and add yourdnsname as 127.0.0.1 - test inside the server that you can reach :
http://yourdnsname.
6) Go to Run and open services.msc, start the PhantomJSWS service then you can test if phantom is running by browsing to http://localhost:selectedport/ - you should see the IIS welcome screen.
Step 3 - IIS Settings :
Application Request Routing Configuration :
1) Open inetmgr, go to the top level (sever level) and select Features View.
2) Select Application Request Routing Catch (should be the first one).
3) On the right side select Sever Proxy Settings.
4) Select checkbox - Enable proxy.
5) Save settings.
Url-Rewrite Configuration :
1) Open inetmgr, and select you're site or Application.
2) In Features View - select Url Rewrite
3) Add the following settings :
{ HTTP_USER_AGENT }
Rewrite Model
URL: http://localhost:selectedphantomport/{URL}
This setting will rewrite the the request to the phantom windows service.
Test It: add to the url : ?escaped_fragment_= you should get the page yout requested
Exemple: http://yourdnsname/?escaped_fragment_=.
Step 4 - AngularJS Side :
1) Add the directive RenderFinishedDirective.js to you're site and include it to you're AngularApp.
2) In your'e index.html file -> add the directive at the bottom of the body element :
<render-finished-directive></render-finished-directive>
* Note: you can avoid adding this directive by adding this code when your app finish rendering (after all promises end their job) :
if (typeof window.callPhantom == 'function') {
window.callPhantom();
}
That's it - you're done.
Please feel free if you need any help,
Ben Mizrahi
benm@item-soft.com