') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - BeOleg/angular-seo: SEO for AngularJS apps made easy. · GitHub
Skip to content

Latest commit

History

25 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Angular-SEO

SEO for AngularJS apps made easy. Based on PhantomJS and yearofmoo's article.

Requirements

You will need PhantomJS to make this work, as it will render the application to HTML.

How to use

The solution is made of 3 parts:

  • small modification of your static HTML file
  • an AngularJS module, that you have to include and call
  • PhantomJS script

Modifying your static HTML

Just add this to your <head> to enable AJAX indexing by the crawlers.

 <meta name="fragment" content="!" />

AngularJS Module

Just include angular-seo.js and then add the seo module to you app:

angular.module('app', ['ng', 'seo']);

If you are using RequireJS, the script will detect it and auto define itself BUT you will need to have an angular shim defined, as angular-seo requires it:

requirejs.config({
paths: {
angular: 'http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.0.8/angular.min',
},
shim: {
angular: {
exports: 'angular'
}
}
});

Then you must call $scope.htmlReady() when you think the page is complete. This is nescessary because of the async nature of AngularJS (such as with AJAX calls).

function MyCtrl($scope) {
Items.query({}, function(items) {
$scope.items = items;
$scope.htmlReady();
});
}

If you have a complicated AJAX applicaiton running, you might want to automate this proccess, and call this function on the config level.

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. There's alwasys the basic setup of calling $rootScope.htmlReady() from the controller.

Example:

varapp=angular.module('myApp',['angular-seo']).config(function($routeProvider,$httpProvider){$locationProvider.hashPrefix('!');$routeProvider.when({...});var$http,interceptor=['$q','$injector',function($q,$injector){varerror;functionsuccess(response){$http=$http||$injector.get('$http');var$timeout=$injector.get('$timeout');var$rootScope=$injector.get('$rootScope');if($http.pendingRequests.length<1){$timeout(function(){if($http.pendingRequests.length<1){$rootScope.htmlReady();}},1000);}returnresponse;}functionerror(response){$http=$http||$injector.get('$http');return$q.reject(response);}returnfunction(promise){returnpromise.then(success,error);}}];$httpProvider.responseInterceptors.push(interceptor);

And that's all there is to do on the app side.

PhantomJS Module

For the app to be properly rendered, you will need to run the angular-seo-server.js with PhantomJS. Make sure to disable caching:

$ phantomjs --disk-cache=no angular-seo-server.js [port] [URL prefix]

URL prefix is the URL that will be prepended to the path the crawlers will try to get.

Some examples:

$ phantomjs --disk-cache=no angular-seo-server.js 8888 http://localhost:8000/myapp
$ phantomjs --disk-cache=no angular-seo-server.js 8888 file:///path/to/index.html

Testing the setup

Google and Bing replace #! (hashbang) with ?_escaped_fragment_= so htttp://localhost/app.html#!/route becomes htttp://localhost/app.html?_escaped_fragment_=/route.

So say you app is running on http://localhost:8000/index.html (works with file:// URLs too). First, run PhantomJS:

$ phantomjs --disk-cache=no angular-seo-server.js 8888 http://localhost:8000/index.html
Listening on 8888...
Press Ctrl+C to stop.

Then try with cURL:

$ curl 'http://localhost:8888/?_escaped_fragment_=/route'

You should then have a complete, rendered HTML output.

Running in behind Nginx or Apache

If course you don't want regular users to see this, only crawlers. To detect that, just look for an _escaped_fragment_ in the query args.

Nginx(hasbang url):

server{
listen 80;
server_name example.com;
if ($args ~ _escaped_fragment_) {
proxy_pass http://127.0.0.1:8888;
break;
}
}

Nginx(pushState url): The second condition won't work for non-pushState URLs since those bots that don't support the escaped_fragment format will just remove anythin that's after the hasbang from the the requested url.

server{
listen 80;
server_name example.com;
if ($args ~ _escaped_fragment_) {
proxy_pass http://127.0.0.1:8888;
break;
}
if ($http_user_agent ~* (LinkedInBot|UnwidFetchor|voyager)){
rewrite ^(.*)$ /?_escaped_fragment=$1 last;
proxy_pass http://127.0.0.1:8888;
break;
}
}

Apache(untested):

<IfModule mod_rewrite.c>
RewriteEngine On
# Don't rewrite files or directories
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteCond %{HTTP_USER_AGENT} ^(LinkedInBot|UnwidFetchor|voyager).+
RewriteRule ^/[a-zA-Z0-9]+[/]?$ /?_escaped_fragment_=$1 [QSA,L]
ProxyPass / http://127.0.0.1:8888
ProxyPassReverse / http://127.0.0.1:8888
#Simulate _escaped_fragment_ query string for bots that don't do it on their
# Rewrite everything else to index.html to allow pushState deep linking
RewriteRule ^/[a-zA-Z0-9]+[/]?$ /index.html [QSA,L]
</IfModule>

Upstart script for auto-start & respawn: $sudo vi /etc/init/phantomjs.conf

description "phantomjs"
start on runlevel [2345]
stop on runlevel [!2345]
console log
setuid tomigo
setgid tomigo
respawn
script
exec phantomjs --disk-cache=no --ignore-ssl-errors=yes /opt/path/to/angular-seo-server.js 8888 https://example.com/$
end script

And then just:

$phantomjs start

and you're on!

For development enviorments\staging(remote, local), just run the command with nohup or setsid:

$setsid phantomjs --disk-cache=no /opt/path/to/angular-seo-server.js 8888 http://127.0.0.1
$nohup phantomjs --disk-cache=no /opt/path/to/angular-seo-server.js 8888 http://127.0.0.1

Differenct is that nohup outputs to ~/nohup.out while running in the background, and setsid run in the current terminal session, and when it's closed outputs to /dev/null githalytics.com alpha

About

SEO for AngularJS apps made easy.

Resources

Stars

13 stars

Watchers

3 watching

Forks

Releases

Packages

Contributors