Display Posts Shortcode – Full Content

The Display Posts Shortcode plugin lets you display a list of posts based on any criteria. By default it displays titles only. The example below makes it display the post’s title and full content.

  • Custom importer for WP Recipe Maker

    I frequently work with food bloggers to improve the design, speed, and functionality of their websites. This often includes migrating to WP Recipe Maker, the industry-leading recipe card plugin.

    WP Recipe Maker already includes importers for many popular recipe plugins. You can skip to the end to see how to import your recipes if you are using BigOven, Cookbook, Cooked, Create, EasyRecipe, FoodiePress, Meal Planner Pro, Purr Design, Recipe Card, Simmer, Simple Recipe Pro, Tasty, WordPress.com, WP Ultimate Recipe, Yummly, or ZipList.

    Sometimes during our discovery phase we’ll find the previous developer built their own custom recipe card directly in the theme. In the process of redesigning the site we’ll need build a custom importer and migrate all the recipes to WP Recipe Maker.

    Switching to WP Recipe Maker lets us take advantage of all the modern features it provides (schema for SEO, advanced templating, user ratings…), and stay up-to-date with future changes required for SEO.

    Here’s a walkthrough of how we import the recipes:

    1. Review the recipe data structure
    2. Build the custom importer
    3. Import recipes

    Review the recipe data structure

    The first step is to figure out how the current plugin is storing the recipe data. Most custom built recipe cards will store it as post meta, but it could be stored in a separate post type or custom database tables.

    A quick tip for inspecting post meta is to drop this code in functions.php in my local development environment:

    /**
     * Display all post meta
     *
     */
    add_action( 'wp_footer', function() {
    	if( is_single() )
    		ea_pp( get_post_meta( get_the_ID() ) );
    });
    
    /**
     * Pretty Printing
     *
     */
    function ea_pp( $obj, $label = '' ) {
    	$data = json_encode( print_r( $obj,true ) );
    	?>
    	<style type="text/css">
    		#bsdLogger {
    		position: absolute;
    		top: 30px;
    		right: 0px;
    		border-left: 4px solid #bbb;
    		padding: 6px;
    		background: white;
    		color: #444;
    		z-index: 999;
    		font-size: 1.25em;
    		width: 400px;
    		height: 800px;
    		overflow: scroll;
    		}
    	</style>
    	<script type="text/javascript">
    		var doStuff = function(){
    			var obj = <?php echo $data; ?>;
    			var logger = document.getElementById('bsdLogger');
    			if (!logger) {
    				logger = document.createElement('div');
    				logger.id = 'bsdLogger';
    				document.body.appendChild(logger);
    			}
    			////console.log(obj);
    			var pre = document.createElement('pre');
    			var h2 = document.createElement('h2');
    			pre.innerHTML = obj;
    			h2.innerHTML = '<?php echo addslashes($label); ?>';
    			logger.appendChild(h2);
    			logger.appendChild(pre);
    		};
    		window.addEventListener ("DOMContentLoaded", doStuff, false);
    	</script>
    	<?php
    }
    

    When viewing a single post, there will be a column added to the right side of the screen that lists all of its metadata:

    It’s also a good idea to dig into the theme/plugin to see how it’s accessing and displaying the recipe data. Simple fields like Prep Time and Cook Time are usually straightforward, but more complicated fields like Ingredients and Directions may require more work to assemble.

    The site I’m working on right now is storing ingredients and instructions as serialized arrays.

    $ingredients_data = maybe_unserialize( get_post_meta( $id, '_recipe_ingredients', true ) );

    Write down all the data you’ll need to access and how it is stored. I’ll usually create a simple spreadsheet with Field Type (“ingredients”), Field Meta Key (_recipe_incredients), and Notes (“stored as serialized array”).

    Build the custom importer

    Now that you know how the current data is stored, you can get to work writing the importer.

    You can find all of WP Recipe Maker’s current importers and their example importer in /plugins/wp-recipe-maker/includes/import/.

    To create your own custom importer, create a file in that directory named class-wprm-import-{something}.php, where {something} is your unique importer name. I’ll usually use the client’s name for this, so in the custom importer I’m building for The Mom 100 it’s called class-wprm-import-themom100.php.

    Note: If you update WP Recipe Maker, you’ll lose any custom importers you have added to it. I recommend storing a copy of your custom importer in your core functionality plugin or theme. It won’t actually be loaded from there (WPRM only looks at the /import/ directory in its plugin), but it’s a good idea to store a copy where it won’t be lost.

    Copy the contents of the class-wprm-import-example.php file into your new file, then rename the class from WPRM_Import_Example to WPRM_Import_{Something}, changing {Something} to match what you used in the file name.

    Update the get_uid() method to use your slug (ex: themom100), and the get_name() method to use your name (ex: The Mom 100).

    Get Recipes

    The next step is to update the get_recipe_count() and get_recipes() methods used to find the recipes on the site.

    On this project, recipes are every post in the recipe post type. If your recipes are mixed in with posts, you would write a query that searches for posts containing a meta key only used by recipes.

    It’s also important to eliminate recipes you have already imported. After I finish importing a recipe, I’m going to run update_post_meta( $post_id, '_recipe_imported', 1 ); to indicate this one has been imported. In my query below, I’m looking for recipes that do not have that key because we only want to list recipes that haven’t been imported yet.

    For get_recipe_count() you want to find the total recipe count, so set posts_per_page arbitrarily high and only query for IDs.

    	/**
    	 * Get the total number of recipes to import.
    	 *
    	 * @since    1.20.0
    	 */
    	public function get_recipe_count() {
    
    		$loop = new WP_Query( array(
    			'fields' => 'ids',
    			'post_type' => 'recipe',
    			'posts_per_page' => 999,
    			'meta_query' => array(
    				array(
    					'key' => '_recipe_imported',
    					'value' => 1,
    					'compare' => 'NOT EXISTS',
    				)
    			)
    		) );
    
    		return $loop->found_posts;
    
    	}
    

    For the get_recipes() method, we’re getting a paginated list of recipes to import. You want to keep the list to a reasonable size (ex: 20) so it doesn’t time out if someone tries importing all the recipes in the list at once. You’ll also want to use the $page parameter so that clicking the pagination links actually changes the posts shown.

    Use this loop to build the $recipes array that includes the post ID, post title, and edit post link.

    	/**
    	 * Get a list of recipes that are available to import.
    	 *
    	 * @since    1.20.0
    	 * @param	 int $page Page of recipes to get.
    	 */
    	public function get_recipes( $page = 0 ) {
    		// Return an array of recipes to be imported with name and edit URL.
    		// If not the same number of recipes as in "get_recipe_count" are returned pagination will be used.
    
    		$loop = new WP_Query( array(
    			'post_type' => 'recipe',
    			'posts_per_page' => 20,
    			'paged' => $page,
    			'meta_query' => array(
    				array(
    					'key' => '_recipe_imported',
    					'value' => 1,
    					'compare' => 'NOT EXISTS',
    				)
    			)
    		) );
    
    		$recipes = array();
    		foreach( $loop->posts as $post ) {
    			$recipes[ $post->ID ] = array(
    				'name' => $post->post_title,
    				'url'  => get_edit_post_link( $post->ID )
    			);
    		}
    		return $recipes;
    	}

    Get Recipe

    The get_recipe() method is where all the hard work happens. We’re going to take the recipe data we mapped out earlier to build the $recipe array used by WP Recipe Maker to generate its recipe.

    The first parameter is $id which is the Post ID. Use this to access any necessary post data. Don’t assume the post data is already set up. Instead of using get_the_title(), use get_the_title( $id ).

    The example file will already contain all the necessary fields for the recipe. Go through and replace them with how your current recipe data is stored. Here’s how the simple fields looked in my importer:

    $recipe['name'] = get_the_title( $id );
    $recipe['summary'] = get_post_meta( $id, '_recipe_subtitle', true );
    $recipe['author_name'] = get_the_author( $id );
    $recipe['image_id'] = get_post_thumbnail_id( $id );
    $recipe['servings'] = get_post_meta( $id, '_r_yield', true );
    $recipe['servings_unit'] = 'Servings';
    $recipe['prep_time'] = get_post_meta( $id, '_r_prep_time', true );
    $recipe['cook_time'] = get_post_meta( $id, '_r_grill_time', true );
    $recipe['total_time'] = $recipe['prep_time'] + $recipe['cook_time'];

    Ingredients and Instructions use arrays of groups, each group containing an optional name (ex: “For the Sauce”) and an array of ingredients/instructions. For each ingredient you can specify array( 'raw' => $ingredient ); and WP Recipe Maker will handle processing that raw data (ex: “1 tsp salt”) into its components (“1” count, “tsp” unit, and “salt” ingredient).

    Here’s what my ingredients and instructions looked like:

    $ingredients_data = maybe_unserialize( get_post_meta( $id, '_recipe_ingredients', true ) );
    if( !empty( $ingredients_data ) ) {
    	foreach( $ingredients_data as $data_group ) {
    		$group = array();
    		if( !empty( $data_group['title'] ) )
    			$group['name'] = wp_strip_all_tags( $data_group['title'] );
    		for( $i = 0; $i < count( $data_group['nums'] ); $i++ ) {
    			$ingredient = '';
    			if( !empty( $data_group['nums'][ $i ] ) )
    				$ingredient .= $data_group['nums'][ $i ];
    			if( !empty( $data_group['types'][ $i ] ) && 'no type' !== $data_group['types'][ $i ] )
    				$ingredient .= ' ' . $data_group['types'][ $i ];
    			if( !empty( $data_group['names'][ $i ] ) )
    				$ingredient .= ' ' . $data_group['names'][ $i ];
    			if( !empty( $ingredient ) )
    				$group['ingredients'][] = array( 'raw' => $ingredient );
    		}
    		if( !empty( $group ) )
    			$recipe['ingredients'][] = $group;
    	}
    }
    
    $instructions_data = maybe_unserialize( get_post_meta( $id, '_recipe_steps_data', true ) );
    
    $instructions = array();
    foreach( $instructions_data as $item ) {
    	$instructions[] = array(
    		'text' => $item['content'],
    	);
    }
    
    // Instructions have to follow this array structure consisting of groups first.
    $recipe['instructions'] = array(
    	array(
    		'name' => '', // Group names can be empty.
    		'instructions' => $instructions
    	)
    );

    Replace Recipe

    The replace_recipe() method lets you run additional code after the recipe has been imported. If your previous recipe plugin used a shortcode, use this to find/replace it with the WP Recipe Maker shortcode.

    In my case the previous theme hardcoded the recipe below the post content, so I’m appending the WP Recipe Maker shortcode to the end of the post content.

    I also use this method to mark the post as imported, using my custom field _recipe_imported, and to set the parent post for the recipe, using the custom field wprm_parent_post_id.

    	/**
    	 * Replace the original recipe with the newly imported WPRM one.
    	 *
    	 * @since    1.20.0
    	 * @param	 mixed $id ID of the recipe we want replace.
    	 * @param	 mixed $wprm_id ID of the WPRM recipe to replace with.
    	 * @param	 array $post_data POST data passed along when submitting the form.
    	 */
    	public function replace_recipe( $id, $wprm_id, $post_data ) {
    		// The recipe with ID $id has been imported and we now have a WPRM recipe with ID $wprm_id (can be the same ID).
    		// $post_data will contain any input fields set in the "get_settings_html" function.
    		// Use this function to do anything after the import, like replacing shortcodes.
    
    		// Mark as migrated so it isn't re-imported
    		update_post_meta( $id, '_recipe_imported', 1 );
    
    		// Set parent post that contains recipe
    		update_post_meta( $wprm_id, 'wprm_parent_post_id', $id );
    
    		// Add the WPRM shortcode
    		$post = get_post( $id );
    		$content = $post->post_content . ' [wprm-recipe id="' . $wprm_id . '"]';
    		wp_update_post( array( 'ID' => $id, 'post_content' => $content ) );
    
    	}
    

    That’s it! The importer is built and ready for testing. You’ll want to import a few recipes to ensure everything is working as expected. If you run into any issues, I recommend looking through the existing importers for guidance / ideas.

    If your current recipe plugin stores the entire ingredient list as an HTML bulleted list, check out the parse_recipe_component_list() method in class-wprm-import-purr.php for an example of parsing the HTML into individual ingredients.

    Importing recipes

    Once you have a WP Recipe Maker importer for your current recipe plugin, the process of importing recipes is very straightforward.

    In the WordPress backend go to WP Recipe Maker > Import Recipes. You should see a section with your recipe plugin’s name and the number of recipes it found. Click “Explore Import Options”

    On the next screen you’ll see a list of recipes you can import. Select all the ones you’d like to import, then click “Import Selected Recipes”.

    I recommend you import one recipe at a time, then view the post to make sure all the recipe data made it over.

  • SearchWP Metrics with Live Search

    SearchWP greatly improves the standard WordPress site search (more information). You can also install the SearchWP Metrics extension for analytics on popular search terms, overall search traffic, search queries with no results, and more.

    SWP Metrics collects this data by adding URL variables to the permalinks on the search results page. As an example, click the search icon in my site’s menu and do a search, then look at the URLs returned.

    If you are using the Live Ajax Search plugin to display search results as the user types a query, these won’t be tracked and could represent a good percentage of your searches.

    You can customize the markup for the live search by creating a directory and file in your theme, searchwp-live-ajax-search/search-results.php.

    Add the following to the file to track live searches.

    <?php if ( have_posts() ) :
    	do_action( 'searchwp_metrics_click_tracking_start' );
    
    	?>
    	<?php while ( have_posts() ) : the_post(); ?>
    		<?php $post_type = get_post_type_object( get_post_type() ); ?>
    		<div class="searchwp-live-search-result" role="option" id="" aria-selected="false">
    			<p><a href="<?php echo esc_url( get_permalink() ); ?>">
    				<?php the_title(); ?> (<?php echo esc_html( $post_type->labels->singular_name ); ?>) »
    			</a></p>
    		</div>
    	<?php endwhile; ?>
    <?php else : ?>
    	<p class="searchwp-live-search-no-results" role="option">
    		<em><?php esc_html_x( 'No results found.', 'swplas' ); ?></em>
    	</p>
    <?php
    	do_action( 'searchwp_metrics_click_tracking_stop' );
    endif; ?>

    The original template was copied from /wp-content/plugins/searchwp-live-ajax-search/templates/search-results.php.

    I added do_action( 'searchwp_metrics_click_tracking_start' ); to the start, and do_action( 'searchwp_metrics_click_tracking_stop' ); to the end. This tells SWP Metrics to add the URL variables to the links found in here.

  • Include recipe rating on archive pages

    Star ratings are a great way to encourage your visitors to read your recipes and share their own experience. WP Recipe Maker makes it easy to let readers add their rating.

    Use the [wprm-recipe-rating] shortcode to display a recipe’s rating. If you want to build it into your theme, edit the single.php file and add this where you’d like the star rating to appear:

    echo do_shortcode( '[wprm-recipe-rating]' );

    This works great on single posts because the shortcode will find the first recipe in the first post on the page. But if you call this shortcode on an archive page, it will display the first post’s rating for every post.

    The way to fix this is to specify the specific recipe ID in the shortcode. We can search the post content for the recipe, and if a recipe is found, output the star rating using that recipe’s ID.

    Add this function to your theme’s functions.php file.

    /**
     * Star Rating 
     * @see https://www.billerickson.net/include-recipe-rating-on-archive-pages
     *
     */
    function be_star_rating() {
    
    	// Return early if WP Recipe Maker isn't active 
    	if( ! class_exists( 'WPRM_Recipe_Manager' ) )
    		return;
    
    	global $post;
    	$recipes = WPRM_Recipe_Manager::get_recipe_ids_from_content( $post->post_content );
    	if ( isset( $recipes[0] ) ) {
    		$recipe_id = $recipes[0];
    		echo do_shortcode( '[wprm-recipe-rating id="' . $recipe_id . '"]' );
    	}
    
    }

    Then call be_star_rating() where you want the star rating to appear. Here’s how I use it in the template partial used in the screenshot above:

    echo '<article class="post-summary">';
    	echo '<a class="entry-image-link" href="' . get_permalink() . '" tabindex="-1" aria-hidden="true">' . get_the_post_thumbnail( get_the_ID(), 'small_thumbnail' ) . '</a>';
    
    	echo '<header class="post-summary__content">';
    		be_star_rating();
    		echo '<h4 class="entry-title"><a href="' . get_permalink() . '">' . get_the_title() . '</a></h4>';
    	echo '</header>';
    echo '</article>';
  • Category Specific Search Form

    I include category landing pages in many of the websites I build. These transform category archives into engaging destinations that surface popular and seasonal articles, include SEO friendly content, and encourage deeper content discovery.

    While the homepage might include the standard search form that searches through all site content, these category landing pages need a category-specific search form.

    There’s a few ways I approach this, depending on the type of site I’m building (Genesis or custom) and how the form will be added to category archives. This article will provide a general overview of how it’s built and a few different implementations so you can find the solution that best fits your needs.

    The tl;dr; version is we’ll add a hidden input field to the search form that stores the category slug, then modify the main query on search results page to filter by that category if provided.

    Quick Links:

    1. Customizing the search form
    2. Customizing the search query
    3. Using searchform.php with Genesis
    4. Automatically add search form to category archives
    5. Category Search shortcode
    6. Category Search block
    7. Category Search ACF module

    Customizing the search form

    Most themes will include a searchform.php file which provides the markup for the search form throughout the site. Here’s the searchform.php file from my starter theme:

    <?php
    /**
     * Search form
     *
     * @package      EAStarter
     * @author       Bill Erickson
     * @since        1.0.0
     * @license      GPL-2.0+
    **/
    ?>
    
    <form role="search" method="get" class="search-form" action="<?php echo esc_url( home_url( '/' ) ); ?>">
    	<label>
    		<span class="screen-reader-text">Search for</span>
    		<input type="search" class="search-field" placeholder="Search…" value="<?php echo get_search_query(); ?>" name="s" title="Search for" />
    	</label>
    	<button type="submit" class="search-submit"><?php echo ea_icon( array( 'icon' => 'search', 'title' => 'Submit' ) );?></button>
    </form>

    We’re going to make two changes to the form:

    1. Add a hidden input field containing the category slug we’d like to search.
    2. Make the placeholder text customizable

    A simple option would be to check if we’re on a category archive to make our changes. But this would affect all search forms on the site. I only want to affect a specific search form on the page, not other search forms that might be in the site header, sidebar, or footer.

    I’m going to check for a global variable, $be_search_form_args, which can be used to customize the search form on a per-form basis. You set up the global variable, call get_search_form() , then reset the global variable.

    My new searchform.php file looks like this:

    <?php
    /**
     * Search form
     *
     * @package      EAStarter
     * @author       Bill Erickson
     * @since        1.0.0
     * @license      GPL-2.0+
    **/
    
    global $be_search_form_args;
    if( empty( $be_search_form_args ) )
    	$be_search_form_args = array();
    
    $search_form_args = shortcode_atts(
    	array(
    		'placeholder' => 'Search the site …',
    		'category' => false,
    	),
    	$be_search_form_args
    );
    
    ?>
    
    <form role="search" method="get" class="search-form" action="<?php echo esc_url( home_url( '/' ) ); ?>">
    	<label>
    		<span class="screen-reader-text">Search for</span>
    		<input type="search" class="search-field" placeholder="<?php echo esc_attr( $search_form_args['placeholder'] ); ?>" value="<?php echo get_search_query(); ?>" name="s" title="Search for" />
    		<?php
    		if( !empty( $search_form_args['category'] ) {
    			echo '<input type="hidden" name="be_search_cat" value="' . esc_attr( $search_form_args['category'] ) . '" />';
    		}
    		?>
    	</label>
    	<button type="submit" class="search-submit"><?php echo ea_icon( array( 'icon' => 'search', 'title' => 'Submit' ) );?></button>
    </form>

    If I want to display a search form limited to Appetizers, I’ll use:

    global $be_search_form_args;
    $be_search_form_args = array( 
    	'category' => 'appetizers', 
    	'placeholder' => 'Search appetizers…'
    );
    get_search_form();
    $be_search_form_args = array();

    If someone searches that form for “hummus”, the resulting URL will be
    site.com/?s=hummus&be_search_cat=appetizers

    Customizing the search query

    Now that we’re passing the category we want in the URL, we need to customize the search results query to look for it. We’ll use pre_get_posts.

    /**
     * Search query
     * @link https://www.billerickson.net/category-specific-search-form/
     */
    function be_search_query( $query ) {
    	if( $query->is_main_query() && ! is_admin() && $query->is_search() ) {
    		if( !empty( $_GET['be_search_cat'] ) )
    			$query->set( 'category_name', esc_attr( $_GET['be_search_cat'] ) );
    	}
    }
    add_action( 'pre_get_posts', 'be_search_query' );
    

    For more information, see my article on Customizing the Main Query.

    Using searchform.php with Genesis

    If your site was built with the Genesis theme framework, you probably don’t have a searchform.php file in your child theme. The search form itself is defined in the Genesis parent theme, and can be customized using the genesis_search_form filter.

    You could use that filter to customize the form markup directly. I find it simpler to create a searchform.php file in my child theme, then use the filter to tell Genesis to use it. That’s what I do in my Genesis starter theme.

    First create a searchform.php file in your child theme, using the markup above or any other search form markup you’d like.

    Then add the following to your theme’s functions.php file:

    /**
     * Custom search form
     *
     */
    function be_search_form() {
    	ob_start();
    	get_template_part( 'searchform' );
    	return ob_get_clean();
    }
    add_filter( 'genesis_search_form', 'be_search_form' );

    Automatically add search form to archive pages

    Now that you have customized the search form and the query on search results, it’s time to add your form to the site.

    We’ll create a function, be_category_search_form(), that only runs on category archive pages and automatically pulls in the relevant information.

    /**
     * Category search form 
     * @link https://www.billerickson.net/category-specific-search-form/
     *
     */
    function be_category_search_form() {
    	if( ! is_category() )
    		return;
    
    	global $be_search_form_args;
    	$be_search_form_args = array(
    		'category' => get_query_var( 'category_name' ),
    		'placeholder' => 'Search this category…',
    	);
    	get_search_form();
    	$be_search_form_args = array();
    }

    How you add this function to your category archive page will depend upon your theme. If you’re using a Genesis theme, you can use a hook:

    add_action( 'genesis_before_loop', 'be_category_search_form' );

    If your theme has a category.php or archive.php file, you can place it in there.

    Category search shortcode

    Another way you could use this is as a shortcode, so you could display the search form on a few specific categories.

    Most themes will output the category description as introductory text, or have their own “Introductory Text” field when editing a category. Assuming your theme allows shortcodes in this field, add [be_category_search] into it for a category.

    Then add the following code to your theme’s functions.php file or a core functionality plugin:

    /**
     * Category search form shortcode 
     * @see https://www.billerickson.net/category-specific-search-form/
     *
     */
    function be_category_search_shortcode( $atts = array() ) {
    	$atts = shortcode_atts( 
    		array(
    			'category' => false,
    			'placeholder' => false,
    		), 
    		$atts, 
    		'be_category_search'
    	);
    
    	global $be_search_form_args;
    	$be_search_form_args = $atts;
    	get_search_form();
    	$be_search_form_args = array();
    
    }
    add_shortcode( 'be_category_search', 'be_category_search_shortcode' );

    Category search block

    When possible, I build category landing pages as a block area so we can use Gutenberg blocks on them. I’ll then build my category search block using Advanced Custom Fields. For more information, see Building a block with ACF.

    I’ll register my custom block:

    acf_register_block_type( array(
    	'name'				=> 'category-search',
    	'title'				=> __( 'Category Search', 'clientname' ),
    	'render_template'		=> 'partials/block-category-search.php',
    	'category'			=> 'widgets',
    	'icon'				=> 'search',
    	'mode'				=> 'preview',
    ));

    I’ll create an ACF field group for the block that lets the user select the category and specify the placeholder text.

    I’ll also create the render template file, block-category-search.php, for the block:

    <?php
    /**
     * Category Search block
     *
     * @package      Client
     * @author       Bill Erickson
     * @since        1.0.0
     * @license      GPL-2.0+
    **/
    
    global $be_search_form_args;
    $be_search_form_args = array(
    	'category' => get_field( 'category' ),
    	'placeholder' => get_field( 'placeholder' ),
    );
    get_search_form();
    $be_search_form_args = array();

    Category Search ACF Module

    If the category landing page is too complex to build using the Gutenberg block editor, I’ll use ACF flexible content.

    One of the layouts will be category_landing and have fields for category and placeholder.

    I’ll have the following in my ea_module() function:

    case 'category_landing':
    	global $be_search_form_args;
    	$be_search_form_args = array(
    		'category' => $module['category'],
    		'placeholder' => $module['placeholder'],
    	);
    	get_search_form();
    	$be_search_form_args = array();
    	break;

    For more information, see Landing Pages with ACF Flexible Content.

  • Custom logo on the WordPress login

    Personalizing the WordPress login screen with your own logo is a great idea, especially if you have users logging in.

    On many of the food blogger websites we build, users can register and login to save their favorite recipes. We want consistent branding throughout the entire user experience, including the login screen.

    Both of my starter themes have this functionality built-in. If you’re using a different theme, create a login-logo.php file in your theme with the following:

    <?php
    /**
     * Login Logo
     *
     * @package      EAGenesisChild
     * @author       Bill Erickson
     * @since        1.0.0
     * @license      GPL-2.0+
    **/
    
    /**
     * Login Logo URL
     *
     */
    function ea_login_header_url( $url ) {
        return esc_url( home_url() );
    }
    add_filter( 'login_headerurl', 'ea_login_header_url' );
    add_filter( 'login_headertext', '__return_empty_string' );
    
    /**
     * Login Logo
     *
     */
    function ea_login_logo() {
    
    	$logo_path = '/assets/images/logo.svg';
    	if( ! file_exists( get_stylesheet_directory() . $logo_path ) )
    		return;
    
    	$logo = get_stylesheet_directory_uri() . $logo_path;
        ?>
        <style type="text/css">
        .login h1 a {
            background-image: url(<?php echo $logo;?>);
            background-size: contain;
            background-repeat: no-repeat;
    		background-position: center center;
            display: block;
            overflow: hidden;
            text-indent: -9999em;
            width: 312px;
            height: 100px;
        }
        </style>
        <?php
    }
    add_action( 'login_head', 'ea_login_logo' );

    Update the $logo_path to point to your logo in your theme.

    Keep the width attribute unchanged, but you may want to update the height attribute to better fit your logo. To calculate what the height should be, use the formula:

    height = logoHeight / logoWidth * 312

    Don’t forget to include the login-logo file in your functions.php file. Assuming you put it in /inc/login-logo.php, add this to your functions.php file:

    include_once( get_stylesheet_directory() . '/inc/login-logo.php' );

    Customize it even further

    You can style the entire login page in that block of CSS. Check out the login page for Lil Luna:

  • Remove avatars from comments

    When an article has many comments, the avatars within those comments can represent a large percentage of your overall page size. You can drastically decrease the load time on your popular articles by removing the avatars.

    You could go to Settings > Discussion to disable avatars sitewide, but this will also remove author avatars as well.

    If you are using avatars in an author box above or below the post, you should use the following code to remove only the avatars in the comments area. Place this in your theme’s functions.php file or a core functionality plugin.

    /**
     * Remove avatars from comment list
     * @link https://www.billerickson.net/remove-avatars-from-comments/
     */
    function be_remove_avatars_from_comments( $avatar ) {
    	global $in_comment_loop;
    	return $in_comment_loop ? '' : $avatar;
    }
    add_filter( 'get_avatar', 'be_remove_avatars_from_comments' );

    For more performance recommendations, see 10 ways to speed up your WordPress website

  • Custom recipe templates for WP Recipe Maker

    I frequently work with food bloggers to improve the design, speed, and functionality of their websites. The recipe card is the most important element on the page.

    It should be easy to read on all devices, look professional, and express their unique personality. A custom recipe template is a great way to accomplish this.

    Note: This tutorial is designed for developers familiar with WordPress and CSS. If you’re a food blogger and need help, contact me. If I can’t help, I can usually direct you to someone who can.

    Quick Links:

    1. Template Mode: Modern vs Legacy
    2. Creating a Legacy Template
    3. Creating a Modern Template
    4. Add Blocks
    5. Style your template
    6. Multiple custom templates

    Template Mode: Modern vs Legacy

    The first decision you’ll need to make is which template mode to use.

    A Legacy template is stored directly in your theme, inside the /wprm-templates/ directory.

    A Modern template uses the WPRM Template Editor, accessible in WP Recipe Maker > Settings > Template Editor. You can use their recipe card builder to add blocks, customize their settings, and style them with CSS.

    The legacy mode makes sense for developers, but made it difficult for users to modify their recipe cards. You must use the modern template mode to take advantage of new features and specify different recipe designs based on the type of content (ex: food vs how-to).

    I’ll walk you through creating a template with both approaches so you can decide which is best for you.

    Creating a Legacy Template

    Let’s say you want to create a template called “Client Name”. Inside your active WordPress theme, create the following folder structure: /wprm-templates/recipe/client-name/.

    Inside the client-name/ directory, create a file named client-name.php. This will hold the markup of your custom recipe card. Also create client-name.css, which will hold all the styling for your recipe card.

    You can find examples of legacy templates inside the WP Recipe Maker plugin. Go to /templates/recipe/legacy to see the three legacy themes built into the plugin. I recommend you copy the contents of an existing theme’s PHP file into your custom theme and start customizing it.

    To use your custom theme, log into the backend of your WordPress site and go to WP Recipe Maker > Settings. Select “Legacy” as the template mode, and select your “Client Name” template from the dropdown.

    Creating a Modern Template

    Go to WP Recipe Maker > Settings and make sure “Modern” is the selected template mode. Then scroll down and click the “Open the Template Editor” button. You’ll see a screen that looks like this:

    Select one of the existing recipe templates (ex: Basic, Classic…), then click the “Clone & Edit Template” button. You’ll be asked to name the template, then taken to the Template Editor.

    I like to start in the “Edit HTML” tab of the Template Editor. I clear out all the extraneous markup and shortcodes I don’t need. I then start writing the markup I need in my recipe template.

    The recipe template featured below begins with a site logo, then a two-column header with title and summary on left, image and buttons on right. I have the right column markup first because it will be on top on mobile.

    Add blocks

    WP Recipe Maker uses shortcodes for each element of the recipe card. They refer to these as blocks in the template editor.

    Rather than having to remember all the available shortcodes and their parameters, you can use the “Add Blocks” tab to select exactly which shortcode you need and where you want it inserted. The “Edit Blocks” tab lets you manage the shortcode parameters.

    The screenshot below shows the three steps to adding a block

    1. Select the “Add Blocks” tab at the top, then click on a block to add. I selected “Recipe Image”
    2. Select after which block you would like the “Recipe Image” block to appear.
    3. Customize the “Recipe Image” block settings. Select the image size, style, etc.

    After you have inserted your block, you can click the “Edit HTML” tab to see the shortcode it generated. The “Recipe Image” block created the following shortcode in my recipe template:

    [wprm-recipe-image size="thumbnail"]

    Style your template

    Now that you have all the blocks you’d like to appear in your recipe card, click the “Edit CSS” tab to begin styling it. As you type CSS, a live rendering of the recipe card will refresh below the CSS editor.

    Personally, I prefer writing CSS in my code editor. I use SASS so I can access brand colors via variables, nest styles, and more.

    I leave the “Edit CSS” tab empty for now, and visit a page on my development site that is using the recipe card. In my theme I create a new recipe.scss SASS file that generates a separate recipe.cssfile I enqueue on the frontend temporarily.

    I build my recipe card stylesheet in here, testing the recipe card in my actual site rather than the Template Editor. Once I have the styles the way I want them, I copy the compiled recipe.css file into the “Edit CSS” tab and dequeue my temporary stylesheet.

    You could decide not to add any styles to the Template Editor and keep them all in your theme. This works fine if you only have one custom recipe template, but one of the most powerful features in WP Recipe Maker is support for multiple recipe cards.

    Multiple custom templates

    When you have the Modern template mode selected, you’ll notice that you can select templates for more than one type of recipe.

    One of my clients uses the “Food” recipe type for recipes and the “How To” recipe type for workouts. We created a custom template for each. The workout template included custom fields for muscle group, difficulty, and equipment needed.

    Another client wanted to include a compact version of their recipe card as a featured recipe on category landing pages.

    As WP Recipe Maker adds support for more recipe types, and as Google expands the types of schema it looks for, multiple recipe templates will become even more important.

  • “Tweet this” blockquote in Gutenberg block editor

    Adding a “click to tweet” feature to your articles is a great way to encourage readers to share your content.

    I use custom block styles make it as easy as possible. You simply type your quote, click style options, then select “Tweet”.

    The “Tweet this” button is added server-side when the block is rendered rather than client-side with JavaScript, so it is AMP compatible and better for performance.

    You can use the is-style-tweet class on the blockquote to style it differently than your standard blockquotes, like adding the twitter icon to the top.

    Register block style

    First, we need to register a tweet block style for the core/quote block type. See my article on Block Styles in Gutenberg for more information.

    wp.blocks.registerBlockStyle( 'core/quote', {
    	name: 'tweet',
    	label: 'Tweet',
    });

    Now when you insert a blockquote, you can select the “Tweet” style in the top left corner:

    Add “Tweet This” button

    Next, we’ll use the render_block hook to modify the blockquote output to include our Tweet This button. This only runs on the frontend so the Tweet This button won’t appear in the Gutenberg block editor.

    This hook includes two parameters:

    • $block_content (string) The block content to display on the page
    • $block (array) The full block, including name and attributes

    Add the following code to your theme’s functions.php file or a Core Functionality plugin. It does the following:

    • Only applies to blockquotes with the tweet style (class of is-style-tweet).
    • Grabs all the text before the citation (if there is one) and strips all HTML. This will be used as the text of the tweet.
    • Appends the current post’s permalink to the tweet.
    • If you’re using Yoast SEO and have a Twitter handle specified, it appends @via with your twitter handle.
    • Appends a “Tweet This” button to the blockquote.
    /**
     * Tweet Quote
     *
     */
    function ea_tweet_quote( $block_content, $block ) {
    	if( 'core/quote' === $block['blockName'] && !empty( $block['attrs']['className'] ) && 'is-style-tweet' === $block['attrs']['className'] ) {
    
    		$content = explode( '<cite>', $block_content );
    		$text = rawurlencode( wp_strip_all_tags( $content[0]) ) . ' ' . get_permalink();
    		$url = 'https://twitter.com/intent/tweet?&text=' . $text;
    		$seo_data = get_option( 'wpseo_social' );
    		if( !empty( $seo_data['twitter_site'] ) )
    			$url .= '&via=' . esc_attr( $seo_data['twitter_site'] );
    
    		$original = '</blockquote>';
    		$new = '<a class="wp-block-button__link small" href="' . esc_url_raw( $url ) . '" target="_blank" rel="noopener noreferer">Tweet This</a>' . $original;
    		$block_content = str_replace( $original, $new, $block_content );
    	}
    	return $block_content;
    }
    add_filter( 'render_block', 'ea_tweet_quote', 10, 2 );
  • Building a WordPress theme with Gutenberg

    Many of the articles I’ve shared recently describe specific features in the Gutenberg block editor and the Genesis theme framework.

    I think it would be helpful to take a step back and walk you through my process of building a custom WordPress theme from start to finish. I encourage you to also review my Developer’s Guide to Gutenberg.

    I focus on a recent Genesis child theme I built for Her Packing List, but the process applies to any WordPress theme.

    In this article:

    1. Start with design
    2. Style Guide
    3. Custom blocks
    4. Gutenberg specific styling
    5. Finish building the base template
    6. Archives
    7. Block Areas
    8. Modular Template

    Start with design

    Gutenberg has accelerated a change in the way we design and build themes. We still use a three phase process of Discovery, Design and Development, but we now think in more modular terms.

    Rather than designing in pages, with each page often being a unique template with its own styling and metaboxes, we design in blocks and assemble the pages using these blocks.

    We actually used a modular design approach before Gutenberg, but were constrained on the development side by the classic editor. Our designs used reusable components, but these were often managed with metaboxes and shortcodes. Now with Gutenberg our modular design can be implemented with core and custom blocks.

    It’s crucial to work with a designer who understands the Gutenberg block editor. My two design partners, Duane Smith and Andrew Pautler, create incredible web experiences both through their design expertise and by enabling our clients to fully use the features of WordPress.

    Thank you! I am just blown away at how well everything is thought through in your development! I LOVE it!

    avatarRobyn Stone
    Add a Pinch

    I won’t go into any more detail on our design process, but I think it’s important to note that a key factor in building an easy-to-manage website is starting with a design that was built for the block editor, rather than trying to squeeze a custom design into the block editor.

    Style Guide

    After I’ve set up my development environment and download a fresh copy of my starter theme, I start development by building out the Style Guide.

    The above is just a sampling from the style guide. Here’s the full style guide: desktop | tablet | mobile.

    First I update my _base.scss SASS file with brand colors, Gutenberg color options, grid width, and breakpoints. Here’s the _base.scss file for Her Packing List.

    I review the core blocks to see which will need additional block styles. This site had quite a few – underline and fancy styles for headings, arrow and icon styles for lists, and more. Here’s the editor.js file for this project.

    In the content editor I build the Style Guide page with all the content from the mockup, up until the “Custom Gutenberg Blocks” section. Then I write the CSS in _blocks.scss so the Style Guide matches the mockup.

    Custom Blocks

    I build all of my custom blocks using ACF. I avoid using block library plugins like Atomic Blocks and CoBlocks because they contain many blocks that we don’t need, and the ones we would use often allow customizing the styling (ex: specify padding for each block).

    I don’t want content creators to worry about design. They should focus on content creation and let the theme handle the styling. ACF makes it incredibly easy to quickly build custom blocks with minimal options, keeping the interface as simple as possible for content creators.

    Before I start coding, I review the visual and technical requirements for each custom block and determine how I’ll build it.

    The above is just a sampling from the style guide. Here’s the full style guide: desktop | tablet | mobile.

    • Social Media Share Buttons will be implemented with Shared Counts. I’m not actually building a custom block for this since they will automatically be added to the end of every post by Shared Counts. I just have to style them to match the design.
    • Social Media Share Links will be an ACF block with no options. It will pull social links from Yoast SEO.
    • Pinterest CTA will be an ACF block with two image upload fields.
    • Email Subscribe CTA will be a WPForms block with some CSS.
    • HPL World CTA will be an ACF block with no options.
    • Email Contact Form is WPForms.
    • Product Listing is an ACF block with a title and repeatable field for products. Each product has a name, image, URL, and price.
    • Quick Links is an ACF block with a repeatable field for links.
    • Gear We Use and Travel Resources use the WP core group block with a background color set. They are saved as a reusable blocks since they are used on multiple pages.
    • Related Posts is an ACF block with no options. It will be used in the Single Post Block Area.
    • Next Stop is an ACF block with no options. The dropdowns are populated with terms from a custom taxonomy and take you to the term archive.
    • Fact Box is the WP core group block with a background color.

    See my article Building a block with Advanced Custom Fields for information on building custom blocks. Also take a look at the sample code at the end of ACF with version control to see how I organize the ACF code in the theme.

    Gutenberg Specific Styling

    After ensuring all of the blocks match the design on the frontend, it’s a good idea to switch back to the Gutenberg block editor and make sure your blocks look the same there too.

    The backend editor should match the frontend as close as possible so content editors have a good idea of how the page will look before publishing.

    If you’re using SASS, it’s easy to generate an additional stylesheet with only the relevant block styles, then enqueue it as editor styles. My editor-style.scss loads:

    • _base.scss with my variables for colors
    • _blocks.scss with my block-specific styling
    • _gutenberg.scss for Gutenberg-only styling

    WordPress loads some CSS only in Gutenberg that can alter the styling of your blocks. I use my Gutenberg-only partial for minor tweaks to get the backend blocks matching the frontend.

    I also updated the post title to be white on a red background, and customized the sizes of wide and full blocks. Here’s my _gutenberg.scss for Her Packing List.

    Finish building the base template

    Now that the blocks in the content area are complete, I focus on everything that’s not in the block editor. This includes building out the site header, navigation menu, sidebar, and site footer.

    Once the Style Guide is complete, I move over to the Single Post design and build out the additional post elements, like the comments and author box.

    All of the elements after the author box are managed with the After Post Block Area, described in its own section below.

    Genesis can now automatically add the featured image to the top of posts, and let writers disable it on a per-post basis. Add the following to your theme’s functions.php file:

    add_post_type_support( 'post', 'genesis-singular-images' );

    See the “Featured image output options” section of the Genesis 3.1 release notes for more information.

    Archives

    After building out all the singular content (Style Guide, Single Post, and individual pages), I move on to the dynamic archives.

    My approach to archives is a bit different than most Genesis theme developers, but will be familiar to those building custom themes outside of Genesis.

    What makes Genesis unique are its hooks and filters. These allow you to customize any aspect in Genesis, and insert your own elements wherever you’d like. Want to add something before the post title? Use the genesis_entry_header hook with a priority less than 10.

    These same hooks run when a post is on an archive page, so any customizations you make need to be scoped with conditional tags (ex: is_single() ). If you’re using genesis_custom_loop() for related posts, you need to limit your customizations to the primary post on the page using if( get_the_ID() === get_queried_object_id() ).

    Scoping your customizations is even more difficult if you use a modular design approach like we do. We’ll typically have a few different “post summary” styles used throughout the site, on the archive page, in a related posts section on a single post, and in custom “Post Listing” blocks on landing pages.

    I prefer to remove the standard genesis loop (and all of its hooks) on archive pages and use template partials instead. I can then design the “post summary” styles as template partials and use them wherever they are needed.

    On Her Packing List I built 3 different archive partials, found in /partials/ directory of the theme.

    archive.php

    echo '<article class="post-summary">';
    	echo '<a class="entry-image-link" href="' . get_permalink() . '" tabindex="-1" aria-hidden="true">' . get_the_post_thumbnail( get_the_ID(), 'ea_archive' ) . '</a>';
    	echo '<p class="post-summary__meta">' . ea_icon( array( 'icon' => 'chat-circles', 'size' => 16 ) ) . get_comments_number_text() . '</p>';
    	echo '<h5 class="entry-title"><a href="' . get_permalink() . '">' . get_the_title() . '</a></h5>';
    echo '</article>';
    

    archive-related.php

    echo '<article class="post-summary related">';
    	echo '<a class="entry-image-link" href="' . get_permalink() . '" tabindex="-1" aria-hidden="true">' . get_the_post_thumbnail( get_the_ID(), 'ea_archive', array( 'class' => 'nopin' ) ) . '</a>';
        echo '<h3><a href="' . get_permalink() . '">' . get_the_title() . '</a></h3>';
    echo '</article>';
    

    archive-featured.php

    echo '<article class="post-summary featured">';
    	echo '<a class="entry-image-link" href="' . get_permalink() . '" tabindex="-1" aria-hidden="true">' . get_the_post_thumbnail( get_the_ID(), 'large' ) . '</a>';
        echo '<div class="post-summary__content">';
            echo '<h2 class="is-style-underline">Featured Post</h2>';
            echo '<h2 class="entry-title"><a href="' . get_permalink() . '">' . get_the_title() . '</a></h2>';
            the_excerpt();
            echo '<p class="post-summary__meta">' . ea_icon( array( 'icon' => 'chat-circles', 'size' => 16 ) ) . get_comments_number_text() . '</p>';
        echo '</div>';
    echo '</article>';

    In my archive.php template file, I made the first post on the page use the archive-featured.php partial with:

    /**
     * Featured post partial
     *
     */
    function ea_archive_featured_post( $context ) {
    	global $wp_query;
    	if( ! get_query_var( 'paged' ) && 0 === $wp_query->current_post )
    		$context = 'featured';
    	return $context;
    }
    add_filter( 'ea_loop_partial_context', 'ea_archive_featured_post' );
    

    For more information on this approach, see Using Template Parts with Genesis.

    Block Areas

    Our sites often have repeated elements outside the content area, like related posts at the bottom of a single post.

    You could hardcode these features into the template file, or create a widget area so your client can edit it.

    We prefer creating Block Areas, which are like widget areas but use the block editor.

    To include the “After Post” block area to our single posts, I added this to single.php:

    /**
     * After Post modules
     *
     */
    function ea_after_post_area() {
        if( function_exists( 'ea_block_area' ) )
    	   ea_block_area()->show( 'after-post' );
    }
    add_action( 'genesis_after_entry', 'ea_after_post_area', 9 );

    Block Area in Archives

    We also provided the ability to insert block areas inside archive pages.

    On certain categories, our client wanted to use the “Post Listing” block we created to list recent posts in subcategories of the current category. These would appear after the first (“featured”) post, and before the second post.

    I edited the block_area post type registration to include categories and tags as supported taxonomies. Then on archive pages, I do a taxonomy query to see if any block areas have the current category, and if so I output the block area after the first post.

    /**
     * Archive block area
     *
     */
    function ea_archive_block_area() {
    
    	if( get_query_var( 'paged' ) )
    		return;
    
    	global $wp_query;
    	if( 1 !== $wp_query->current_post )
    		return;
    
    	$tax = is_category() ? 'category' : ( is_tag() ? 'post_tag' : false );
    	if( ! $tax )
    		return;
    
    	$loop = new WP_Query( array(
    		'post_type' => 'block_area',
    		'posts_per_page' => 1,
    		'tax_query' => array(
    			array(
    				'taxonomy' => $tax,
    				'field'		=> 'term_id',
    				'terms'		=> array( get_queried_object_id() )
    			)
    		)
    	));
    
    	if( $loop->have_posts() ): while( $loop->have_posts() ): $loop->the_post();
    		echo '<div class="block-area archive-block-area">';
    		the_content();
    		echo '</div>';
    	endwhile; endif; wp_reset_postdata();
    }
    add_action( 'genesis_before_entry', 'ea_archive_block_area' );

    Modular Template

    Her Packing List doesn’t include a modular template, but I’m including a note here because many of the sites we build do have them.

    I think of Gutenberg as a “rich content builder”, but not a page builder or layout builder. It’s not a good tool for things like displaying content across multiple columns on a grid and adjusting that layout at different breakpoints.

    I hope Gutenberg never becomes a full-on page builder because that will drastically increase the complexity of content editing. I’m a firm believer in the separation of content and design.

    Some pages are too difficult to implement directly in the Gutenberg block editor. On the sites we build, these are often homepages, landing pages, and category landing pages.

    I create a modules.php page template that disables the editor, then loads an ACF metabox with a Flexible Content field. We’ll design and build a dozen or so modules. For more information, see Landing Pages with ACF Flexible Content.

    During our discovery and design process we identify the types of blocks/modules needed and their overall complexity. We try to use blocks for everything if possible, like we did with Her Packing List.

    If the elements reach a certain level of complexity, we’ll work with our client to either scale back the complexity to something manageable with blocks, or start separating elements into blocks and modules.

    The downside to modules is you lose the flexibility and visual editing of a block editor, but it can greatly simplify the content creation process on complex pages.

    This is a conversation we have with the client during discovery and design. Our goal is to make their site as easy and enjoyable to manage as possible, so they should be involved in deciding how we build these features.

  • Update form field values in WPForms

    In the GenesisWP Slack, Dan asked a great question about WPForms:

    How can I add two WPForms fields together to display the result in the form’s notification?

    Dan Brubaker

    I would create a hidden field named “Total” and set it to the sum of the two other fields. We can then use a smart tag for displaying the total field’s value in the notification and confirmation message.

    wpforms_process_filter

    We can use the wpforms_process_filter filter to customize field values right after the form has been submitted, but before it is saved in the database and emails are sent.

    This filter includes three parameters

    • $fields(array) Sanitized entry field values/properties.
    • $entry(array) Original $_POST global.
    • $form_data(array) Form settings/data

    Example

    My form uses number fields for Red Shirts and Blue Shirts. I also have a hidden field called “Total Shirts”.

    We can then update the Total Shirts field value on submission.

    I’m using the form ID and field IDs to target the appropriate fields. Alternatively, you could target custom CSS classes added to the form and fields.

    /**
     * WPForms, update total field
     * @link https://www.billerickson.net/dynamically-update-fields-in-wpforms/
     *
     * @param array $fields Sanitized entry field values/properties.
     * @param array $entry Original $_POST global.
     * @param array $form_data Form settings/data
     * @return array $fields
     */
    function be_wpforms_update_total_field( $fields, $entry, $form_data ) {
    
    	// Only run on my form with ID = 7785
    	if( 7785 != $form_data['id'] )
    		return $fields;
    
    	// Add red shirts (field ID 3) and blue shirts (field ID 4) into total (field ID 5)
    	$fields[5]['value'] = intval( $fields[3]['value'] ) + intval( $fields[4]['value'] );
    
    	return $fields;
    }
    add_filter( 'wpforms_process_filter', 'be_wpforms_update_total_field', 10, 3 );
    

    Once the form is submitted, it shows the confirmation message including the total.

Bill Erickson

Bill Erickson is a freelance WordPress developer and a contributing developer to the Genesis framework. For the past 14 years he has worked with attorneys, publishers, corporations, and non-profits, building custom websites tailored to their needs and goals.

Ready to upgrade your website?

I build custom WordPress websites that look great and are easy to manage.

Let's Talk