Tutorials WordPress Tips

How to add proper pagination as default in WordPress themes

When you scroll to the bottom of an index page on a WordPress site, what do you see? Chances are it’s two links, one saying “Older Posts” and one saying “Newer Posts”. Scarcely has any default setting been more engagingly uninformative, and unnecessarily so. This “let’s not tell the visitor where she is on the site nor how much other content is available” approach to navigation is the epitome of User Experience archaism and should be but a footnote in the annals of WordPress history. Instead it is a cornerstone of the free WordPress theme experience and the default setting for all baseline WordPress themes (_s included) with one groundbreaking exception: Twenty Fourteen.

It is time we the people who build WordPress themes cast aside the rotting corpse of Older and Newer posts and replace it with proper pagination. After all the function is already built into WordPress – in fact it has been for a very, very long time. So, without further ado let me provide you with the code you need to get started and a few tweaks to move your WordPress pagination into the 21st century.

paginate_links() – the old friend you never knew you had

You don’t need fancy plugins or custom code to get proper pagination in your index pages. The paginate_links() function will do that for you in a snap. Here it is in its most basic form, straight from the Codex:

global $wp_query;

$big = 999999999; // need an unlikely integer

echo paginate_links( array(
	'base' => str_replace( $big, '%#%', esc_url( get_pagenum_link( $big ) ) ),
	'format' => '?paged=%#%',
	'current' => max( 1, get_query_var('paged') ),
	'total' => $wp_query->max_num_pages
) );

(If you’re wondering what the $big variable is for it helps output a clean URL when the page is loaded. Pay no mind.)

Slot this code into your template file where you want the pagination to appear (i.e. replace the current Older Posts and Newer Posts code with this one) and you’re good to go. If you want the basic solution that is. Which probably isn’t what you want.

Options Aplenty

Luckily for us we don’t have to be content with the default example above. paginate_links() has plenty of parameters we can use to change its output. The parameters you should care about from a design perspective are:

  • end_size: Decides how many numbers to display at the start and end of the list. Defaults to 1 meaning you’ll get output like this: 2 … 5 6 7 … 10. If you change the number to 2 you’ll get 2 3 … 5 6 7 … 9 10. You get the idea. Visually 1 looks best but in some cases you may want to change this value.
  • mid_size: Decides how many numbers to display on the left and right side of the current page (not including the current page). Defaults to 1. So if you set mid_size to 2 and are on page 6 you’d get 2 … 4 5 6 7 8 … 10.
  • prev_next: True or false decides whether a Previous and Next link should appear in the list or not. The actual wording is controlled separately (see below).
  • prev_text and next_text: The actual text output displayed if prev_next is set to true. Default output is « Previous and Next » and the strings are wrapped and prepared for translation.
  • type: Decides what the function outputs. The default plain outputs a list of anchor tags (links) which imo makes no sense. This can be switched to either array (great if you want complete control and know how to handle arrays in PHP) or list which outputs an unordered list and should be the default setting. I strongly recommend  setting type to list so you can work with the output in a reasonable way.

There are other variables but they are technical and control the functionality which I’ll address at the end of this article.

Upcycling existing code

Proper WordPress Pagination
So, let’s say you want to add pagination to your site (not unlike what you see directly above here) and you want to do it in a clean way. In that case it behooves you to create a function, stash it in functions.php or template-tags.php in your theme or child theme, and call it in when necessary. The easiest way to get this working, and working properly, is to upcycle the function provided in Twenty Fourteen and make some subtle changes to its output:


if ( ! function_exists( 'yourtheme_paging_nav' ) ) :
 * Display navigation to next/previous set of posts when applicable.
 * Based on paging nav function from Twenty Fourteen

function yourtheme_paging_nav() {
	// Don't print empty markup if there's only one page.
	if ( $GLOBALS['wp_query']->max_num_pages < 2 ) {

	$paged        = get_query_var( 'paged' ) ? intval( get_query_var( 'paged' ) ) : 1;
	$pagenum_link = html_entity_decode( get_pagenum_link() );
	$query_args   = array();
	$url_parts    = explode( '?', $pagenum_link );

	if ( isset( $url_parts[1] ) ) {
		wp_parse_str( $url_parts[1], $query_args );

	$pagenum_link = remove_query_arg( array_keys( $query_args ), $pagenum_link );
	$pagenum_link = trailingslashit( $pagenum_link ) . '%_%';

	$format  = $GLOBALS['wp_rewrite']->using_index_permalinks() && ! strpos( $pagenum_link, 'index.php' ) ? 'index.php/' : '';
	$format .= $GLOBALS['wp_rewrite']->using_permalinks() ? user_trailingslashit( 'page/%#%', 'paged' ) : '?paged=%#%';

	// Set up paginated links.
	$links = paginate_links( array(
		'base'     => $pagenum_link,
		'format'   => $format,
		'total'    => $GLOBALS['wp_query']->max_num_pages,
		'current'  => $paged,
		'mid_size' => 3,
		'add_args' => array_map( 'urlencode', $query_args ),
		'prev_text' => __( '&larr; Previous', 'yourtheme' ),
		'next_text' => __( 'Next &rarr;', 'yourtheme' ),
		'type'      => 'list',
	) );

	if ( $links ) :

	<nav class="navigation paging-navigation" role="navigation">
		<h1 class="screen-reader-text"><?php _e( 'Posts navigation', 'yourtheme' ); ?></h1>
			<?php echo $links; ?>
	</nav><!-- .navigation -->


To make this work in a template simply call the function like this:

<?php yourtheme_paging_nav(); ?>

The function will output clean HTML markup with the pagination links wrapped in list items within an unordered list like this:

<nav class="navigation paging-navigation" role="navigation">
	<h1 class="screen-reader-text">Posts navigation</h1>
	<ul class='page-numbers'>
		<li><a class="prev page-numbers" href="http://localhost/page/7/">&larr; Previous</a></li>
		<li><a class='page-numbers' href='http://localhost/'>1</a></li>
		<li><span class="page-numbers dots">&hellip;</span></li>
		<li><a class='page-numbers' href='http://localhost/page/5/'>5</a></li>
		<li><a class='page-numbers' href='http://localhost/page/6/'>6</a></li>
		<li><a class='page-numbers' href='http://localhost/page/7/'>7</a></li>
		<li><span class='page-numbers current'>8</span></li>
		<li><a class='page-numbers' href='http://localhost/page/9/'>9</a></li>
		<li><a class='page-numbers' href='http://localhost/page/10/'>10</a></li>
		<li><a class='page-numbers' href='http://localhost/page/11/'>11</a></li>
		<li><span class="page-numbers dots">&hellip;</span></li>
		<li><a class='page-numbers' href='http://localhost/page/14/'>14</a></li>
		<li><a class="next page-numbers" href="http://localhost/page/9/">Next &rarr;</a></li>
</nav><!-- .navigation -->

Now all you need is some basic CSS to make it all look good:

/* =Index pagination
----------------------------------------------- */

/* The containing box with a nice white background */
.paging-navigation {
	font-family: sans-serif;
	padding: 1em;
	background: #fff;
	background: hsl(0, 0%, 100%);

/* Remove bullets and list indentation */
.paging-navigation ul {
	list-style-type: none;
	margin: 0;
	padding: 0;

/* Make the list items appear horizontally */
.paging-navigation li {
	display: inline;

/* Give each link and the current item some padding to make them easy to click */, {
	padding: .3em .7em;
	color: #333;
	color: hsl(0, 0%, 20%);

/* Link hover state */ {
	color: #000;
	color: hsl(0, 0%, 0%);

/* Current page bold and dark */
.paging-navigation .current {
	font-weight: bold;
	color: #000;
	color: hsl(0, 0%, 0%);

Of course you can elaborate on this to your heart’s content, but this is the gist of it.

Go Forth and Paginate

Now that you see how easy it is to add proper pagination to your WordPress index pages I think you will agree with me there is no good reason to keep the old Older and Newer posts links in place. If you don’t agree with me, by all means leave your disagreements in the comments below and let’s have a discussion. Otherwise, familiarize yourself with paginate_links(); and give your visitors a better user experience.

WordPress Tips

WordPress Importer not importing Attachments? Try exporting All Statuses!

Ever tried to export content from a WordPress site and then import it to another with the WordPress Importer only to find the attachments were not imported? You are not alone. I just spent a couple of hours troubleshooting the issue and I made a very interesting, if perplexing, discovery that may solve your problem.

First a little back story. Of the many things that drive people crazy and induces a burning desire to take the application for a spin in a blender is the WordPress Importer. To say the plugin is rife with problems would be an understatement. A simple Google search will show you that the importer not importing attachments is a problem that has been around for years and has yet to be dealt with properly. Not impressive in the least. Now let’s get to the point of my story.

The Problem

Here’s the scenario: Site 1 has 900+ posts of which about 800 have already been moved to Site 2 by porting the database. Now the new posts (about 100) need to be ported over using the importer. Because there are about 30 draft posts in Site 1 that have already been ported, I wanted to ignore these so in the exporter under Statuses I selected “Published”.

Run the importer and all the posts are imported but the image and attachment links are all pointing back to Site 1. Not what I want.

So I do some tinkering with the import file, move the files manually, and generally mess around with everything. This has worked in the past, but for some unexplainable reason it doesn’t work now. The attachments are not being imported.

The Solution (which makes no sense)

So, after banging my head against the keyboard for a while I decided to try something different: Instead of setting Statuses to “Published” I set it to the default “All Statuses”. The xml file appears, I import it, and behold: The attachments are imported!

The Reason (which makes even less sense)

Comparing the two export files one thing becomes immediately obvious: In the first file – in which I specified I only wanted Published statuses, the attachments are referenced in the posts, but the attachment table is ignored. In the second file – in which I exported All Statuses – the attachment table is included. In other words the WordPress export function defines Attachment posts as something other than Published. Which of course is rubbish, but whatever.

So, if you are having problems getting your attachments to import, try setting Statuses to All Statuses when you export and maybe, just maybe, you’ll get WordPress to do what you want.

WordPress Tips

Anatomy of a vague-baiting spam comment

Have you ever approved a friendly comment on your WordPress site only to be hit by a barrage of comment spam the next day? If so you were probably the victim of what I like to call vague-baiting. Let me explain:

At a recent meeting of the Vancouver WordPress Meetup Group someone asked about how to avoid getting so much comment spam on their WordPress site. The simple answer to that question is to install the Akismet plugin which should catch 98% or so of your spam and which learns and improves over time. But that’s not the whole answer.

You see, not all spam is created equal. Spammers know that you use tools like Akismet and other moderation systems to keep them at bay so they try their best to trick you into letting them past your blockades. They do this by submitting innocent looking comments like this:

Example of typical spam comment

What you see above is a typical spam comment as it came through on my site. Akismet didn’t flag it because it doesn’t have any of the tell-tale signs of being a spam comment, but it is spam nonetheless. This begs a rather obvious question:

Why all the trickery for a spam comment that has no value?

The purpose of spam is to drive your visitors to other sites that the spammer can earn money from. So why on earth would someone go to such lengths to post a spam comment with no nefarious links and a completely innocuous and pointless message? The answer lies in how WordPress and most other publishing systems work: A common way to avoid spammers is to simply set the site so that the first comment from a new email address is always held for moderation. Only commenters with an already approved comment can comment freely on the site. And Akismet also avoids flagging messages from approved commenters as spam. I think you can see where this is going.

To get around your moderation wall a comment that seems to be praising the site is submitted. Once that comment is approved however, a barrage of spam of the worst kind will hit the site. This vague-baiting aims to trick new or unwitting site owners into approving spammers as valid commenters on their sites. And I’m sad to say it works really well. So how do you identify vague-baiting? Follow me down the rabbit hole:

The Vaguest Comment Ever

The vague-baiting comment is easiest recognized by its insanely vague content. Reading the comment one could think it was written by a real reader, but upon closer inspection you realize this comment could be applied to pretty much any posting on any website about any subject. It is so vague its informational value is close to zero.

The typical content of these comments follows a standard formula: Vague praise of the author for writing a good post / argument / site followed by even vaguer reference to some of the content (usually of the form “your argument is great!”) and then some form of statement indicating that the commenter reads your site regularly / was referred by a friend who reads the site regularly / is going going to recommend other people read the site regularly / all of the above. Some of the comments also indicate that the commenter found the site through a Google search “on the subject”.

The Hyperlink to Nowhere

WordPress and most other publishing platforms ask the commenter to enter a URL to their site. If the commenter enters a URL to an objectionable site or one connected with other spam comments, the comment automatically gets flagged. So instead the vague-baiting comment will usually provide a garbage URL pointing nowhere or a URL to a dummy Facebook profile or similar social media profile as is the case in the grab above. This is done to give you the impression that this is a real person and that they just messed up their URL.

The Bizarre Email Address

A simple way of preventing the most blatant (and stupid) spam bots is to ask for an email address when a comment is posted. Because the vague-baiting spammer wants you to approve the comment and thereby the email address, it has to be a real one. However, they do this so much they use randomized email addresses. Usually you’ll see an email address with a rubbish prefix like ttp856956 followed by either hotmail, yahoo, gmail or some other public email service with a geo suffix in an unusual location, in this case Taiwan. This email suffix needs to be seen in context with the IP address as explained below.

The IP Address to Elsewhere

The final telltale sign of a vague-baiting comment is the IP address. The IP address is the address of the actual computer submitting the comment. In this case the IP address is located in Lima, Peru, not Taiwan as the email address suggests. This in itself could have a reasonable explanation – say the person from Taiwan was visiting Peru – but a quick Google search shows that this IP address shows up again and again as the originator of spam. In other words, case closed.

Avoid Spam by Being Logical

Based on what you’ve seen above, avoiding vague-baiting comments is pretty simple: Be logical. If you get a comment with an unnecessarily vague message, it is probably spam. The threshold for someone going through the trouble of leaving a comment is always pretty high, and no sane person is going to go through the trouble of leaving a super vague comment with no value on your site. And if the vagueness wasn’t enough of a red flag, take a look at the sender URL, email address and IP address and see if they all make sense. If they don’t you can hit the spam button safe in the knowledge that you averted a poorly disguised attack on your site.