├── README.md ├── angular-seo-server.js ├── angular-seo.js └── bower.json /README.md: -------------------------------------------------------------------------------- 1 | Angular-SEO 2 | ----------- 3 | 4 | SEO for AngularJS apps made easy. Based on [PhantomJS](http://phantomjs.org/) and [yearofmoo's article](http://www.yearofmoo.com/2012/11/angularjs-and-seo.html). 5 | 6 | 7 | Requirements 8 | ============ 9 | 10 | You will need [PhantomJS](http://phantomjs.org/) to make this work, as it will render the application to HTML. 11 | 12 | 13 | How to use 14 | ========== 15 | 16 | The solution is made of 3 parts: 17 | - small modification of your static HTML file 18 | - an AngularJS module, that you have to include and call 19 | - PhantomJS script 20 | 21 | 22 | Modifying your static HTML 23 | ========================== 24 | 25 | Just add this to your `
` to enable AJAX indexing by the crawlers. 26 | ``` 27 | 28 | ``` 29 | 30 | AngularJS Module 31 | ================ 32 | 33 | Just include `angular-seo.js` and then add the `seo` module to you app: 34 | ``` 35 | angular.module('app', ['ng', 'seo']); 36 | ``` 37 | 38 | If you are using [RequireJS](http://requirejs.org/), the script will detect it and auto define itself *BUT* you will need to have an `angular` shim defined, as `angular-seo` requires it: 39 | ``` 40 | requirejs.config({ 41 | paths: { 42 | angular: 'http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.0.3/angular.min', 43 | }, 44 | shim: { 45 | angular: { 46 | exports: 'angular' 47 | } 48 | } 49 | }); 50 | ``` 51 | 52 | Then you must call `$scope.htmlReady()` when you think the page is complete. This is necessary because of the async nature of AngularJS (such as with AJAX calls). 53 | ``` 54 | function MyCtrl($scope) { 55 | Items.query({}, function(items) { 56 | $scope.items = items; 57 | $scope.htmlReady(); 58 | }); 59 | } 60 | ``` 61 | 62 | If you have a complicated AJAX applicaiton running, you might want to automate this proccess, and call this function on the config level. 63 | 64 | **Please that this is only a suggestion we've came up with in order to make your life easier, and might work great with some set-ups, while not working at all with others, overall, you should try it yourself and see if it's a good fit for your needs.** 65 | There's alwasys the basic setup of calling $rootScope.htmlReady() from the controller. 66 | 67 | Example: 68 | ```javascript 69 | var app = angular.module('myApp', ['angular-seo']) 70 | .config(function($routeProvider, $httpProvider){ 71 | $locationProvider.hashPrefix('!'); 72 | $routeProvider.when({...}); 73 | 74 | var $http, 75 | interceptor = ['$q', '$injector', function ($q, $injector) { 76 | var error; 77 | function success(response) { 78 | $http = $http || $injector.get('$http'); 79 | var $timeout = $injector.get('$timeout'); 80 | var $rootScope = $injector.get('$rootScope'); 81 | if($http.pendingRequests.length < 1) { 82 | $timeout(function(){ 83 | if($http.pendingRequests.length < 1){ 84 | $rootScope.htmlReady(); 85 | } 86 | }, 700);//an 0.7 seconds safety interval, if there are no requests for 0.7 seconds, it means that the app is through rendering 87 | } 88 | return response; 89 | } 90 | 91 | function error(response) { 92 | $http = $http || $injector.get('$http'); 93 | 94 | return $q.reject(response); 95 | } 96 | 97 | return function (promise) { 98 | return promise.then(success, error); 99 | } 100 | }]; 101 | 102 | $httpProvider.responseInterceptors.push(interceptor); 103 | ``` 104 | 105 | And that's all there is to do on the app side. 106 | 107 | 108 | PhantomJS Module 109 | ================ 110 | 111 | For the app to be properly rendered, you will need to run the `angular-seo-server.js` with PhantomJS. 112 | Make sure to disable caching: 113 | ``` 114 | $ phantomjs --disk-cache=no angular-seo-server.js [port] [URL prefix] 115 | ``` 116 | 117 | `URL prefix` is the URL that will be prepended to the path the crawlers will try to get. 118 | 119 | Some examples: 120 | ``` 121 | $ phantomjs --disk-cache=no angular-seo-server.js 8888 http://localhost:8000/myapp 122 | $ phantomjs --disk-cache=no angular-seo-server.js 8888 file:///path/to/index.html 123 | ``` 124 | 125 | 126 | Testing the setup 127 | ================= 128 | 129 | Google and Bing replace `#!` (hashbang) with `?_escaped_fragment_=` so `http://localhost/app.html#!/route` becomes `http://localhost/app.html?_escaped_fragment_=/route`. 130 | 131 | So say you app is running on `http://localhost:8000/index.html` (works with `file://` URLs too). 132 | First, run PhantomJS: 133 | ``` 134 | $ phantomjs --disk-cache=no angular-seo-server.js 8888 http://localhost:8000/index.html 135 | Listening on 8888... 136 | Press Ctrl+C to stop. 137 | ``` 138 | 139 | Then try with cURL: 140 | ``` 141 | $ curl 'http://localhost:8888/?_escaped_fragment_=/route' 142 | ``` 143 | 144 | You should then have a complete, rendered HTML output. 145 | 146 | 147 | Running in behind Nginx (or other) 148 | ================================== 149 | 150 | Of course you don't want regular users to see this, only crawlers. 151 | To detect that, just look for an `_escaped_fragment_` in the query args. 152 | 153 | For instance with Nginx: 154 | ``` 155 | if ($args ~ _escaped_fragment_) { 156 | # Proxy to PhantomJS instance here 157 | } 158 | ``` 159 | [](http://githalytics.com/steeve/angular-seo) 160 | -------------------------------------------------------------------------------- /angular-seo-server.js: -------------------------------------------------------------------------------- 1 | var system = require('system'); 2 | 3 | if (system.args.length < 3) { 4 | console.log("Missing arguments."); 5 | phantom.exit(); 6 | } 7 | 8 | var server = require('webserver').create(); 9 | var port = parseInt(system.args[1]); 10 | var urlPrefix = system.args[2]; 11 | 12 | var parse_qs = function(s) { 13 | var queryString = {}; 14 | var a = document.createElement("a"); 15 | a.href = s; 16 | a.search.replace( 17 | new RegExp("([^?=&]+)(=([^&]*))?", "g"), 18 | function($0, $1, $2, $3) { queryString[$1] = $3; } 19 | ); 20 | return queryString; 21 | }; 22 | 23 | var renderHtml = function(url, cb) { 24 | var page = require('webpage').create(); 25 | page.settings.loadImages = false; 26 | page.settings.localToRemoteUrlAccessEnabled = true; 27 | page.onCallback = function() { 28 | cb(page.content); 29 | page.close(); 30 | }; 31 | // page.onConsoleMessage = function(msg, lineNum, sourceId) { 32 | // console.log('CONSOLE: ' + msg + ' (from line #' + lineNum + ' in "' + sourceId + '")'); 33 | // }; 34 | page.onInitialized = function() { 35 | page.evaluate(function() { 36 | setTimeout(function() { 37 | window.callPhantom(); 38 | }, 10000); 39 | }); 40 | }; 41 | page.open(url); 42 | }; 43 | 44 | server.listen(port, function (request, response) { 45 | var route = parse_qs(request.url)._escaped_fragment_; 46 | var url = urlPrefix 47 | + request.url.slice(1, request.url.indexOf('?')) 48 | + '#!' + decodeURIComponent(route); 49 | renderHtml(url, function(html) { 50 | response.statusCode = 200; 51 | response.write(html); 52 | response.close(); 53 | }); 54 | }); 55 | 56 | console.log('Listening on ' + port + '...'); 57 | console.log('Press Ctrl+C to stop.'); 58 | -------------------------------------------------------------------------------- /angular-seo.js: -------------------------------------------------------------------------------- 1 | !function(window, document, undefined) { 2 | var getModule = function(angular) { 3 | return angular.module('seo', []) 4 | .run([ 5 | '$rootScope', 6 | function($rootScope) { 7 | $rootScope.htmlReady = function() { 8 | $rootScope.$evalAsync(function() { // fire after $digest 9 | setTimeout(function() { // fire after DOM rendering 10 | if (typeof window.callPhantom == 'function') { 11 | window.callPhantom(); 12 | } 13 | }, 0); 14 | }); 15 | }; 16 | } 17 | ]); 18 | }; 19 | if (typeof define == 'function' && define.amd) 20 | define(['angular'], getModule); 21 | else 22 | getModule(angular); 23 | }(window, document); 24 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-seo", 3 | "version": "0.1.0", 4 | "main": "./angular-seo.js", 5 | "ignore": [], 6 | "dependencies": { 7 | "angular": "1.3.13" 8 | } 9 | } --------------------------------------------------------------------------------