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.

  • 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.

  • Building a header block in WordPress

    I’m building a website with two different page title styles:

    Standard page title
    Fancy page title

    I built this as a custom Header block so we can customize the page header directly in the Gutenberg block editor. The steps to implement this are:

    1. Create the custom block with Advanced Custom Fields.
    2. Remove the standard page title if this block is used.
    3. Move the block up to touch the site header when it’s the first block on the page.

    Create the header block

    I use Advanced Custom Fields to build custom blocks. Here’s a detailed guide on building custom blocks with ACF.

    I registered my header block and set the render template to the /partials/block-header.php file in my theme.

    acf_register_block_type( array(
    	'name'				=> 'header',
    	'title'				=> __( 'Header', 'core-functionality' ),
    	'render_template'		=> 'partials/block-header.php',
    	'category'			=> 'formatting',
    	'icon'				=> 'schedule',
    	'mode'				=> 'auto',
    	'keywords'			=> array(),
    ));

    I went to Custom Fields > Add New and created this field group:

    Then in partials/block-header.php I added the markup for the header.

    Take note of the first line as it took me a bit of time to figure it out. If you need access to any information about the current post (like the title), standard template tags like get_the_title() and get_the_ID() won’t work in the Gutenberg block editor, but will work on the frontend.

    ACF dynamically loads the block using AJAX and passes the post ID as $_POST['post_id']. So we’ll use that in the backend, and get_the_ID() on the frontend to get the Post ID. We use this to build the default page title ( get_the_title( $post_id ) ).

    // Post ID is in $_POST['post_id'] when rendering ACF block in Gutenberg
    $post_id = get_the_ID() ? get_the_ID() : $_POST['post_id'];
    
    $title = get_field( 'title' );
    if( empty( $title  ) )
    	$title = get_the_title( $post_id );
    
    $content  = get_field( 'content' );
    
    $image = get_field( 'image' );
    if( empty( $image ) )
    	$image = get_option( 'options_ea_default_header' );
    
    echo '<div class="block-header alignfull">';
    	echo '<div class="block-header__image">' . wp_get_attachment_image( $image, 'ea_header' ) . '</div>';
    	echo '<div class="block-header__content">';
    		echo '<h1>' . esc_html( $title ) . '</h1>';
    		echo wpautop( $content );
    	echo '</div>';
    echo '</div>';
    

    The new header block works great, but now there are two page titles. The next step is to remove the standard page title if this block is used.

    Remove the standard page title

    Before Gutenberg, I would have used Genesis Title Toggle to disable the standard page title. The next version of Genesis will include the title toggle functionality, but this still isn’t as user friendly as it could be.

    A better approach is to automatically remove the standard title if this block is used. We should also remove the title if an h1 heading block is used. If a block has innerBlocks (ex: columns, group…), we’ll scan through those as well.

    This function will let you know if the current post has an h1 block. Add it to your theme’s functions.php file, and change acf/header to the name of your heading block.

    /**
     * Recursively searches content for h1 blocks.
     *
     * @link https://www.billerickson.net/building-a-header-block-in-wordpress/
     *
     * @param array $blocks
     * @return bool
     */
    function be_has_h1_block( $blocks = array() ) {
    	foreach ( $blocks as $block ) {
    
    		if( ! isset( $block['blockName'] ) )
    			continue;
    
    		// Custom header block
    		if( 'acf/header' === $block['blockName'] ) {
    			return true;
    
    		// Heading block
    		} elseif( 'core/heading' === $block['blockName'] && isset( $block['attrs']['level'] ) && 1 === $block['attrs']['level'] ) {
    			return true;
    
    		// Scan inner blocks for headings
    		} elseif( isset( $block['innerBlocks'] ) && !empty( $block['innerBlocks'] ) ) {
    			$inner_h1 = be_has_h1_block( $block['innerBlocks'] );
    			if( $inner_h1 )
    				return true;
    		}
    	}
    
    	return false;
    }

    If you’re building a Genesis theme, you can use this function to unhook the heading and .entry-header markup:

    /**
     * Remove entry-title if h1 block used
     * @link https://www.billerickson.net/building-a-header-block-in-wordpress/
     */
    function be_remove_entry_title() {
    	if( ! ( is_singular() && function_exists( 'parse_blocks' ) ) )
    		return;
    
    	global $post;
    	$blocks = parse_blocks( $post->post_content );
    	$has_h1 = be_has_h1_block( $blocks );
    
    	if( $has_h1 ) {
    		remove_action( 'genesis_entry_header', 'genesis_entry_header_markup_open', 5 );
    		remove_action( 'genesis_entry_header', 'genesis_entry_header_markup_close', 15 );
    		remove_action( 'genesis_entry_header', 'genesis_do_post_title' );
    	}
    }
    add_action( 'genesis_before_entry', 'be_remove_entry_title' );

    If you’re building a custom theme, you can integrate the be_has_h1_block() into the function that displays the post title. For example, my EA Starter theme can be customized like so (here’s the original):

    /**
     * Entry Title
     *
     */
    function ea_entry_title() {
    
    	global $post;
    	$blocks = parse_blocks( $post->post_content );
    	if( ! be_has_h1_block( $blocks ) )
    		echo '<h1 class="entry-title">' . get_the_title() . '</h1>';
    }
    add_action( 'tha_entry_top', 'ea_entry_title' )

    Move block up to touch site header

    The .site-inner on this site has 16px of padding on mobile and 40px on tablet/desktop.

    If the header block is the first block on the page, this CSS adds negative margin to the top of the block so it is flush with the header:

    .entry-content  > .block-header:first-child {
    	margin-top: -16px;
    	@media only screen and (min-width: 768px) {
    		margin-top: -40px;
    	}
    }
  • Yoast SEO title and description in site search results

    Yoast SEO lets you customize the meta title and description for your articles, which is used in Google search results and browser tabs.

    Since these are often more descriptive than the actual page title, I recommend using them in your on-site search results as well. (I also recommend using SearchWP for improved site search).

    Yoast SEO stores this information as post metadata, using the key _yoast_wpseo_title for the title and _yoast_wpseo_metadesc for the description.

    But don’t output this directly – the titles often include dynamic variables like %%title%% for the post title.

    You can use wpseo_replace_vars( $string, $post ) to replace the variables with actual text.

    You should also provide a fallback in case Yoast SEO is no longer active, or no custom title was provided.

    Template tags for title and excerpt

    Include the functions below in your theme’s functions.php file or a core functionality plugin.

    You can then use be_search_entry_title() in place of get_the_title(), and be_search_entry_excerpt() in place of get_the_excerpt() in your theme, wherever it defines how posts appear in search results.

    /**
     * Entry title in site search
     * @link https://www.billerickson.net/yoast-seo-title-and-description-in-site-search-results/
     *
     */
    function be_search_entry_title() {
    	$title = '';
    	if( function_exists( 'wpseo_replace_vars' ) ) {
    		global $post;
    		$title = get_post_meta( get_the_ID(), '_yoast_wpseo_title', true );
    		$title = wpseo_replace_vars( $title, $post );
    	}
    	if( empty( $title ) ) {
    		$title = get_the_title();
    	}
    	return $title;
    }
    
    /**
     * Entry excerpt in site search
     * @link https://www.billerickson.net/yoast-seo-title-and-description-in-site-search-results/
     *
     */
    function be_search_entry_excerpt() {
    	$excerpt = '';
    	if( function_exists( 'wpseo_replace_vars' ) ) {
    		global $post;
    		$excerpt = get_post_meta( get_the_ID(), '_yoast_wpseo_metadesc', true );
    		$excerpt = wpseo_replace_vars( $excerpt, $post );
    	}
    	if( empty( $excerpt ) ) {
    		$excerpt = get_the_excerpt();
    	}
    	return $excerpt;
    }
    

    If you’re using one of my starter themes, you’ll use these functions in /partials/archive-search.php.

    If you’re using a StudioPress theme, also add this to your functions.php file so Genesis uses our custom functions:

    /*
     * Genesis, use search entry title
     * @link https://www.billerickson.net/yoast-seo-title-and-description-in-site-search-results/
     *
     */
    function be_genesis_search_entry_title( $title ) {
    	if( is_search() )
    		$title = be_search_entry_title();
    	return $title;
    }
    add_filter( 'genesis_post_title_text', 'be_genesis_search_entry_title');
    
    /**
     * Genesis, use search entry excerpt
     * @link https://www.billerickson.net/yoast-seo-title-and-description-in-site-search-results/
     *
     */
    function be_genesis_search_entry_excerpt() {
    	if( ! is_search() )
    		return;
    
    	remove_action( 'genesis_entry_content', 'genesis_do_post_content' );
    	echo wpautop( be_search_entry_excerpt() );
    }
    add_action( 'genesis_entry_content', 'be_genesis_search_entry_excerpt', 9 );
    
  • Conditionally display form fields in AMP Form

    A common feature on contact forms is displaying certain fields based on the value of another. If a visitor select “Phone” as the best method to contact them, you can display a “Phone Number” field.

    AMP pages cannot load custom JavaScript, but you can use amp-bind to add stateful interactivity.

    This tutorial uses the WPForms plugin in a Native AMP WordPress website, but the underlying approach will work for any amp-form. The only thing that will be different is how you add the attributes to your form fields.

    Stateful CSS classes

    In AMP, you can dynamically change the CSS classes on an element using the [class] attribute, or the data-amp-bind-class attribute. You should also specify the default CSS classes using the standard class attribute.

    When a certain field changes, we will use the following to update a state variable (foo):

    <select on="change:AMP.setState({ foo: event.value})">

    We’ll then use that state variable to toggle CSS classes added to other form fields (ex: hide and show).

    <input class="hide" [class]="foo ? 'show' : 'hide'" />

    See the amp-bind documentation for more information on adding custom stateful interactivity to your AMP pages via data binding and JS-like expressions.

    For a more in-depth example, see my article on building a navigation menu without JavaScript.

    Conditional logic in WPForms

    WPForms is the simplest way to add forms to AMP WordPress websites.

    On standard (non-AMP) WordPress websites, you can use the WPForms Conditional Logic feature to toggle field visibility. But this uses JavaScript, so won’t work on AMP pages.

    If you are using the Transitional / Paired AMP style, where visitors can see both an AMP version and non-AMP version of your site, you should use the tutorial below for the AMP version, then duplicate the logic in the Conditional Logic section in WPForms for the non-AMP version.

    If you are building a Native AMP / AMP-First website (which I recommend), you only have to build this logic once using the guide below.

    Summary of example

    On my contact form I have a dropdown for selecting “How did you find me”. If you select “Referral”, a text field displays asking the referral name. If you select “Other”, a text field displays asking you to describe it more.

    We’ll use the wpforms_field_properties filter to:

    • Modify the select dropdown to update a variable with the dropdown’s value when changed.
    • Modify the text fields to:
      • Hide them with a CSS class, wpforms-conditional-hide
      • Remove the class when the select field is set to the proper value, using the variable

    Adding conditional logic to fields

    I’ve defined a variable at the top, $conditional_settings, which stores all of my form-specific settings. You can use this to set the appropriate form and field IDs you want to toggle.

    Inside the conditional_show array, the key is the field ID, and the value is the select field’s value that should be selected to display this field.

    A hidden field will have .wpforms-conditional-hide added to it. Make sure you include this in your theme’s CSS: .wpforms-conditional-hide { display: none; }

    /**
     * WPForms AMP Conditional Logic 
     *
     * @link https://www.billerickson.net/wpforms-amp-conditional-logic
     *
     * @param array $properties
     * @param array $field 
     * @param array $form_data 
     * @return array $properties
    */
    function be_wpforms_amp_conditional_logic( $properties, $field, $form_data ) {
    
    	$conditional_settings = array(
    		'form_id' => '6307',
    		'toggle_field_id' => '7',
    		'conditional_show' => array(
    			'8' => 'Referral',
    			'9' => 'Other',
    		)
    	);
    
    	$state = 'wpforms' . $form_data['id'] . 'field' . $conditional_settings['toggle_field_id'];
    
    	// Limit to certain form
    	if( $form_data['id'] !== $conditional_settings['form_id'] )
    		return $properties;
    
    	// Toggle field
    	if( $field['id'] === $conditional_settings['toggle_field_id'] ) {
    		$property_on = 'change:AMP.setState({' . $state . ': event.value})';
    		$properties['input_container']['attr']['on'] = $property_on;
    	}
    
    	// Conditionally show fields
    	if( array_key_exists( $field['id'], $conditional_settings['conditional_show'] ) ) {
    		$active_value = $conditional_settings['conditional_show'][ $field['id'] ];
    		$active_classes = join( ' ', $properties['container']['class'] );
    		$inactive_classes = $active_classes . ' wpforms-conditional-hide';
    		$properties['container']['class'][] = 'wpforms-conditional-hide';
    		$properties['container']['attr']['data-amp-bind-class'] = sprintf(
    			'%s == \'%s\' ? \' %s \' : \' %s \'',
    			$state,
    			$active_value,
    			$active_classes,
    			$inactive_classes
    		);
    
    	}
    
    	return $properties;
    }
    add_filter( 'wpforms_field_properties', 'be_wpforms_amp_conditional_logic', 10, 3 );
    
  • WPForms submit button match Gutenberg button style

    We use the Gutenberg color palette and button styling on all sites we build. Buttons have a class of wp-block-button__link, and additional classes for different text/background colors.

    Most plugins use their own unique classes for styling buttons they add. WPForms (my favorite form plugin), uses wpforms-submit for the submit button.

    WPForms does let you specify Submit Button CSS Classes when you edit the form, but you would have to remember to add the class every time you create a form:

    Automatically add the class

    You can automatically add classes to the submit button in every form using the wpforms_frontend_form_data filter.

    <?php
    
    /**
     * WPForms submit button, match Gutenberg button block
     * @link https://www.billerickson.net/wpforms-submit-button-match-gutenberg-button-style/
     */
    function be_wpforms_match_button_block( $form_data ) {
    	$form_data['settings']['submit_class'] .= ' wp-block-button__link';
    	return $form_data;
    }
    add_filter( 'wpforms_frontend_form_data', 'be_wpforms_match_button_block' );

    You can still use the custom CSS classes for form-specific changes to the button.

    To change a specific form’s button from your site-wide default to the “Blue” background color (specified in your site’s color palette), go to WPForms > {select form } > Settings and add this to Submit Button CSS Class: has-background has-blue-background-color

  • WordPress AMP Contact Form

    AMP is a framework for developing fast and SEO-friendly websites. The AMP WordPress plugin lets you easily implement AMP on your WordPress website.

    Most WordPress form plugins use JavaScript for advanced features like conditional logic and form validation. AMP doesn’t allow custom JavaScript, so it can be difficult to build a Native AMP website with a contact form.

    You can build a custom form using the amp-form component, but it takes quite a bit of work. That’s the topic of a future blog post, so subscribe for updates if you’re interested.

    WPForms now supports AMP

    WPForms Lite is a free, simple, and powerful WordPress form plugin. As of June 17th, 2019, it is fully AMP compatible.

    All you have to do is have WPForms Lite and the official AMP WordPress plugin active on your site, and any forms you create with WPForms will automatically work with AMP.

    There will be no AMP errors due to JavaScript files loading. All the features of WPForms Lite work in AMP forms as well.

    AMP only works on HTTPS websites, so you’ll need an SSL certificate for the AMP plugin to work. Most web hosts will provide these for free.

    AMP with WPForms Pro

    The premium version of WPForms includes advanced form fields, conditional logic, newsletter forms, payment processing, and more. I use it on all of the sites I build.

    As of now, WPForms Pro does not automatically work with AMP. Many of the premium features and add-ons still depend upon JavaScript.

    If you’re a developer and know your form does not depend upon JavaScript (see the list below), you can enable AMP compatibility using the wpforms_amp_pro filter.

    // AMP support for forms in WPForms Pro
    add_filter( 'wpforms_amp_pro', '__return_false' );

    Almost all of the forms I build can be done without JavaScript. Whether it’s a contact form, newsletter signup, or eBook download form, building an AMP-compatible form with WPForms is simple.

    All of the forms on this site are built with WPForms and are running on a Native AMP website.

    Here’s how I am conditionally showing/hiding form fields with AMP based on other field values. In my contact form, if you select “Other” or “Referral” as the source, a text field appears asking for more information.

    WPForms JavaScript dependent features

    If you are using one of the following add-ons or form fields, you shouldn’t enable AMP compatibility because they may depend upon JavaScript:

    • WPForms Custom Captcha
    • WPForms Form Abandonment
    • WPForms Offline Form
    • WPForms Stripe
    • WPForms Signatures
    • WPForms Form Locker
    • WPForms Core
      • Conditional logic
      • Date/time field
      • Page break field
      • Rating field
      • Signature field
      • Captcha field
      • Likert field
      • Net Promoter field
      • Payments, Single item field
      • Payments, Total field

    Full AMP compatibility on its way

    The premium version of WPForms will soon be 100% AMP compatible. If you’re familiar with the 80/20 rule, you know it took 20% of the effort to ensure 80% of forms built with WPForms could be AMP compatible.

    I’ll keep this post updated with the latest in WPForms AMP compatibility, and subscribe to my blog for tutorials on AMP and WordPress in general.

  • Display last updated date on articles

    It’s a great idea to include the Published Date and Last Updated date on content you update regularly. This will let your readers know the content isn’t out-dated.

    WordPress stores a published date and modified date for every post on your site.

    You can use Limit Modified Date to make minor changes to an article (like fixing a typo) without changing the modified date.

    There are a few ways you can display the modified date on your articles.

    A Shortcode Option

    If you’re using a Genesis theme, you can use shortcodes in the Post Info and Post Meta areas to display dynamic content (more information).

    Genesis includes shortcodes for both the published date ([post_date]) and the modified date ([post_modified_date]). Here’s a full list of the available Genesis shortcodes.

    But if you use the shortcode for modified date, it will always appear, even if it’s the same as the published date.

    I personally prefer to only show the modified date if it’s more than a week later than the published date.

    Add the code below to your theme’s functions.php file, or a core functionality plugin. You can then use [be_published_modified_date] to display the published date, and include the modified date if it’s more than a week later.

    /**
     * Published & Modified Date
     *
     * @link https://www.billerickson.net/display-last-updated-date-on-articles/
     *
     */
    function be_published_modified_date() {
    	$date = get_the_date( 'U' );
    	$updated = get_the_modified_date( 'U' );
    
    	$output = '<span class="entry-date"><span class="label">Published on</span> ' . get_the_date( 'F j, Y' ) . '</span>';
    	if( $updated > ( $date + WEEK_IN_SECONDS ) )
    		$output .= ' <span class="entry-date-modified"><span class="label">Updated on</span> ' . get_the_modified_date( 'F j, Y' ) . '</span>';
    
    	return $output;
    }
    add_shortcode( 'be_published_modified_date', 'be_published_modified_date' );

    A Code Option

    If you prefer including the code directly in your theme, include the same code listed above.

    Then add the following to your theme file (ex: single.php) where you’d like the published and modified date to appear.

    echo be_published_modified_date();
  • Product Review Schema for Yoast SEO

    Yoast SEO schema uses the Article  type for posts, but you can extend this using plugins or your own custom code.

    In Google’s Structured Data Guidelines, they recommend:

    Try to use the most specific applicable type and property names defined by schema.org for your markup.

    I’m working on a product review website, so most articles are better classified with the Review type. I developed a plugin for:

    • Marking posts as reviews in the backend
    • Adding the additional review metadata to the post (in a custom metabox)
    • Updating the Yoast SEO schema to mark this as a review

    The article below is a guide for developers extending Yoast SEO schema using product reviews as the example.

    If you only want product review schema on your website, you can install my Product Review Schema for Yoast SEO plugin. You do not need to make any code changes.

    Install the plugin

    “Product Review Schema for Yoast SEO” is available for free on GitHub.

    Download Now

    Filtering schema pieces

    The key to extending Yoast SEO schema is the wpseo_schema_graph_pieces filter.

    This filter lets you add (or remove) pieces of the schema graph. Yoast SEO creates the large, structured graph by assembling all of these pieces.

    /**
     * Adds Schema pieces to our output.
     *
     * @param array                 $pieces  Graph pieces to output.
     * @param \WPSEO_Schema_Context $context Object with context variables.
     *
     * @return array $pieces Graph pieces to output.
     */
    public function schema_piece( $pieces, $context ) {
    	require_once( plugin_dir_path( __FILE__ ) . '/class-be-product-review.php' );
    	$pieces[] = new BE_Product_Review( $context );
    	return $pieces;
    }
    add_filter( 'wpseo_schema_graph_pieces', array( $this, 'schema_piece' ), 20, 2 );

    Each graph piece is packaged as a class. In this case, I’m using my custom BE_Product_Review class found in class-be-product-review.php in my plugin.

    Creating a class

    All graph piece classes should be implemented using the WPSEO_Graph_Piece interface (see here).

    class BE_Product_Review implements \WPSEO_Graph_Piece {
    
    }

    The interface requires your class contain two methods:

    1. generate() for generating the graph piece
    2. is_needed() for determining if this graph piece should be added

    Yoast SEO could add additional methods to the interface in the future, which would also be required in your class.

    To make my customizations a bit more future-proof, I’ll extend an existing class rather than build my own. If a future update to Yoast SEO adds another required method, my class will automatically have it because I’m extending a core class that will also include it.

    Extending an existing class

    All of the Yoast SEO schema classes can be found in /wordpress-seo/frontend/schema (see here). I’ll use the Article schema type as my base since a review is a more specific type of article.

    class BE_Product_Review extends \WPSEO_Schema_Article implements \WPSEO_Graph_Piece {
    
    }

    Setup the $context in __construct()

    The $context variable is an object that contains important information about the current page. It contains site-wide data (site_name, site_url) and page-specific data (id is the post ID, canonical is the permalink).

    See /wordpress-seo/frontend/schema/class-schema-context.php for more information.

    We need to make $context accessible in our class. Since we are extending another class, we need to pull the $context from the parent.

    If we were not extending an existing class, you could leave that line out and only have $this->context = $context;

    class BE_Product_Review extends \WPSEO_Schema_Article implements \WPSEO_Graph_Piece {
    
    	/**
    	 * A value object with context variables.
    	 *
    	 * @var WPSEO_Schema_Context
    	 */
    	private $context;
    
    	/**
    	 * Product_Rating constructor.
    	 *
    	 * @param WPSEO_Schema_Context $context Value object with context variables.
    	 */
    	public function __construct( WPSEO_Schema_Context $context ) {
    		parent::__construct( $context );
    		$this->context   = $context;
    	}
    }

    Customize is_needed()

    is_needed() is one of the two methods required by the WPSEO_Graph_Piece. Use this method to determine whether or not this graph piece is needed in this specific context.

    Since I’m extending the WPSEO_Schema_Article class, I could leave this method out of my class and it would use is_needed() from the parent class. But my review schema doesn’t apply in every instance that the article schema would, so I need to define my own display logic.

    For my BE_Product_Review class, I have a custom metabox that allows the editor to mark this as a product review using the be_product_review_include meta key.

    Rather than using get_the_ID() to get the post ID, we’re using $this->context->id.

    	/**
    	 * Determines whether or not a piece should be added to the graph.
    	 *
    	 * @return bool
    	 */
    	public function is_needed() {
    		$post_types = apply_filters( 'be_product_review_schema_post_types', array( 'post' ) );
    		if( is_singular( $post_types ) ) {
    			$display = get_post_meta( $this->context->id, 'be_product_review_include', true );
    			if( 'on' === $display ) {
    				return true;
    			}
    		}
    		return false;
    	}
    

    Generate the graph data

    The generate() method is where the actual magic happens. This is where you define the relevant schema data for this graph piece.

    Make sure you read Google’s guidelines for your schema type, and use the Structured Data Testing Tool often to make sure you have everything correct.

    In my case, the Product Review schema requirements include itemReviewed, reviewRating, reviewBody, and more.

    When building your graph piece, it’s important that you include an @id so it can be referenced by other graph pieces, as well as isPartOf so Yoast SEO knows where to place it in the graph. In my case, the review is part of the article so I have:

    'isPartOf' => array( '@id' => $this->context->canonical . WPSEO_Schema_IDs::ARTICLE_HASH ),

    I added a filter to the end of my $data before returning it so plugins/themes can customize the output if needed.

    To keep the generate() method as clean as possible, you can move logic to separate methods which are referenced inside this one.

    I created the get_review_meta( $key, $fallback ) method for retrieving the review metadata, and providing a fallback in case these required fields haven’t been filled out (name = post title; review body = post content).

    Below is the completed class. I recommend you look at the Product Review Schema for Yoast SEO plugin on GitHub for more information on how this all comes together:

    <?php
    /**
     * Class BE_Review
     */
    class BE_Product_Review extends \WPSEO_Schema_Article implements \WPSEO_Graph_Piece {
    
    	/**
    	 * A value object with context variables.
    	 *
    	 * @var WPSEO_Schema_Context
    	 */
    	private $context;
    
    	/**
    	 * Product_Rating constructor.
    	 *
    	 * @param WPSEO_Schema_Context $context Value object with context variables.
    	 */
    	public function __construct( WPSEO_Schema_Context $context ) {
    		parent::__construct( $context );
    		$this->context   = $context;
    	}
    
    	/**
    	 * Determines whether or not a piece should be added to the graph.
    	 *
    	 * @return bool
    	 */
    	public function is_needed() {
    		$post_types = apply_filters( 'be_product_review_schema_post_types', array( 'post' ) );
    		if( is_singular( $post_types ) ) {
    			$display = get_post_meta( $this->context->id, 'be_product_review_include', true );
    			if( 'on' === $display ) {
    				return true;
    			}
    		}
    		return false;
    	}
    
    	/**
    	 * Adds our Review piece of the graph.
    	 *
    	 * @return array $graph Review markup
    	 */
    	public function generate() {
    		$post          = get_post( $this->context->id );
    		$comment_count = get_comment_count( $this->context->id );
    
    		$data          = array(
    			'@type'            => 'Review',
    			'@id'              => $this->context->canonical . '#product-review',
    			'isPartOf'         => array( '@id' => $this->context->canonical . WPSEO_Schema_IDs::ARTICLE_HASH ),
    			'itemReviewed'     => array(
    					'@type'    => 'Product',
    					'image'    => array(
    						'@id'  => $this->context->canonical . WPSEO_Schema_IDs::PRIMARY_IMAGE_HASH,
    					),
    					'name'     => wp_strip_all_tags( $this->get_review_meta( 'name', get_the_title() ) ),
    			),
    			'reviewRating'     => array(
    				'@type'        => 'Rating',
    				'ratingValue'  => esc_attr( $this->get_review_meta( 'rating', 1 ) ),
    			),
    			'name'         => wp_strip_all_tags( $this->get_review_meta( 'name', get_the_title() ) ),
    			'description' => wp_strip_all_tags( $this->get_review_meta( 'summary', get_the_excerpt( $post ) ) ),
    			'reviewBody'  => wp_kses_post( $this->get_review_meta( 'body', $post->post_content ) ),
    			'author'           => array(
    				'@id'  => get_author_posts_url( get_the_author_meta( 'ID' ) ),
    				'name' => get_the_author_meta( 'display_name', $post->post_author ),
    			),
    			'publisher'        => array( '@id' => $this->get_publisher_url() ),
    			'datePublished'    => mysql2date( DATE_W3C, $post->post_date_gmt, false ),
    			'dateModified'     => mysql2date( DATE_W3C, $post->post_modified_gmt, false ),
    			'commentCount'     => $comment_count['approved'],
    			'mainEntityOfPage' => $this->context->canonical . WPSEO_Schema_IDs::WEBPAGE_HASH,
    		);
    		$data = apply_filters( 'be_review_schema_data', $data, $this->context );
    
    		return $data;
    	}
    
    	/**
    	 * Determine the proper publisher URL.
    	 *
    	 * @return string
    	 */
    	private function get_publisher_url() {
    		if ( $this->context->site_represents === 'person' ) {
    			return $this->context->site_url . WPSEO_Schema_IDs::PERSON_HASH;
    		}
    
    		return $this->context->site_url . WPSEO_Schema_IDs::ORGANIZATION_HASH;
    	}
    
    	/**
    	 * Product review meta
    	 *
    	 * @param string $key
    	 * @param string $fallback
    	 * @return string $meta
    	 */
    	private function get_review_meta( $key = false, $fallback = false ) {
    		$meta = get_post_meta( $this->context->id, 'be_product_review_' . $key, true );
    		if( empty( $meta ) && !empty( $fallback ) )
    			$meta = $fallback;
    		return $meta;
    	}
    }
    
  • How to Create an eBook Optin Form in WordPress

    Giving away eBooks, white papers, and other valuable content is great way to grow your email list.

    On Duane Smith’s website (one of my design partners), we recently added a Free Resources section with downloadable eBooks.

    Building the form

    With WPForms it’s easy to build a newsletter signup form. You can include a link to the PDF in the Confirmation Message shown after the form is submitted.

    In this case, I want to use the same form with dozens of eBooks. I don’t want to create a separate form for each download.

    Here’s the content creation workflow when adding a new eBook:

    1. Go to Pages > Add New
    2. Create a subpage of “Resources” with the title and summary of the eBook
    3. Include the “eBook Download” form we built with WPForms
    4. Upload the eBook in a custom metabox built with Advanced Custom Fields

    I customized the Confirmation Message text to include a button to download the PDF uploaded to this page.

    I’m appending our button to the end of the existing confirmation message because it’s the same for all downloads.

    <?php
    /**
     * WPForms, include eBook download in confirmation message.
     *
     * @link https://www.billerickson.net/wordpress-ebook-optin-form/
     *
     * @param array $form_data
     * @param array $form
     */
    function be_wpforms_ebook_download( $form_data, $form ) {
    	$attachment_id = get_post_meta( get_the_ID(), 'be_ebook', true );
    	if( !empty( $attachment_id ) )
    		wpforms()->frontend->confirmation_message .= '<div class="wp-block-button aligncenter"><a href="' . esc_url_raw( $attachment_id ) . '" class="wp-block-button__link">Download Now</a></div>';
    }
    add_action( 'wpforms_frontend_output_before', 'be_wpforms_ebook_download', 10, 2 );

    I recommend you place this in a Core Functionality plugin so you don’t lose the functionality when changing themes.

    Don’t let Google index your PDF

    Even though we don’t link to the PDF directly on any page – visitors can only access the link by submitting the form – Google may still find it by digging through your uploads directory.

    I recommend updating your robots.txt file to disallow indexing of PDF files. You can either edit it directly on the server via FTP, or use Yoast SEO’s file editor.

    Go to SEO > Tools > File Editor. Click “Create robots.txt file” if you don’t see the box shown below. Add Disallow: /*.pdf to the end of the file and click “Save changes to robots.txt”.

  • Default image for article in Yoast SEO Schema

    I’m a big fan of the new schema functionality in Yoast SEO. It includes all the information search engines want to see, and it’s incredibly extensible through filters and extending classes.

    If a post has a featured image, Yoast SEO will add a graph piece for primaryImageOfPage and will attach that to the Article graph piece. If there’s no featured image, no image is attached to the article.

    When an image is required

    I recently updated my site to be Native AMP, and the next day I received a Google Search Console email describing issues with some of my articles.

    Google has slightly different schema requirements for AMP pages. These are what caused my warnings above:

    • Every Article must have at least one image provided
    • The image must be at least 1200px wide

    To fix this issue, I’m using my site logo as the default image, which is set in SEO > Search Appearance > Organization Logo. Make sure the image you have uploaded there is at least 1200px wide.

    Yoast SEO Default Article Image

    /**
     * Default image in Article schema
     * If there is no featured image, or featured image is
     * < 1200px wide, use the site logo instead.
     *
     * @link https://www.billerickson.net/yoast-seo-schema-default-image/
     *
     * @param array $graph_piece
     * @return array
     */
    function be_schema_default_image( $graph_piece ) {
    	$use_default = false;
    	if( has_post_thumbnail() ) {
    		$image_src = wp_get_attachment_image_src( get_post_thumbnail_id(), 'full' );
    		if( empty( $image_src[1] ) || 1199 > $image_src[1] )
    			$use_default = true;
    	} else {
    		$use_default = true;
    	}
    
    	if( $use_default ) {
    		$graph_piece['image']['@id'] = home_url( '#logo' );
    	}
    	return $graph_piece;
    }
    add_filter( 'wpseo_schema_article', 'be_schema_default_image' );

    Note: this only applies to the Article graph piece. If you have other graph pieces that require a default image, add an additional filter to the bottom using the format wpseo_schema_{type}.

Display Posts Shortcode

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