This website uses cookies to allow us to see how the site is used. If you continue to use this site, we assume that you are okay with this. If you want to use the sites without cookies, please see our privacy policy.

Use a custom post type to send notifications as Html emails to users by the user role

When I set WP Snippets til Christmas up this year I wanted to send an email to all the previous contributors just to notify them this was going to be a thing again for 2019.

As the previous contributors were already registered users and rather than subscribe to a third-party service I wanted to see how I could do this with WordPress core, can’t be that difficult right? So with a little bit of thought and rough testing, I started to formulate a plan.

I needed to think about all the pitfalls and what I wanted to do. First of all, I needed a custom post type but, how was I going to trigger the send? I couldn’t do it on the save_post action hook otherwise the post would get emailed every time it was updated. I would also need to be careful about sending on the save_post with multiple users too, waiting for a post to save after trying to send 300 users an email could take forever! How could I stagger the sending to avoid getting blocked or banned by the server? Would I need to set some post meta on the post as a ‘sent email notification’ marker just so I didn’t send it again? Perhaps I could use the WP Cron to schedule an event to check all Published posts that hadn’t been already been sent by email…

…so with all this in mind, I ended up doing it like so, first all I needed a custom post type but take it out of public visibility.

/**
 * Register a custom post type to handle all the emails notifications.
 * The labels and arguments here might be overkill but I used https://generatewp.com/post-type/
 * to generate the code so you get what is generated.
 * 
 * @source GenerateWP https://generatewp.com/post-type/
 * 
 */
function email_notification() {

	$labels = array(
		'name'                  => _x( 'Emails', 'Post Type General Name', 'wpstx-email-notification-with-post-type' ),
		'singular_name'         => _x( 'Email', 'Post Type Singular Name', 'wpstx-email-notification-with-post-type' ),
		'menu_name'             => __( 'Email Users', 'wpstx-email-notification-with-post-type' ),
		'name_admin_bar'        => __( 'Email', 'wpstx-email-notification-with-post-type' ),
		'archives'              => __( 'Item Archives', 'wpstx-email-notification-with-post-type' ),
		'attributes'            => __( 'Item Attributes', 'wpstx-email-notification-with-post-type' ),
		'parent_item_colon'     => __( 'Parent Item:', 'wpstx-email-notification-with-post-type' ),
		'all_items'             => __( 'All Items', 'wpstx-email-notification-with-post-type' ),
		'add_new_item'          => __( 'Add New Item', 'wpstx-email-notification-with-post-type' ),
		'add_new'               => __( 'Add New', 'wpstx-email-notification-with-post-type' ),
		'new_item'              => __( 'New Item', 'wpstx-email-notification-with-post-type' ),
		'edit_item'             => __( 'Edit Item', 'wpstx-email-notification-with-post-type' ),
		'update_item'           => __( 'Update Item', 'wpstx-email-notification-with-post-type' ),
		'view_item'             => __( 'View Item', 'wpstx-email-notification-with-post-type' ),
		'view_items'            => __( 'View Items', 'wpstx-email-notification-with-post-type' ),
		'search_items'          => __( 'Search Item', 'wpstx-email-notification-with-post-type' ),
		'not_found'             => __( 'Not found', 'wpstx-email-notification-with-post-type' ),
		'not_found_in_trash'    => __( 'Not found in Trash', 'wpstx-email-notification-with-post-type' ),
		'featured_image'        => __( 'Featured Image', 'wpstx-email-notification-with-post-type' ),
		'set_featured_image'    => __( 'Set featured image', 'wpstx-email-notification-with-post-type' ),
		'remove_featured_image' => __( 'Remove featured image', 'wpstx-email-notification-with-post-type' ),
		'use_featured_image'    => __( 'Use as featured image', 'wpstx-email-notification-with-post-type' ),
		'insert_into_item'      => __( 'Insert into item', 'wpstx-email-notification-with-post-type' ),
		'uploaded_to_this_item' => __( 'Uploaded to this item', 'wpstx-email-notification-with-post-type' ),
		'items_list'            => __( 'Items list', 'wpstx-email-notification-with-post-type' ),
		'items_list_navigation' => __( 'Items list navigation', 'wpstx-email-notification-with-post-type' ),
		'filter_items_list'     => __( 'Filter items list', 'wpstx-email-notification-with-post-type' ),
	);
	$args = array(
		'label'                 => __( 'Email', 'wpstx-email-notification-with-post-type' ),
		'description'           => __( 'Email notification system', 'wpstx-email-notification-with-post-type' ),
		'labels'                => $labels,
		'supports'              => array( 'title', 'editor' ),
		'hierarchical'          => false,
		'public'                => false,
		'show_ui'               => true,
		'show_in_menu'          => true,
		'menu_position'         => 25,
		'menu_icon'             => 'dashicons-email',
		'show_in_admin_bar'     => true,
		'show_in_nav_menus'     => false,
		'can_export'            => false,
		'has_archive'           => false,
		'exclude_from_search'   => true,
		'publicly_queryable'    => false,
		'capability_type'       => 'email_notification', // our own cap type
		'map_meta_cap'        	=> true,
		'capability'			=> array(
			'publish_posts'     => 'manage_email_notifications', // the capability to add to the role
		)
	);
	register_post_type( 'email_notification', $args );

}
add_action( 'init', 'email_notification' );

Add the capabilities to the Administrator so only admin can use this functionality, other roles can be added but to keep things simple we are only interested in Administrators for now.

/**
 * Add the capabilities to use this email system to the administrator only
 */
function wpstx_add_role_caps() {

	// The roles to manage this custom post type, other roles can been added to this array but we are only allowing this for Administrators
	$roles = array('administrator');
	
	// Loop through each role as it's an array and assign capabilities
	foreach($roles as $the_role) { 
		$role = get_role($the_role);
		$role->add_cap( 'read' );
		$role->add_cap( 'manage_email_notifications' );
		$role->add_cap( 'read_email_notification');
		$role->add_cap( 'read_private_email_notifications' );
		$role->add_cap( 'edit_email_notification' );
		$role->add_cap( 'edit_email_notifications' );
		$role->add_cap( 'edit_others_email_notifications' );
		$role->add_cap( 'edit_published_email_notifications' );
		$role->add_cap( 'publish_email_notifications' );
		$role->add_cap( 'delete_others_email_notifications' );
		$role->add_cap( 'delete_private_email_notifications' );
		$role->add_cap( 'delete_published_email_notifications' );

	}
}
add_action('admin_init','wpstx_add_role_caps');

Just for some additional and obvious user feedback, I wanted to change the “Publish” button to “Send” so here’s a very simple, fairly seasoned filter hook to do just that and only on my custom post type

/**
 * Change the button that says "Publish" to "Send" on the post type
 * "email_notifications" to make the UI look more obvious that the
 * user is about to send something.
 * 
 * @source https://developer.wordpress.org/reference/hooks/gettext/
 * 
 * @param 	string 	$translation 	Translated text
 * @param 	string 	$text 	Text to translate
 * @param 	string 	$domain 	Textdomain
 * @return 	string 	$translation 	Translated text
 * 
 */
function wpstx_change_publish_to_send( $translation, $text, $domain ) {
	if ( 'email_notification' == get_post_type() && ($text == 'Publish') ) {
		$translation = 'Send';
	}
	return $translation;
}
add_filter( 'gettext', 'wpstx_change_publish_to_send', 10, 3 );

I then needed to somehow save some post meta to the post that I could use later to find only published posts that hadn’t previously been sent, this can be done on the post_status auto-draft which is initiated as soon as a new post is created, I’ve also added a few other checks to stop the running of the function if its not needed.

/**
 * Save some post meta with the email_notification post type
 * on auto-draft or when the post if first initiated
 * 
 * @source https://developer.wordpress.org/reference/hooks/save_post/
 * 
 * @param 	int 		$post_id 	Auto generated post ID
 * @param 	object 		$post 		WP post object
 * @param 	boolean 	$update 	Whether this is an existing post being updated or not
 */
function wpstx_save_notification_status( $post_id, $post, $update = false ) {

	if ( ! current_user_can( 'edit_post', $post_id ) || $update) {
        return; // stop if the current user does not have capabilities to edit this or post is an update 
	}
	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return; // stop if the current post is auto saving
    }
	if( $post->post_type === 'email_notification' || $post->post_status === 'auto-draft' ) {
		add_post_meta($post_id, 'notification_sent', '0'); // add post meta that we will use in the wp cron later
	}

}
add_action( 'save_post', 'wpstx_save_notification_status', 10, 3 );

I also wanted to set the role for the email so that I could effectively segment different emails to different roles, for this I needed to add a custom meta box. This would require a function to add the meta box, a callback function renders the meta box and another function to save the post meta for that meta box, here’s all the code for just for that.

/**
 * Register some custom meta to select user role to email to.
 * 
 * @source https://developer.wordpress.org/reference/hooks/add_meta_boxes/
 */
function wpstx_register_meta_boxes() {
    add_meta_box( 'user-role', __( 'Role to Email', 'wpstx-email-notification-with-post-type' ), 'wpstx_my_display_callback', 'email_notification' );
}
add_action( 'add_meta_boxes', 'wpstx_register_meta_boxes' );

/**
 * Render the meta box to display in the backend.
 * We are also getting all the current WordPress roles with the global $wp_roles
 * here so we can isolate what role to set the email notification to send to
 *
 * @param	object	$post	Current post object.
 */
function wpstx_my_display_callback( $post ) {
	global $wp_roles;
	$roles = $wp_roles->roles;

	// for security when saving the data later
	wp_nonce_field( 'role_to_email', 'role_to_email_nonce' ); 
	// get the current value to check against
	$role_to_email = get_post_meta( $post->ID, '_role_to_email', true ); 

	?>
	<!-- set a label with translation just because it's good practice -->
	<label for='role_to_email'>
		<?php _e( 'Select the user role to email', 'wpstx-email-notification-with-post-type' ); ?>
	</label>
	<!-- add the name key '_role_to_email' for indexing the meta -->
	<select name='_role_to_email'>
		<!-- set a default of selected but disabled on render if the an option hasn't already been selected -->
		<option value='' selected disabled>Select Role</option>
		<?php foreach($roles as $k => $role) { ?>
			<!-- the currently selected option using the wp core function "selected" https://developer.wordpress.org/reference/functions/selected/ -->
			<option value='<?php echo esc_attr($k); ?>' <?php selected( $role_to_email, $k, true ); ?>><?php echo esc_attr($role['name']); ?></option>
		<?php } ?>

	</select>

	<?php
}


/**
 * Save meta box content.
 *
 * @param 	int 	$post_id 	Post ID
 * 
 * @source https://developer.wordpress.org/reference/hooks/save_post/
 */
function wpstx_save_meta_box( $post_id ) {
	if ( ! isset( $_POST['role_to_email_nonce'] ) ) {
        return; // stop if the nounce is not set
    }
    if ( ! wp_verify_nonce( $_POST['role_to_email_nonce'], 'role_to_email' ) ) {
        return; // stop if the nounce doesn't varify
    }
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return; // stop if auto saving the post
    }
    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return; // stop if the current user does not have capabilities to edit this or post
    }
    if ( isset( $_POST['post_type'] ) && 'email_notification' === $_POST['post_type'] ) {
		// only happens if the post type is "email_notification" 
		if(isset($_POST['_role_to_email'])) {
			// only happens if name key is set
			update_post_meta($post_id, '_role_to_email', esc_attr($_POST['_role_to_email']));
		}
    }
}
add_action( 'save_post', 'wpstx_save_meta_box', 10, 1 );

In the above code, a role is set so, I needed a helper function to get all the users for a particular role that will be needed during the sending of the email later in a WP Cron event, this function returns all the users from a particular role if the role is set.

/**
 * A simple helper function to get all users by role if the parameter is set
 * 
 * @param	string/array	$roles	Key for role set in WordPress roles
 */
function wpstx_get_users($roles = '') {
    $args = array();
    if($roles) {
        $args['role'] = $roles;
    }
    $users = get_users($args);
    return $users;
}

So next up comes the WP Cron event itself which is a fair chunk of code. I’ve heavily commented on it line by line to describe what is going on.

It describes what happens after an email notification is set to “Publish” or when “Send” is clicked in the UI for the email notification.

The next custom WP Cron event will get all published posts that do not have a custom meta “notification_sent” equal to “1” then look for all users of a particular role, then set up the email from the post content as a HTML email, send the email, wait 10 seconds and continue to the next user to send another email.

/**
 * This function takes care of all the sending of emails that
 * are hooked to our custom action hook scheduled by the wp cron
 */
function wpstx_send_emails() {

	// get all the posts that are set to published and that
	// do not have a meta key 'notification_sent' set to '1'
	// we will set this to '1' after the email notification is sent
	// this will ensure that we don't send it again
	// https://developer.wordpress.org/reference/functions/get_posts/
	$emails = get_posts(
		array(
			'post_type' => 'email_notification',
			'post_status' => 'publish',
			'meta_key' => 'notification_sent',
			'meta_value' => '1',
			'meta_compare' => '!='
		)
	);

	// $emails will return and array of objects so we'll use a
	// foreach to loop the email notifications ready to send
	foreach ($emails as $send_email) {

		// get role set for sending the email notification 
		$role = get_post_meta($send_email->ID, '_role_to_email', true);
		// if $role is set pass it to helper function 'wpstx_get_users()'
		if(isset($role) && $role != '') {
			$users = wpstx_get_users($role);
		} else {
			$users = wpstx_get_users();
		}

		// now we have all the users to send an email notification to
		// and we can use the $send_email post object to setup some parameters
		foreach ($users as $user) {

			// get the post title by ID and set the subject line for the email
			// https://developer.wordpress.org/reference/functions/get_the_title/
			$subject = get_the_title( $send_email->ID );
			// we only need the post_content of the post and we will filter it using the_content
			// https://developer.wordpress.org/reference/functions/get_post_field/
			// https://developer.wordpress.org/reference/hooks/the_content/
			$content = apply_filters('the_content', get_post_field('post_content', $send_email->ID));
			// load the $message variable with a personalised "Hey username" 
			$message = '<p>Hey ' . $user->user_login . '</p>';
			// concatinate the $content to the end of the "Hey..." $message variable
			$message .= $content;
			// as we are sending html we need to set an email header as an array
			$headers = array('Content-Type: text/html; charset=UTF-8');
			// Send the email to the user with the subject, message and headers.
			// https://developer.wordpress.org/reference/functions/wp_mail/
			wp_mail($user->user_email, $subject, $message, $headers);
			// IMPORTANT!!
			// Your host will have a limit on the amount of eamils you can send per minute/hour from your
			// server so it is super important to set a delay between each emails sent, otherwise
			// you could get your hosting account temporarily blocked on banded for spammy looking activity.
			// I'm simpy using a 10 second delay between each sending of an email, you might need to increase
			// this depending on the amount of users you have to send to
			sleep(10);

		}

		// Once the email has been sent now set the post with a post meta
		// key 'notification_sent' equal to '1' this will ensure we never
		// pick it up as published post to send again as set in the
		// WordPress function get_posts() above.
		update_post_meta($send_email->ID, 'notification_sent', '1');

	}

}
add_action( 'wpstx_check_scheduled_emails', 'wpstx_send_emails' );

/**
 * This sets the next event time for our action hook
 * "wpstx_check_scheduled_emails" that fires "wpstx_send_emails"
 */
wp_next_scheduled( 'wpstx_check_scheduled_emails' );

/**
 * This checks if the event is set or past and sets a new one if it has past
 * or at least that's the way I interpret the way it works :)
 */
if ( ! wp_next_scheduled( 'wpstx_check_scheduled_emails' ) ) {
	// now we can tap in to the WordPress cron schedule, there is a choice of
	// ‘hourly’, ‘twicedaily’, and ‘daily’
	// you can set your own custom time intervals as well 
	// https://developer.wordpress.org/plugins/cron/understanding-wp-cron-scheduling/
	// in this case we are firing the custom action hook "wpstx_check_scheduled_emails"
	// on the next hourly interval
    wp_schedule_event( time(), 'hourly', 'wpstx_check_scheduled_emails' );
}

The benefit of doing it this way free’s up the backend processing time and user’s interaction by pushing all the heavy work to a background cron event, you don’t have to click a button and wait for the process to finish which could be a while, for instance, if you had 1000 users with a 10 second delay between sending each email 🙂

And here is a final job to remove the schedule, this is just good practice if it is a plugin for instance and you wanted to remove the event after deactivation of the plugin.

/**
 * Setup a deactivation hook to remove this cron if this plugin is disabled
 * 
 * @source https://developer.wordpress.org/reference/functions/register_deactivation_hook/
 */
function wpstx_deactivate_email_notification_cron() {
	// retrieve the event
	$timestamp = wp_next_scheduled( 'wpstx_check_scheduled_emails' );
	// unset the event
	wp_unschedule_event( $timestamp, 'wpstx_check_scheduled_emails' );
}
register_deactivation_hook( __FILE__, 'wpstx_deactivate_email_notification_cron' );

That’s how I set up a custom post type as an Email Notification system using the WP Cron.

@github https://github.com/eirichmond/wpstx-email-notification-with-post-type

1

How to Disable All Unwanted Image Sizes

This year, give your site the gift that keeps on giving: better optimized image handling. As of version 5.3, WordPress automatically generates seven (7!) additional images for every image that you upload via the WP Media Library (and/or RTE/Visual editor). So in addition to the original image on the server, WordPress generates images with the following dimensions:

Continue reading “How to Disable All Unwanted Image Sizes”

2

Restricting Access to the WordPress Dashboard By User Role

I see this a lot with people using the WP User Manager plugin to turn their WordPress site into a community or membership site. Users register for the site from the front end form, they are given a certain role (eg. subscriber), they then can manage their own profile from the front end of the site, but then also have access to the WordPress dashboard (via /wp-admin).

This isn’t the best experience for users who shouldn’t be seeing any form of wp-admin (even if they can’t do much in it). Here’s how you can lock down the dashboard to only administrators:

/**
 * Only allow access to the wp-admin dashboard for users with the manage_options capability (administrators).
 * Customize the capability as needed https://wordpress.org/support/article/roles-and-capabilities/
 */
function wpum_restrict_wp_admin_access() {
	if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
		// Don't hijack AJAX requests
		return;
	}

	if ( ! is_admin() ) {
		// We aren't in the admin
		return;
	}

	if ( current_user_can( 'manage_options' ) ) {
		// User has the correct role
		return;
	}

	// Redirect to the homepage. Customize as needed
	wp_safe_redirect( home_url() );
	exit;
}

add_action( 'init', 'wpum_restrict_wp_admin_access' );

The admin bar will still appear for users on the front end of the site, which WP User Manager has a setting to disable it, but you can do this manually with the following:

/**
 * Only show the wp-admin bar for users with the manage_options capability (administrators).
 * Customize the capability as needed https://wordpress.org/support/article/roles-and-capabilities/
 *
 * @param bool $show_admin_bar
 *
 * @return bool
 */
function wpum_hide_admin_bar( $show_admin_bar ) {
	if ( ! current_user_can( 'manage_options' ) ) {
		return false;
	}

	return $show_admin_bar;
}

add_filter( 'show_admin_bar', 'wpum_hide_admin_bar' );
3

Get Gutenberg reusable blocks outside the Block Editor

Did you know that Gutenberg Reusable Blocks are stored as a bundled Post Type in WordPress database? Yep they are. You can even access them on the Reusable Blocks hidden admin screen available in example.com/wp-admin/edit.php?post_type=wp_block.

Caption: Reusable Blocks admin screen

If Reusable Blocks are registered as a post type, then they are queryable as well as any post type in WordPress.

So we could easily get them outside of the editor…

Yeah! Looks great, let’s build some cool stuff!

Introducing get_reusable_block PHP function

This function will only get the formatted HTML content of a given Reusable block ID and return it.

Note: I’m prefixing the function with advent_ for the sake of WordPress Coding Standards 😎

function advent_get_reusable_block( $block_id = '' ) {
	if ( empty( $block_id ) || (int) $block_id !== $block_id ) {
		return;
	}
	$content = get_post_field( 'post_content', $block_id );
	return apply_filters( 'the_content', $content );
}

So we can easily get any reusable block content by providing its ID. The following snippet will use get_reusable_block() function to display this block outside the editor (in your footer for example):

echo advent_get_reusable_block( 57 );

Example of advanced use: build a Reusable block shortcode

It’s quite easy to build a custom shortcode using our previous get_reusable_block function:

function advent_reusable_block_shortcode( $atts ) {
	extract( shortcode_atts( array(
		'id' => '',
	), $atts ) );
	if ( empty( $id ) || (int) $id !== $id ) {
		return;
	}
	$content = advent_get_reusable_block( $id );
	return $content;
}
add_shortcode( 'reusable', 'advent_reusable_block_shortcode' );

We now have a brand new shortcode, which can be easily used to display reusable block even in places where the block editor is not available!

[reusable id="57"]

Use cases for this shortcode are various:

  • Custom Post Types without Block Editor enabled (so they are in Classic Editor mode)
  • Text Widget (so reusable blocks can be added in your footer or in your sidebar)
  • Advanced Custom Field (ACF) WYSIWYG editor field
  • Gravity form confirmation message (the message that is displayed to users after filling the form)
  • And even on… websites with Classic Editor plugin! So you can use Gutenberg Reusable Blocks without having it in any of your Custom Post Types, lol 🤪

Here is a plugin that is using this snippet. It also provide a Reusable Block Widget and in provides an extended admin interface to manage and preview reusable blocks:

Merry Christmas, lovely WordPress people! 🎁

4

Automatically Add Child Pages to Menus

To create a menu in WordPress, you have to create your pages, and then hop over to Appearance->Menus to set up your menu hierarchy. I’ve always wondered why menus are not intrinsically tied to page hierarchies. There is a ‘Parent Page’ setting on the Page Edit screen, so why not use it for your menu hierarchy? If the parent page is in your menu, child pages could be automatically added to the menu. Doing so has the added bonus of setting your hierarchy for things like Yoast breadcrumbs and menus in the same place so they always match, and changes don’t need to be made in two places.

To automatically add child pages to a menu, we can use the filter hook: nav_menu_item_args. The filter is called for each top level page added to the menu you are displaying with WordPress’ wp_nav_menu() function. We’ll check each top level page to see if it has children, get the children with wp_list_pages(), and add them to the unordered list.

if ( ! function_exists( 'aacpm_add_sub_pages_toggles_to_main_menu' ) ) {
    function aacpm_add_sub_pages_toggles_to_main_menu( $args, $item, $depth ) {
        
        //You could check the $args->theme_location to determine if child pages should be automatically added to this menu or not.
        // if ( $args->theme_location == 'primary' ) {
            $children = null;
            
            $args2 = array('depth'   => 1, //only 1 level of children
                'child_of'           => $item->object_id, // Note $object->ID is menu id and not post id
                'echo'               => 0,
                'title_li'           => '',
                'sort_column' => 'menu_order' //you'll need https://wordpress.org/plugins/simple-page-ordering/ to order your child pages
            );
            $children = wp_list_pages($args2);                            

            if ( $children ) {
                //You might want to add additional markup to your menus to add things like toggles to open/close submenus, etc. Add with $args->before or $args->after                

                //Child pages are added to the menu with this line
                $args->after = '<ul class="sub-menu">' . $children . '</ul>';
            }  
        //} // end theme_location check
        return $args;
    }

    //add function to the nav_menu_item_args filter
    add_filter( 'nav_menu_item_args', 'aacpm_add_sub_pages_toggles_to_main_menu', 999, 3 );

}

In the wp_list_pages() $args2, I used ‘sort_column’ => ‘menu_order’. To set the menu_order, you’ll need a plugin. I use the ‘Simple Page Ordering’ plugin: https://wordpress.org/plugins/simple-page-ordering/ The ‘Simple Page Ordering’ plugin adds the ability to reorder your pages by drag and drop in the Admin Pages List (/wp-admin/edit.php?post_type=page). Now your editors can set the menu hierarchy and order sub pages without hopping over to Appearance -> Menus.

Using the code above, the CSS classes on the child list items are different from the default Appearance-> Menus submenu classes. For example, active child menus get the class ‘current_page_item’ only instead of both ‘current-menu-item current_page_item’. You might need to edit your CSS to highlight active child pages in the menu.

If you want to add a class to the parent list item to match the ‘menu-item-has-children’ class added with the default Appearance->Menus, hook into the nav_menu_css_class filter:

if ( ! function_exists( 'aacpm_add_parent_class_to_menu' ) ) {

    function aacpm_add_parent_class_to_menu($classes, $item, $args, $depth){
        if($depth != 1 ){ //if depth == 1 don't display child pages
            $children = get_pages( array( 'child_of' => $item->object_id ) );
            if($children){
                $classes[] = 'menu-item-has-children';
            }
        }
        return $classes;
    }

    add_filter( 'nav_menu_css_class', 'aacpm_add_parent_class_to_menu', 999, 4);
}

You can add this code to your functions.php or create a plugin. I put together a small plugin you can download here. The plugin also adds the ability for Editors to access the Appearance menu ( edit_theme_options ) so they can change/reorder the top level menu pages, edit widgets, and change theme settings. Changing menus and editing widget content are things I usually want Editors to be able to do, but I don’t want to give them Administrator access to change/add plugins or site settings.

With this code, your Editors only need to set the top level pages in their Menu. Child pages (pages with a ‘Parent Page’ set to a top level page in the Menu) will automatically be added to their menus.

5

WordPress Speed Improvement Snippets

WordPress out of the box is quite an efficient piece of software, however with recent developments it can be more bloated than it could be. Since going freelance, about 40% of my work has been improving the speed of WordPress for sites. Most of that has been removing bloat.

There are plenty of plugins out there that do a lot of work to improve speed. However I’ve a light, one file plugin that removes some of the standard functionality. This file includes the following:-

  • Switches off emoji support.
  • Removes the “generator” line in the head.
  • Removes oEmbeds.
  • Removes feed creation.

This plugin is installed on client sites, and is designed to be simple and lightweight, and do a few small jobs well.

<?php
/*
	Plugin Name: Dwi'n Rhys Speed Improvement Plugin
	Plugin URI: https://dwinrhys.com/
	Description: Plugin used to do a bunch of speed changes. Will be documented at a later point.
	Version: 0.1
	Author: Dwi'n Rhys
	Author URI: https://dwinrhys.com/
	License: GPL v3

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

/**
 * Switch off emoji scripts from loading.
 *
 * To disable this, add define('DWINRHYS_ENABLE_EMOJIS', 1 ); to your wp-config.php file
 */
if ( ! defined( 'DWINRHYS_ENABLE_EMOJIS' ) ) {
	remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
	remove_action( 'wp_print_styles', 'print_emoji_styles' );
}

/**
 * Remove the WP Generator
 * 
 * To disable this, add define('DWINRHYS_ENABLE_WPGENERATOR', 1 ); to your wp-config.php file
 */
if ( ! defined( 'DWINRHYS_ENABLE_WPGENERATOR' ) ) {
	remove_action( 'wp_head', 'wp_generator' );
}

/**
 * Switch off embeds from loading.
 * 
 * To disable this, add define('DWINRHYS_ENABLE_EMBEDS', 1 ); to your wp-config.php file
 */
if ( ! defined( 'DWINRHYS_ENABLE_EMBEDS' ) ) {
	add_action( 'wp_footer', 'dwinrhys_dequeue_embeds' );
}

/**
 * Switch off RSS Feeds
 * 
 * To disable this, add define('DWINRHYS_ENABLE_FEEDS', 1 ); to your wp-config.php file
 */
if ( ! defined( 'DWINRHYS_ENABLE_FEEDS' ) ) {
	remove_action( 'wp_head', 'feed_links', 2 );
	remove_action( 'wp_head', 'feed_links_extra', 3 );
}

/**
 * Function to disable embeds
 *
 * @return void
 */
function dwinrhys_dequeue_embeds() {
	wp_dequeue_script( 'wp-embed' );
}

Re-enabling Certain Features

Now, not every site will need these features disabling, and especially with clients you may want to leave a few of these features on. To do so, you can add the following lines to your wp-config.php file.

define('DWINRHYS_ENABLE_EMOJIS', 1 ); // Enable Emojis
define('DWINRHYS_ENABLE_WPGENERATOR', 1 ); // Enable Generator Line
define('DWINRHYS_ENABLE_EMBEDS', 1 ); // Enable Embeds
define('DWINRHYS_ENABLE_FEEDS', 1 ); // Enable Feeds.
6

Add custom meta column in the Users list

This is a very simple way to add a custom column to display in your user’s list. Say you want to show some preference that’s been added to the user’s profile with add_user_meta

First of all, let’s register the extra column

/**
 * The function hooked to the filter hook manage_users_columns that simply
 * adds a new column by key and value to the existing core columns for the users list
 * 
 * @author  Elliott Richmond <me@elliottrichmond.co.uk>
 * @since 0.1.0
 * @param array   $columns    Existing columns array.
 * @return string $columns    The new columns array.
 * 
 */
function wpstx_custom_user_columns($columns) {
    $columns['select_role'] = 'Preference';
    return $columns;
}
add_filter('manage_users_columns', 'wpstx_custom_user_columns', 10, 1);

Next, we’ll add the data that you want to show when that row is rendered, if the column name is equal to the key we provided above in the manage_users_colums hook we’ll use get_user_meta to get the data we want to show by the users ID and return the value.

/**
 * The function hooked to the filter hook manage_users_custom_column that displays
 * the information we want to see by the in individual user by ID or row listed
 * 
 * @author  Elliott Richmond <me@elliottrichmond.co.uk>
 * @since 0.1.0
 * @param string   $val    		   Existing columns array.
 * @param string   $column_name    The key value of the column that we will switch to set in our function 'wpstx_custom_user_columns'.
 * @param int      $user_id    	   The users ID used to identify the row the list is going to output.
 * 
 * @return string  $val    		   The existing or new string we want to display in the users list.
 * 
 */
function wpstx_custom_user_row( $val, $column_name, $user_id ) {
    switch ($column_name) {
		case 'select_role' :
			$meta_key = get_user_meta($user_id, 'select_role', true);
			$val = wpstx_select_role_output($meta_key);
			return $val;
        default:
    }
    return $val;
}
add_filter( 'manage_users_custom_column', 'wpstx_custom_user_row', 10, 3 );

You’ll notice I’m also using a function on the meta value, this is just a simple helper function to change the output by the key saved in the user’s meta, rather than use my function you could simply return $meta_key instead.

/**
 * This is an extra function to change a lowercase meta key
 * from the standard WordPress way of storing keys and changing
 * them to our prefered value as defined by the key value
 * 
 * @author  Elliott Richmond <me@elliottrichmond.co.uk>
 * @since 0.1.0
 * @param string  $meta_key    The incoming meta key.
 * @return string $preference  The output text.
 * 
 */
function wpstx_select_role_output($meta_key) {
	// die early if empty
	if ($meta_key == '') {
		return;
	}
	$output = array(
		'subscriber' => 'Recieve snippets',
		'contributor' => 'Submit snippets',
	);
	$preference = $output[$meta_key];
	return $preference;
}

@source https://github.com/eirichmond/wpstx-user-columns.git

7

Create a specific menu for logged in users

Here’s a way to add a specific menu for logged in users, this is useful if you’re building a membership site, where users need to login first but no longer need to see the login link after login and the menu can then be replaced with a users Account, Settings or Preferences menu.

So, first of all, we will need 2 menu locations.

One for logged out users to login, and one for users after they have logged in.

// Register Navigation Menus
function wpstx_logged_in_user_menu() {

	$locations = array(
		'logged-in-menu' => __( 'User Menu', 'wpstx_logged_in_user_menu' ),
		'logged-out-menu' => __( 'Login', 'wpstx_logged_in_user_menu' ),
	);
	register_nav_menus( $locations );

}
add_action( 'init', 'wpstx_logged_in_user_menu' );

Next, we need to create an action hook that renders the menu conditionally depending if the user is logged in or not. Here we will use PHP Ternary operator logic on the built-in WordPress function is_user_logged_in() to render the one menu or the other.

// Function with a conditional for logged in/out users
function wpstx_users_menu() {
    wp_nav_menu(
        array(
            'theme_location' => is_user_logged_in() ? 'logged-in-menu' : 'logged-out-menu'
        )
    );
}
// create an action hook for uses in your theme
// to render the menu simply add "do_action('wpstx_users_menu');" anywhere in your theme
add_action('wpstx_users_menu','wpstx_users_menu');

This action hook can then be used in your theme’s header/footer or wherever you want to put it to conditionally render the function...

// add this to one of your template files
do_action('wpstx_users_menu');

With this snippet running all you need to do now is go to:
Dashboard > Appearance > Menus
to create a couple of menus and allocate them to the relevant menu locations.

Create a menu for non-logged in users
Create a menu for logged in users
How the menu looks to users who are not logged in
How the menu looks to users who are logged in

@source https://github.com/eirichmond/wpstx-specific-menu-for-loggedin-users

8

Using cURL to Determine the Status Code of a URL

Whenever we’re working with remote requests within WordPress, we’re likely using one of the functions that are available through the core API. These are functions such as:

And they’re great and I recommend using them.

Depending on the project, you may need to determine the HTTP status of the page before making the request. And if you want to do that, I recommend two things:

  1. HTTP Status Codes
  2. The code below.

For example, the following code determines the status code of a URL using cURL:

<?php
/**
 * Determines if a specific URL returns a valid page. This is experimental and it is based on
 * the status code.
 *
 * @param string $url the url to evaluate
 *
 * @return bool true if the URL returns a status code of 404; otherwise, false
 */
public function isValidUrl(string $url): bool
{
	$curl = curl_init($url);
	curl_setopt($curl, CURLOPT_URL, $url);
	curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
	$response = curl_exec($curl);
  
	$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
  	curl_close($curl);
  
  	return (404 !== $httpCode);
}

@source: https://gist.github.com/tommcfarlin/2ad96af9aa3007807686ac87b630dcd7#file-00-is-valid-url-php

Of course, you can replace 404 with any value from the above page to evaluate it to any given status you need.

Regardless, before making a request to a given URL, this particular function can help you determine the course of action you need to take in your project before actually making a request and have it fail.

9

Shortcode for Copyright, Credit and Login Access Sitewide

Once a site is launched, one of the most fundamental tools a client needs is easy access to login and logout of the site. Telling them where the WP login page is located is not something they will typically remember. It’s become quite a lifesaver to put a small footer link on every page that allows them to login, logout, or get back to the dashboard. Also, its a great place to provide attribution back to the site designer, if that’s something you want to do. Couple that with an automatically updating copyright statement and we have a very useful footer snippet!

Use this handy shortcode to display a short line or two of text at the bottom of every page showing your site’s copyright (automatically updated to this year) and a simple login/logout link. It also optionally will provide credit for the site’s designer.

Here’s an Example

Logged out View of Shortcode
Logged In View of Shortcode

Using the Shortcode

  1. Copy the following into your functions.php file.
  2. Update the default settings to be your own site designer information.
  3. Then place the shortcode [credits] into the footer of your site.
<pre class="wp-block-code"><code>// Copyright/Credit/Access Shortcode
// Usage: [credits oscredit="false" title="" link="" link_title="" linebreak="true"]
// All parameters optional. Defaults set below.

add_shortcode('credits', 'opensky_credits_function');
function opensky_credits_function( $atts, $content = null ) {
   extract( shortcode_atts( array(
      'oscredit' => 'true', //set true|false to show or hide designer's credit
      'title' => 'Open Sky Web Studio', // designer's name
      'link' => 'https://www.openskywebstudio.com', // link to designer's website
      'link_title' => 'Open Sky Web Studio | Clean, Effective Websites', // designer's link title for seo
      'linebreak' => 'true', // true|false to display on one line or two
      ), $atts ) );

	$creds = "<span id='credits'>Copyright &copy; " . date('Y') . " <a href='".get_option('home')."' title='".get_bloginfo('name')." | ".get_bloginfo('description')."'>".get_bloginfo('name')."</a>. All Rights Reserved. ";
	
	if (strtolower($oscredit) == "false")
		$creds .= "<!--";

	if (strtolower($linebreak) == "true")
		$creds .= "<br/>";
	
	$creds .= "Site Design &bull; <a href='".$link."' title='".$link_title."' target='_blank'>".$title."</a> &bull; ";

	if (strtolower($oscredit) == "false")
		$creds .= "-->";
	$creds .= wp_loginout('', false) . wp_register(' &bull; ', '', false) . '</span>';

	return $creds;
}</code></pre>
10
Shortcode for Copyright, Credit and Login Access Sitewide
11
Shortcode for Copyright, Credit and Login Access Sitewide
12
Shortcode for Copyright, Credit and Login Access Sitewide
13
Shortcode for Copyright, Credit and Login Access Sitewide
14
Shortcode for Copyright, Credit and Login Access Sitewide
15
Shortcode for Copyright, Credit and Login Access Sitewide
16
Shortcode for Copyright, Credit and Login Access Sitewide
17
Shortcode for Copyright, Credit and Login Access Sitewide
18
Shortcode for Copyright, Credit and Login Access Sitewide
19
Shortcode for Copyright, Credit and Login Access Sitewide
20
Shortcode for Copyright, Credit and Login Access Sitewide
21
Shortcode for Copyright, Credit and Login Access Sitewide
22
Shortcode for Copyright, Credit and Login Access Sitewide
23
Shortcode for Copyright, Credit and Login Access Sitewide
24