Article

Derek Jones's avatar

SEO and User-friendly Ajax Pagination

by: Derek Jones on: 12/10/2016 | Read in 8 minutes

This article will teach you how to make SEO-friendly and user-friendly Ajax pagination using ExpressionEngine in under 10 minutes.

Here are the goals we will accomplish with this progressive enhancement:

  • Great user experience
  • Bandwidth-friendly for mobile users
  • Minimize server-side resources
  • SEO-friendly, specifically:
    • Crawlable by all search engines
    • Proper page titles
    • Proper browser history
    • Proper 404 behavior

In our example we are building a Really Important Website that has a fantastic list of key contacts for every important location in the universe. It’s a massive list, so we want to paginate it, and I’d like to show eight at a time. We’re going to work from the outside in, so that that layout components will make sense.

First thing to do is to make a parent HTML layout that will also accept Ajax requests without sending or processing the whole page. We’re going to use the new {is_ajax_request} variable in ExpressionEngine 3.2 to do this. Notice that I’m prefixing my layout template names with an underscore. This makes them “hidden” templates that cannot be directly accessed by a visitor; they can only be accessed when you specify them as either layouts or embeds.

layouts/_html-layout.html

{if is_ajax_request}
	{layout:contents}
{if:else}
	<!DOCTYPE html>
	<html>
	<head>
		<meta charset="utf-8">
		<!-- Here we let templates using this layout set the title tag. -->
		<title>{if layout:title != ''}{layout:title} | {/if}{site_name}</title>
	</head>
	<body>
		<!-- Here you probably have your site nav, a page header, etc., but for
		  for this example, I'm abbreviating the markup to only what is relevant
		  to our Ajax pagination example.
		-->


		<!-- The layout contents variable will be replaced with the content of templates
			that use this layout. We also are adding a way for templates to provide an
			id attribute so we can hook onto this container for CSS or JavaScript.
		-->
		<section id="{if layout:content_id}{layout:content_id}{/if}">
			{layout:contents}
		</section>

		<!-- The rest of your site's markup for sidebars, your footer, other scripts etc. would
			go here. Notice that we also have a layout variable for any page-specific JavaScript
			that a given template might need to load or provide.
		-->
		<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
		{layout:js}
	</body>
	</html>
{/if}

Notice that if it’s an Ajax request, the only content we are outputting is the container contents supplied by our templates. In our case it’s going to be primarily a Channel Entries tag. We want this outer wrapper to be general purpose and reusable. We use a {layout:js} variable that also lets the templates supply needed behaviors. We could do the same with sidebars and other components that are not the same on every page of the site.

Now let’s peel one layer of the onion back and look at our next layout template:

layouts/_multi-layout

As you can tell by the name, we’re now getting more specific in our layout’s purpose, in this case for multi-entry pages. Ultimately this will be used for our Managers listing page, And it is general purpose, so it can be used on other multi-entry pages as needed. It will hold our JavaScript for the Ajax pagination, defined in a {layout:set name="js"}{/layout:set} tag pair which we already told the parent _html-layout wrapper where it belongs.

{layout='layouts/_html-layout'}

<!-- Provide an id attribute for our content container. -->
{layout:set name='content_id' value='managers-listing'}

<!-- Bring forward content from our template. -->
{layout:contents}

<!-- Set the JS that all of our "Managers" content pages need. It may look scary, but
	there are only a dozen or so lines of code, the rest are verbose comments
	explaining the methodology.
-->
{layout:set name="js"}
	<script>
		$(document).ready(function()
		{
			// '#managers-listing' is the id we have provided for our content's parent container.
			// '.managers-listing-pagination' is a class we will give to our pagination links containers.
			// Since the content, including pagination, is replaced in the DOM by each Ajax request,
			// we define this event handler with event delegation, watching the parent container
			// that exists in our original markup and is not replaced or removed from the DOM.
			$('#managers-listing').on('click', '.managers-listing-pagination a', function(e){
				// Prevent the browser from its normal behavior when the link is clicked
				// and grab the href of the pagination link they clicked.
				e.preventDefault();
				var source = $(this).attr('href');

				// Add a load indicator for slow connections. Use whatever you like, if you aren't
				// familiar with implementing them you can get some ideas at http://cssload.net
				var loadIndicator = $('<div class="loader" id="ajax-load-indicator"></div>');
				$('#managers-listing').prepend(loadIndicator);

				// Fetch our content
				$.get(source, function(data)
				{
					// Insert our new content, removing the load indicator.
					$('#managers-listing').html(data);
					$('#ajax-load-indicator').fadeOut('fast').remove();

					// Update our page title for browser tabs, getting the page number from the pagination link they clicked.
					// Since our pagination links exist twice in the DOM (top and bottom), we only want to grab the :first
					// or page 3 will display "Page 33", and so on.
					var title = 'Managers - Page ' + $('.managers-listing-pagination:first a.active').text() + ' | {site_name}';
					document.title = title;

					// For security reasons, pushState() will not update the URL if it includes a domain,
					// so we're using regex to keep only the path, e.g. /managers/P4.
					var path = source.replace(/https?:\/\/[^\/]+/i, '');

					// Push this page onto the browser history stack for forward/back button functionality.
					history.pushState({}, title, path);
				});
			});
		});
	</script>
{/layout:set}

The inline comments explain in detail what we’re doing. Basically we watch for pagination links to be clicked, fetch their content via Ajax, and make sure the page titles and the browser history are updated accordingly. That way we’ve progressively enhanced the user experience without breaking any expected browser behavior for the sake of being slick. And for search engine robots that do understand JavaScript, it’s very important for indexing.

Now we need to create a template for the /managers URL that will use the _multi-layout. We’ll add our Channel Entries tag with pagination, and we’re done!

managers/index

{layout='layouts/_multi-layout'}

{exp:channel:entries channel='managers' limit='8' paginate='both' orderby='title' sort='asc'}
	{if no_results}
		{redirect='404'}
	{/if}

	<!-- Your markup to display the entries would go here. -->
	<h2>SEO and User-friendly Ajax Pagination</h2>

	<!-- Our pagination block is below, it will be placed above and below the entries,
		since we specified paginate="both"
	-->
	{paginate}
		<!-- Set a layout variable for the page title. This is important when the full
			page is accessed and by user agents that do not have JavaScript. For our
			Ajax requests, we've already taken care of this in _managers-layout. Notice
			that in this case the " | {site_name}" bit is taken care of in our
			_html-layout where the title tag is output.
		-->
		{layout:set name='title'}Managers - Page {current_page}{/layout:set}

		<!-- Our pagination links are in a container with the class we used in our JavaScript earlier. -->
		<div class='managers-listing-pagination'>
			{pagination_links}
		</div>
	{/paginate}
{/exp:channel:entries}

Again the inline comments explain what we’re doing, but I would like to draw attention to some protection we’ve added against URL fiddling or mistyped links.

{if no_results}
	{redirect='404'}
{/if}

This tells ExpressionEngine to display the site’s 404 page (with proper 404 headers), which we’ve defined in our Template Settings if there aren’t any results. When would that be? When the URL gives clues to the tag about what to show, but there aren’t any entries matching that criteria. For example, out of bounds page requests, like page 4,872 when there aren’t that many entries: /managers/P4872.

Where to go from here?

This simple example can be expanded upon, and of course marked up and styled to your heart’s content. The key technical elements of the implementation are:

  • Use the {paginate}{/paginate} tag pair both for pagination and to set the page title on initial load of a URL.
  • Make Ajax calls light-weight, by only processing and serving the tags necessary to deliver the content.
  • Use history.pushState() to maintain page titles and browser history both for user experience and SEO friendliness.
  • Set a 404 redirect for out of bounds requests, run a tight ship!
  • Stay DRY by using layouts and the {is_ajax_request} variable so you do not have to create special Ajax templates.
  • This same technique can be directly applied to spanning a single entry across multiple pages.

Follow these principles and you can easily deliver SEO and user-friendly Ajax pagination, impressing clients and improving the experience for their site’s visitors.

Derek Jones's avatar

Derek Jones

Son of a master craftsman. I help make ExpressionEngine. Amateur cyclist and skate-skiier.

Have ideas on how to improve this article? .(JavaScript must be enabled to view this email address) or share your feedback with @ellislab on Twitter.

ExpressionEngine News

#eecms, #events, #releases