Author: Till Wehowski
Posted on: 2016-03-17
Package: µ.Flow
However when you have already implemented a regular site with multiple pages, moving on to single page application approach can require a lot of work and compromise prior SEO (Search Engine Optimizations).
Read this article to learn about a simpler approach based on using HTML5 pushState and AJAX requires that requires minor changes to your site pages and provide a smooth user experience preserving your site past SEO efforts.
Contents
Introduction
Main Goals
Client Side Code using AngularJS
The Client Side Details
Server Side Details
Conclusion
Introduction
In this article I will talk about "HTML5 Mobile Application", a topic with certainly many aspects. For now I would like to cover the following aspects:
- It is recommended for "mobile apps" so I will cover how can it load partial content asynchronously without reloading the whole page
- How do I asynchronously load single page apps covering SEO concerns
Well, this last topic is not so trivial now. Google expects Web sites that are modern and friendly to give them higher search result rankings.
On the other hand, I think Google often tries to force their own standards, like the issue of #! (hashbang) URLs and things like that, so you have to deal with there own ideas rather to use common sense.
So in short, my solution that want to present here may not be the ultimate solution, but how I think it should be implemented as a quick and easy workaround.
Main Goals
As we all know, development of mobile apps is "in vogue" nowadays or it will become so common that many developers will fear to have to work twice as much to do the complete job.Modern Single Page Apps may not be optimized for SEO (Search Eengine Optimization) purpose.
If you read modern books they often tell you to start your application thinking of it to be used by mobile users and then enhance it to work on desktop browsers.
Well, the reality is, most existing applications are developed as desktop browser app and now there is a need to update them to work as mobile applications.
In this blog article I would like to describe how I changed a desktop browser based app to an responsive SEO-friendly HTML5 Web App.
Client Side Code using AngularJS
.directive('frdlAjaxLinkBoddystripped', ['$location', '$window', function($location, $window) { return { restrict: "A", controller: ['$element', '$attrs', '$timeout', '$compile', '$location', '$window', function($element, $attrs, $timeout, $compile, $location, $window) { /* $locationProvider.html5Mode( true ); $element.text( htmlentities($element.text) );*/ $element.on('click', function(ev) { if ('undefined' !== window.history.replaceState) { var el = ev.target; var url = el.getAttribute('href'); var uHost = new frdl.Url(url).getHost(); if ('' !== uHost && new frdl.Url().getHost() !== uHost) return true; ev.preventDefault(); var dest = 'body'; var destMeta = document.querySelector('meta[name="frdl-ajax-link-boddystripped.destination"]'); if (null !== destMeta) { dest = destMeta.getAttribute('content'); } $.ajax({ url: url, /* crossDomain: ('www.webfan.de' !== new frdl.Url().getHost() && 'webfan.de' !== new frdl.Url().getHost()) ? false :true, */ crossDomain: true, cache: true, headers: { 'X-Requested-With': 'XMLHttpRequest' /*, 'Origin' : new frdl.Url().getScheme() + '://' + new frdl.Url().getHost() */ , 'X-Flow-Ajax-Link-Boddystripped': 'destination="' + dest + '";' }, type: 'GET', dataType: 'html' }) .done(function(html) { /* alert('System maintenance ' + dest + ' ' + html);*/ $(dest).html(html); var OnUnload = function(e) { e.preventDefault(); /* e.stopPropagation(); */ return false; }; window.addEventListener('beforeunload', OnUnload); window.history.replaceState({}, 'Title', url); window.removeEventListener('beforeunload', OnUnload); setTimeout(function() { try { var newTitle = document.querySelector('meta[name="document.title"]').getAttribute('content'); } catch (err) { } if ('undefined' !== typeof newTitle) $('title').html(newTitle); try { $(document).scrollTop(0); } catch (err) { } $(document).trigger('readystatechange'); if (true === frdl.Device().isMobile || true === frdl.Device().isTablet) { $(dest).trigger('create'); } }, 10); /* $location.url(url); $location.search(new frdl.Url(url).getParams()); */ }).fail(function(jqXHR, textStatus) { console.error('Error: ' + url + ' ' + jqXHR.status + ' ' + textStatus); window.location.href = url; }).always(function() { }); return false; } else { return true; } }); }] }; }])
The Client Side Details
This is written in AngularJS but I am sure anyone can convert it to plain JavaScript easily.
What it does? It handles all link marked with the frdlAjaxLinkBoddystripped attribute. If the link href refers to a different or untrusted domain, no action is taken and the default event will be triggered when the link is clicked.
Otherwise the link is pushed to the HTML5 state history, so the click event will be prevented. Instead an AJAX request is sent to the URL in the link href to retrieve the content.
Now when a a link is clicked:
<a class = "site_menue_link" href="/" frdl-ajax-link-boddystripped = ""> Home </a>Sending Ajax request with headers
headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-Flow-Ajax-Link-Boddystripped': 'destination="' + dest + '";' }
A meta element sent by server that should change the page title.Server Side Details
In the output handler of the server detects the request coming via AJAX checking the headers and transforms the HTML output. By default the AJAX request response only needs the HTML of the body.
ob_start( function( $content ) {Furthermore the AJAX request header can pass a CSS selector to just request a smaller part of the page content:
$cH = $parse_headers( $_SERVER, $headers ); if(isset( $headers[ 'X-Requested-With'] ) && 'XMLHttpRequest' === $headers[ 'X-Requested-With' ]) {
preg_match( "/<title>(.*)<\/title>/s", $content, $titleMatches); preg_match( "/<body>(.*)<\/body>/s", $content, $matches); //for cache GET: $_GET['_______X-Requested-With'] = 'XMLHttpRequest'; if(isset( $matches[1] )) $content=((isset( $titleMatches[1] )) ? '<meta name="document.title" content="' . strip_tags( $titleMatches[1] ).'" />' : '').$matches[1];
}
//... });
$content_reduced = null; if(defined( '_BLOG_DOMAINE_' ) && isset( $headers[ 'X-Requested-With' ]) && 'XMLHttpRequest' === $headers[ 'X-Requested-With' ] && isset($headers['X-Flow-Ajax-Link-Boddystripped'])) { $s = strip_tags( $headers[ 'X-Flow-Ajax-Link-Boddystripped' ]); $_GET['_______X-Flow-Ajax-Link-Boddystripped']=$s; $s = explode(';', $s); foreach($s as $p => $opt) { $o = explode('=', $opt, 2); if( 'destination' === $o[0]) { $selector = $o[1]; $selector = trim( $selector, '" '); $selector = str_replace("'", '"', $selector); foreach( $html->find( $selector ) as $element) { if(null === $content_reduced ) $content_reduced=''; $content_reduced .= $element->innertext; } } } }The HTML meta elements meta[name="frdl-ajax-link-boddystripped.destination"] content define the destination of the new content retrieved via AJAX that will be inserted in the page.
The browser script uses HTML5 window.histroy.replaceState to set the history with hash URLs. That means the new URLs can be used and they will, "clicked" synchronously next time, produce the absolutely same result as when using asynchronous AJAX requests,
Conclusion
Using this approach there is no no need to have a special mobile version of the site. There is also no need to use #! hashbang URLs or ?_escaped_fragment_ requests, which could require a another server side strategy.
You just need to to use JavaScript to synchronize your page presentation with the server side pages output. You do not need to change the links URL schema of your existing pages.
If you liked this article or you have a question about this approach to migrate your site Web pages to fast AJAX based Web apps using HTML5 pushState, post a comment here.
You need to be a registered user or login to post a comment
21,121 JavaScript developers registered to the JS Classes site.
Be One of Us!
Login Immediately with your account on:
Comments:
1. Update changes - Till Wehowski (2016-10-17 13:02)
Removed dependency on angularJS... - 1 reply
Read the whole comment and replies