Adding related pages and links in your articles

Do you like the WordPress 'insert link' ability in your posts? Then you'll love this little snippet.

In my latest project, the client asked me if he could somehow link his other posts, and maybe some other links in his post like some sort of related links. The site is a news article one, so each author, if he so likes, can add his own related articles.

Naturally, one will require a meta box. I’ve built various meta boxes over the past year and a half since I’ve been developing WordPress themes. So the first thing that came to my mind was that I’ll need an input field. Ok, this covers the link issue, but how can I link related posts from within the WordPress? I knew that tinymce editor has the ability to link to existing content. This is an awesome ability to have, and since it’s embedded in the WordPress itself, there’s got to be a way to use this to my advantage.

Coding without knowing what you’re doing can lead you to more problems than it solves.

As any developer knows, first thing you do is: google. One of the beauties of the internet is that we have all the information right here on our fingertips. What our parents had to go to the library to find out, we can just google (or bing). You’ll see many developers out there complaining how there are many so called ‘developers’ that only take code from Stackoverflow, paste it in their code and claim it as their own. Without knowing what the code you’ve pasted in does, this is a seriously bad practice. Coding without knowing what you’re doing can lead you to more problems than it solves. But it definitely is a way to learn coding. And why should you waste countless of hours building out something from scratch, when there is a solution (or similar one) already available?

This little introduction was written because I googled and stumbled on a piece of code I could use. First one was repeater fields that I found on gist by user da1nonly. This provided the option to add many links if you want. Second one I found on wordpress.stackexchange.

Combining these two information, and modifying them, of course, I came up with a repeater fields with WordPress link posts/pages functionality.

Full code

<?php
add_action('admin_init', 'add_meta_boxes', 1);
function add_meta_boxes() {
	add_meta_box( 'repeatable-fields', esc_html__('Related Articles', 'mytheme'), 'my_related_articles_meta_box_display', 'post', 'normal', 'high');
}
function my_related_articles_meta_box_display() {
	global $post;
	$repeatable_fields = get_post_meta($post->ID, 'repeatable_fields', true);
	wp_nonce_field( 'repeatable_meta_box_nonce', 'repeatable_meta_box_nonce' );
?>
	<script type="text/javascript">
	jQuery(document).ready(function($) {
		'use strict';
		$('#add-row').on('click', function() {
			var row = $('.empty-row.screen-reader-text').clone(true);
			row.removeClass('empty-row screen-reader-text');
			row.insertBefore('#repeatable-fieldset-one tbody>tr:last');
			return false;
		});
		$('.remove-row').on('click', function() {
			$(this).parents('tr').remove();
			return false;
		});
		$('#repeatable-fieldset-one tbody').sortable({
			opacity: 0.6,
			revert: true,
			cursor: 'move',
			handle: '.sort'
		});
		var _link_sideload = false; //used to track whether or not the link dialogue actually existed on this page, ie was wp_editor invoked.
		var link_btn = (function($){
			'use strict';
			var _link_sideload = false; //used to track whether or not the link dialogue actually existed on this page, ie was wp_editor invoked.
			/* PRIVATE METHODS
			-------------------------------------------------------------- */
			//add event listeners
			function _init() {
			    $('body').on('click', '#add-link', function(event) {
			    	function add_empty_row() {
						var row = $('.empty-row.screen-reader-text').clone(true);
						row.removeClass('empty-row screen-reader-text');
						row.insertBefore('#repeatable-fieldset-one tbody>tr:last');
						return false;
					}
					if ( !$('#repeatable-fieldset-one tbody').find('tr:first-of-type').hasClass('empty-row screen-reader-text') ) {
						if ($('#repeatable-fieldset-one tbody').find('tr:first-of-type input[name="article_name[]"]').val() !== '') {
							add_empty_row();
						}
					} else{
						add_empty_row();
					}
			        _addLinkListeners();
			        _link_sideload = false;
			        if ( typeof wpActiveEditor != 'undefined') {
			            wpLink.open();
			        } else {
			            window.wpActiveEditor = true;
			            _link_sideload = true;
			            wpLink.open();
			        }
			        return false;
			    });
			}
			/* LINK EDITOR EVENT HACKS
			-------------------------------------------------------------- */
			function _addLinkListeners() {
			    $('body').on('click', '#wp-link-submit', function(event) {
			        var linkAtts = wpLink.getAttrs();
			        var link_val_container = $('.article_link').eq(-2);
			        var name_val_container = $('.article_name').eq(-2);
			        var link_text = $('#wp-link-text').val();
			        link_val_container.val(linkAtts.href);
			        name_val_container.val(link_text);
					var $frame = $('#content_ifr'),
					$added_links = $frame.contents().find("a[data-mce-href]");
					$added_links.each(function(){
						if ($(this).attr('href') === linkAtts.href) {
							$(this).remove();
						}
					});
			        wpLink.textarea = $('body');
			        _removeLinkListeners();
			        return false;
			    });
			    $('body').on('click', '#most-recent-results ul li', function(event){
			    	var item_title = $(this).find('.item-title').text();
			    	$('#wp-link-text').val(item_title);
			    });
			    $('body').on('click', '#wp-link-cancel', function(event) {
			        function remove_empty_row() {
						$('#repeatable-fieldset-one tbody > tr:eq(-2)').remove();
						return false;
					}
					remove_empty_row();
			        _removeLinkListeners();
			        return false;
			    });
			}
			function _removeLinkListeners() {
			    if(_link_sideload){
			        if ( typeof wpActiveEditor != 'undefined') {
			            wpActiveEditor = undefined;
			        }
			    }
			    wpLink.close();
			    $('body').off('click', '#wp-link-submit');
			    $('body').off('click', '#wp-link-cancel');
			}
			/* PUBLIC ACCESSOR METHODS
			-------------------------------------------------------------- */
			return {
			    init: _init,
			};
		})(jQuery);
		// Initialize
		link_btn.init();
	});
	</script>
	<style type="text/css">
	#repeatable-fieldset-one .icons{
		font: 400 20px/1 dashicons;
		speak: none;
		display: inline-block;
		padding: 0 2px 0 0;
		top: 0;
		left: -1px;
		position: relative;
		vertical-align: top;
		-webkit-font-smoothing: antialiased;
		-moz-osx-font-smoothing: grayscale;
		text-decoration: none!important;
		cursor: move;
		cursor: grab;
		cursor: -moz-grab;
		cursor: -webkit-grab;
	}
	#repeatable-fieldset-one .icons:active{
		cursor: grabbing;
		cursor: -moz-grabbing;
		cursor: -webkit-grabbing;
	}
	#repeatable-fieldset-one a .icons{
		color: #0073aa;
	}
	#repeatable-fieldset-one a:hover .icons{
		color: #0073aa;
	}
	#repeatable-fieldset-one .icons .dashicons-menu:before{
		content: "\f333";
	}
	</style>

	<table id="repeatable-fieldset-one" width="100%">
	<thead>
		<tr>
			<th width="40%"><?php esc_html__('Article Name', 'mytheme'); ?></th>
			<th width="50%"><?php esc_html__('URL', 'mytheme'); ?></th>
			<th width="2%"></th>
			<th width="2%"></th>
		</tr>
	</thead>
		<tbody>
		<?php
		if ( $repeatable_fields ) :
			foreach ( $repeatable_fields as $field ) {
		?>
			<tr>
				<td><input type="text" class="widefat article_name" name="article_name[]" value="<?php if($field['article_name'] != '') echo esc_attr( $field['article_name'] ); ?>" /></td>

				<td><input type="text" class="widefat article_link" name="url[]" value="<?php if ($field['url'] != '') echo esc_attr( $field['url'] ); else echo 'http://'; ?>" /></td>
				<td><a class="button remove-row" href="#">-</a></td>
				<td><a class="sort"><i class="icons dashicons-menu"></i></a></td>

			</tr>
		<?php
			}
		else :
		?>
			<tr>
				<td><input type="text" class="widefat article_name" name="article_name[]" /></td>
				<td><input type="text" class="widefat article_link" name="url[]" value="http://" /></td>
				<td><a class="button remove-row" href="#">-</a></td>
				<td><a class="sort"><i class="icons dashicons-menu"></i></a></td>
			</tr>
		<?php endif; ?>
			<tr class="empty-row screen-reader-text">
				<td><input type="text" class="widefat article_name" name="article_name[]" /></td>
				<td><input type="text" class="widefat article_link" name="url[]" value="http://" /></td>
				<td><a class="button remove-row" href="#">-</a></td>
				<td><a class="sort"><i class="icons dashicons-menu"></i></a></td>
			</tr>
		</tbody>
	</table>

	<p><a id="add-row" class="button" href="#"><?php esc_html_e('Add another', 'mytheme'); ?></a>
	<p><a id="add-link" class=".link-btn button" href="#"><?php esc_html_e('Add posts', 'mytheme'); ?></a>
	</p>

	<?php
}
add_action('save_post', 'my_related_articles_meta_box_save');
function my_related_articles_meta_box_save($post_id) {
	if ( ! isset( $_POST['repeatable_meta_box_nonce'] ) ||
		! wp_verify_nonce( $_POST['repeatable_meta_box_nonce'], 'repeatable_meta_box_nonce' ) )
		return;
	if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)
		return;
	if (!current_user_can('edit_post', $post_id))
		return;
	$old = get_post_meta($post_id, 'repeatable_fields', true);
	$new = array();
	$names = $_POST['article_name'];
	$urls = $_POST['url'];
	$count = count( $names );
	for ( $i = 0; $i < $count; $i++ ) {
		if ( $names[$i] != '' ) :
			$new[$i]['article_name'] = stripslashes( strip_tags( $names[$i] ) );
		if ( $urls[$i] == 'http://' )
			$new[$i]['url'] = '';
		else
			$new[$i]['url'] = stripslashes( $urls[$i] ); // and however you want to sanitize
		endif;
	}
	if ( !empty( $new ) && $new != $old )
		update_post_meta( $post_id, 'repeatable_fields', $new );
	elseif ( empty($new) && $old )
		delete_post_meta( $post_id, 'repeatable_fields', $old );
}

Click on the code to copy all of it (in the ‘Toggle Plain Code’ mode in the upper right corner). This is a big chunk of code so let’s break it down a bit.

Meta box creation

First, you’ll create a meta box of course.

<?php
add_action('admin_init', 'add_meta_boxes', 1);
function add_meta_boxes() {
	add_meta_box( 'repeatable-fields', esc_html__('Related Articles', 'mytheme'), 'my_related_articles_meta_box_display', 'post', 'normal', 'high');
}

Next, you define your callback function for the metabox to render. In my case I’ve called it my_related_articles_meta_box_display . The ‘my_related’ is put as a function prefix. It’s a good practice to give your custom functions your unique prefix (I usually put the theme name as one), so that you don’t have accidental clashes with any plugins you may install later on. Also a good practice is to put if(!function_exists(‘my_related_articles_meta_box_display’)){}  as a wrapper around the whole function. That way if the function is accidentally defined in some hook before ‘admin_init’, you won’t have any php errors. I’ve left it out because, well to be honest, I forgot. But the function name is really unique enough so that no errors should happen :D

In the callback function we define a variable for repeater fields, and a nonce field (better safe than sorry). After that we have javascript code that will determine what happens when we click on certain buttons, and the layout of the metabox. We have two on click events for adding and removing the repeated rows

$('#add-row').on('click', function() {
	var row = $('.empty-row.screen-reader-text').clone(true);
	row.removeClass('empty-row screen-reader-text');
	row.insertBefore('#repeatable-fieldset-one tbody>tr:last');
	return false;
});
$('.remove-row').on('click', function() {
	$(this).parents('tr').remove();
	return false;
});

And one sortable event, so that you can rearrange your fields as you see fit.

$('#repeatable-fieldset-one tbody').sortable({
	opacity: 0.6,
	revert: true,
	cursor: 'move',
	handle: '.sort'
});

The javascript was added inside the php code, which may not be the best practice. It’s always better to separate your style, script and code into separate files and call them appropriately. But this will work too.

The adding link button was a bit tricky. First we look if the wp_editor  is loaded, because we need it to have the linking ability. Then we add event listeners – add link button on click. First we create a function to create an empty row. I’ve added the check to see if there is an empty field before adding one, if one is there you don’t need to add a new one; just add the value in the existing one.

_addLinkListeners() function had to be changed from the original. The default action of the add link button is to actually add the link in your content. So if you just pasted the code from the wp.stackexchange, you’ll end up having links in your input field in the metabox, and in the content as well. Tricky part is that the tinymce editor is loaded in an iframe. Luckily, this iframe is from the same source as where you are (your own blog in the backend), so you can safely fiddle with it (mainly the content that is written inside). This is why I added

var $frame = $('#content_ifr'),
$added_links = $frame.contents().find("a[data-mce-href]");
$added_links.each(function(){
	if ($(this).attr('href') === linkAtts.href) {
		$(this).remove();
	}
});

you can search for the added links, and then just remove them from the content, while leaving them in the repeater field. Neat :)

The rest is just click events if you decide to cancel or to add the link.

Meta box save

Last but not the least part is the saving of the metabox.

add_action('save_post', 'my_related_articles_meta_box_save');

function my_related_articles_meta_box_save($post_id) {
	if ( ! isset( $_POST['repeatable_meta_box_nonce'] ) ||
		! wp_verify_nonce( $_POST['repeatable_meta_box_nonce'], 'repeatable_meta_box_nonce' ) )
		return;
	if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)
		return;
	if (!current_user_can('edit_post', $post_id))
		return;
	$old = get_post_meta($post_id, 'repeatable_fields', true);
	$new = array();
	$names = $_POST['article_name'];
	$urls = $_POST['url'];
	$count = count( $names );
	for ( $i = 0; $i < $count; $i++ ) {
		if ( $names[$i] != '' ) :
			$new[$i]['article_name'] = stripslashes( strip_tags( $names[$i] ) );
		if ( $urls[$i] == 'http://' )
			$new[$i]['url'] = '';
		else
			$new[$i]['url'] = stripslashes( $urls[$i] ); // and however you want to sanitize
		endif;
	}
	if ( !empty( $new ) && $new != $old )
		update_post_meta( $post_id, 'repeatable_fields', $new );
	elseif ( empty($new) && $old )
		delete_post_meta( $post_id, 'repeatable_fields', $old );
}

First are the usual checks: nonce, if you’re not in autosave, and if you can edit the post in the first place. Then you get the post meta with repeater fields, and create an empty array where you’ll store your links and names. For every name field, you’ll add a pair of names and url, and then update post meta if your array is not empty, and not the existing one.

And that’s it. With help I found on line, that greatly shortened the coding time, and some modifications, I successfully created a new type of meta box for posts.

The look of the repeater field. You can drag and drop it, add and remove it, and add link to internal posts and pages (click to enlarge the image).

So how do you use them in the single post? I like to add a check if anything is inside the fields before outputting it in the page (so that no HTML is being generated).

<?php if (array_key_exists('repeatable_fields', $post_meta)):
    $related_articles = unserialize($post_meta['repeatable_fields'][0]);
?>

Where $post_meta = get_post_meta($post->ID); . Since they are saved as serialized array, you need to unserialize them first. Then you can output them out in a simple foreach loop:

<?php
foreach ($related_articles as $related_k => $related_v) {
	$post_id = url_to_postid($related_v['url']);
	if ($post_id != 0) {
		$comment_no = get_comments_number($post_id);
		if ($comment_no != 0) {
			echo '<a href="'.esc_url($related_v['url']).'">'.$related_v['article_name'].'<span class="comments_number">'.$comment_no.'<i class="icon-comment"></i></span></a>';
		} else{
			echo '<a href="'.esc_url($related_v['url']).'">'.$related_v['article_name'].'</a>';

		}
	} else{
		echo '<a href="'.esc_url($related_v['url']).'">'.$related_v['article_name'].'</a>';
	}
}
?>

Here I added the comment number for each post next to the icon (if there are any). Simple as that. Just be sure to close the if loop with<?php endif; ?> that you opened when you checked if there are any links in the repeater field.

I hope you liked this article. If you have any questions, ask in the comments below, and feel free to share the knowledge on :)

Join the Discussion