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.

  • SASS Color Function

    The custom WordPress themes we build use brand colors for certain design elements and the Gutenberg color palette.

    We usually have three versions of each color: the main color, and lighter/darker versions for hover effects. We’ll often auto-generate the lighter/darker versions, but sometimes we need to specify a custom color.

    In the past I’ve added all of the colors as their own independent variables:

    // Brand Colors
    // -- normal / darker / lighter
    $blue_1: #59BACC;
    $blue_2: darken( $blue_1, 10% );
    $blue_3: lighten( $blue_1, 10% );
    $green_1: #58AD69;
    $green_2: #458D53;
    $green_3: #7ABE88;

    I’ve recently shifted to using a custom SASS function, brand-color(), that makes it easier to select the color.

    a {
    	color: brand-color( 'blue' );
    
    	&:hover {
    		color: brand-color( 'blue', 'darken' );
    }

    Inside my theme I define a map of $brand_colors. If you only specify the main color, it will auto-generate the lighten and darken variants. But you can also manually specify your own colors for those.

    The list of 6 variables above has been reduce to:

    $brand_colors: (
    	'blue'		: #59BACC,
    	'green'		: #58AD69,
    	'green_darken'	: #458D53,
    	'green_lighten'	: #7ABE88,
    );

    And here’s my custom brand-color() function. Take a look at my base theme to see how it’s included.

    If you’d like to build your own SASS functions, take a look at the @function documentation.

    /**
     * Brand Color
     *
     */
    @function brand-color( $key, $variant: null ) {
    	@if map-has-key( $brand_colors, $key ) {
    		$color: map-get( $brand_colors, $key );
    		@if ( 'lighten' == $variant ) {
    			$lighten_key: $key + '_lighten';
    			@if map-has-key( $brand_colors, $lighten_key ) {
    				$color: #{map-get( $brand_colors, $lighten_key )};
    			} @else {
    				$color: lighten( $color, 10% );
    			}
    		}
    		@else if( 'darken' == $variant ) {
    			$darken_key: $key + '_darken';
    			@if map-has-key( $brand_colors, $darken_key ) {
    				$color: #{map-get( $brand_colors, $darken_key )};
    			} @else {
    				$color: darken( $color, 10% );
    			}
    		}
    		@return $color;
    	} @else {
    		@error "The #{$key} color could not be found in $brand_colors";
    	}
    }
    
  • Reusable Blocks accessible in WordPress admin area

    Reusable Blocks are one of my favorite features in the new Gutenberg block editor. You can make content “reusable” and insert it into multiple pages.

    We use this on GBC Law Group to list their services throughout the site. If they add a new service, it will automatically update everywhere at once.

    What are reusable blocks?

    Reusable blocks let you write content once and use it on multiple pages of your site. If you update the reusable block on any of those pages, it will automatically update everywhere it was used, keeping them in sync.

    Reusable blocks are actually posts in a custom post type. When you create a reusable block, it adds those blocks to a new post in the wp_block post type.

    To create a reusable block, start by adding any block to the page, then click “Add to Reusable Blocks”. Give your reusable block a title, then click “Save”. You can click “Edit” to add additional content.

    When you insert a reusable block in a page, it references the reusable block’s post ID so it can always pull in the latest content. That’s how you can edit the reusable block once and it’s updated everywhere on your site.

    On this website I have made a “Practice Areas” reusable block that contains multiple “Service” blocks – a custom block I built using Advanced Custom Fields.

    When I’m editing another page, I can search the block list for my “Practice Areas” reusable block to insert it.

    If you click the “Manage All Reusable Blocks” link, you can manage them like any other custom post type. But it would be even simpler if we included this link in the admin menu.

    Add reusable blocks to admin menu

    The following code will add “Reusable Blocks” to the WordPress admin menu. Add this to your theme’s functions.php file or a core functionality plugin:

    /**
     * Reusable Blocks accessible in backend
     * @link https://www.billerickson.net/reusable-blocks-accessible-in-wordpress-admin-area
     *
     */
    function be_reusable_blocks_admin_menu() {
        add_menu_page( 'Reusable Blocks', 'Reusable Blocks', 'edit_posts', 'edit.php?post_type=wp_block', '', 'dashicons-editor-table', 22 );
    }
    add_action( 'admin_menu', 'be_reusable_blocks_admin_menu' );

    You can now click “Reusable Blocks” to see the full listing and edit them.

    When you click on a reusable block, you get the familiar block editor for managing all the content within that reusable block.

    WordPress Plugin

    I’ve built a plugin you can install to add this functionality without editing any code.

    Download and install Reusable Blocks UI.

  • Include custom fields in your WP Recipe Maker template

    The recipe editor in WP Recipe Maker includes fields like cook time, ingredients, and other common recipe items. You can use custom fields to add additional information to your recipe cards.

    SkinnyMs. includes Weight Watchers’ SmartPoints on all of their recipes, but there isn’t a field for this in WP Recipe Maker. They also use WP Recipe Maker for workouts and needed custom fields for items like Level of Difficulty and Targeted Muscle Group.

    Adding custom fields

    In the backend of your WordPress website, go to WP Recipe Maker > Manage, then click “Your Custom Fields” at the top, and “Custom Fields” in the secondary navigation at the top.

    Click the “Create Custom Field” button in the top right to create a new custom field. In the popup window you will select a type of field (text, image…), a key, and a name.

    The key should be lowercase and have no spaces; you can use hyphens or underscores to separate the words.

    Using custom fields

    Now whenever you create or edit a recipe, you can scroll down to the Custom Fields section to fill out your custom fields.

    Displaying custom fields

    Finally, you will need to edit your recipe card template to display your custom field. For more information, see my article on Custom Recipe Templates for WP Recipe Maker.

    Go to WP Recipe Maker > Settings and open the Template Editor. Select your template and click “Edit Template”.

    Click “Add Blocks” and select “Recipe Custom Field Container”, then select after which block it should appear. You’ll then see the “Edit Blocks” tab open. From the dropdown select the custom field you’d like displayed and any additional display options (style, text style…). You should also specify the Label at the bottom.

    Save your recipe template, exit out of the Template Editor, then go view a recipe that includes a custom field. The custom field should now appear in your recipe card.

  • SearchWP Related metabox on specific post types

    SearchWP has a related posts extension for generating a list of related posts based on their relevancy engine.

    The metabox appears on all post types by default, and you can use the searchwp_related_excluded_post_types filter to exclude certain post types.

    This often includes post types that have no need for related posts, like ACF field groups:

    Rather than listing the excluded post types, I prefer specifying the few post types to include.

    In the code below, I have an $allowed array listing the post types on which I’d like the metabox to appear. It then generate a list of all other post types and excludes SearchWP Related on those.

    /**
     * SearchWP Related on specific post types
     * @link https://www.billerickson.net/searchwp-related-metabox-on-specific-post-types/
     */
    function ea_searchwp_related_exclude( $exclude = array() ) {
    	$allowed = array( 'post', 'resource' );
    	$all = array_keys( get_post_types() );
    	return array_diff( $all, $allowed );
    }
    add_filter( 'searchwp_related_excluded_post_types', 'ea_searchwp_related_exclude' );
  • 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 your theme or core functionality plugin 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.

    Then use the wprm_importer_directories filter to tell WP Recipe Maker the directory in which your importer can be found. It will search that directory for files using the above naming structure, so you can include a directory that has other files in it without issues.

    I placed my custom importer in the /inc/ directory of my core functionality plugin, then added the following code:

    // Plugin directory
    define( 'EA_DIR' , plugin_dir_path( __FILE__ ) );
    
    /**
     * Include custom WPRM importer
     * @link https://www.billerickson.net/custom-importer-for-wp-recipe-maker/
     */
    function be_custom_wprm_importer( $directories ) {
    	$directories[] = EA_DIR . '/inc/';
    	return $directories;
    }
    add_filter( 'wprm_importer_directories', 'be_custom_wprm_importer' );

    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

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