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.

  • 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
     * @see 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:

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

     * WPForms, include eBook download in confirmation message.
     * @see 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.
     * @see 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}.

  • Building a Native AMP WordPress site

    After building a few AMP sites for clients, I was surprised by how easy it can be to build a completely custom theme that’s AMP compatible.

    I spent an hour yesterday converting this website to native AMP. You can check the AMP validator to see my posts are valid AMP pages.

    I’ll walk you through the basics of setting up an AMP site, discuss areas that tripped me up, and provide some helper functions to make AMP site development easier.

    Quick Links

    1. What is AMP?
    2. AMP WordPress plugin
    3. Three AMP template modes
    4. Fixing Validation Issues
    5. Plugins without JavaScript
    6. Navigation without JavaScript
    7. Google Analytics with AMP
    8. AMP helper functions

    What is AMP?

    AMP is Google’s approach to dramatically speeding up the display and development of websites (more info).

    AMP uses a limited and restricted form of markup they call AMP HTML. It also limits the amount of CSS you can have on a page to 50kb, and no JavaScript.

    With Cloudflare you can display your Real URL rather than the Google URL for your AMP cached website.

    Why should you use AMP?

    • AMP forces you to build an incredibly fast website, which helps your SEO because speed is a ranking factor.
    • Google makes those pages load even faster with pre-rendering, so when a user clicks your link it feels like an instantaneous, 0 second load time.
    • Google may reward you with placement in the Top Stories carousel above search results.
    • The AMP WordPress plugin includes a CSS Tree Shaker, which can drastically reduce the amount of CSS loaded on a page. This page is only loading 35% of my CSS.

    AMP WordPress Plugin

    The official AMP plugin takes care of most of the hard work. It converts your site’s markup to AMP HTML when viewing the AMP version of your site, so you only have to maintain standard HTML markup in your theme.

    It also includes CSS Tree Shaking, which determines exactly what CSS is needed on the current page and creates a page-specific stylesheet with just those styles. When you view the source of an AMP page, it includes a comment at the top showing you the breakdown of CSS:

    The style[amp-custom] element is populated with:
         0 B: style[amp-custom=]
        73 B (57%): link#amp-default-css[rel=stylesheet][id=amp-default-css][href=https://www.billerickson.net/wp-content/plugins/amp/assets/css/amp-default.css?ver=1.1.3][type=text/css][media=all]
       998 B (14%): link#shared-counts-css[rel=stylesheet][id=shared-counts-css][href=https://www.billerickson.net/wp-content/plugins/shared-counts/assets/css/shared-counts.min.css?ver=1.3.0][type=text/css][media=all]
       196 B (1%): link#wp-block-library-css[rel=stylesheet][id=wp-block-library-css][href=https://www.billerickson.net/wp-includes/css/dist/block-library/style.min.css?ver=5.2.1][type=text/css][media=all]
     18786 B (50%): link#ea-style-css[rel=stylesheet][id=ea-style-css][href=https://www.billerickson.net/wp-content/themes/billerickson-2019/assets/css/main.css?ver=1559684395][type=text/css][media=all]
      2664 B (23%): link#gistpress-css[rel=stylesheet][id=gistpress-css][href=https://github.githubassets.com/assets/gist-embed-a9a1cf2ca01efd362bfa52312712ae94.css?ver=5.2.1][type=text/css][media=all]
        74 B: a.monsterinsights-outbound-link amp-wp-1982de1[href=https://gist.github.com/billerickson/6c9fa9c3bce739f551685cfa3b6bf369/raw/9c04eb1e7b1a1f82fea7f5d87b3b6acd9e8e8c79/navigation.php][class=monsterinsights-outbound-link amp-wp-1982de1][data-vars-category=outbound-link][data-vars-action=https://gist.github.com/billerickson/6c9fa9c3bce739f551685cfa3b6bf369/raw/9c04eb1e7b1a1f82fea7f5d87b3b6acd9e8e8c79/navigation.php][data-vars-label=view raw]
        80 B: div.amp-wp-539b047[class=amp-wp-539b047]
    Total included size: 22,871 bytes (35% of 65,101 total after tree shaking)

    It adds a “Validate AMP” item to the admin bar so you can quickly see if the current page is valid or invalid:

    It also adds an “Error Index” in the backend that lets you track down which pages have errors and what plugin or theme is causing it.

    The errors shown below are from an old version of WPForms. The current version has full AMP compatibility, so all my pages with forms are fully AMP compatible.

    Three AMP template modes

    When you first install the AMP plugin, you’re asked which template mode you’d like to use.

    Native is the recommended approach, and means you serve only AMP pages to all visitors. Your canonical URL is AMP, and there’s no non-AMP versions of your pages. This requires the most work to implement because you have to meet the strict AMP requirements on your website that all users access.

    You can use native even when serving a mix of valid and invalidated pages (see below). This tutorial focuses on using the native template mode.

    Transitional lets you maintain your canonical URL as non-AMP, and create a paired AMP page using your own template files. Adding ?amp to the end of a URL serves up the AMP version used in Google’s mobile SERPs. This is a good option if you’re not able to fully implement your website in an AMP compatible way but want a paired down AMP version that still looks like your website. It’s named transitional because you can use it until you can make a Native AMP version.

    Reader is the most basic option. Your main (canonical) URLs are non-AMP and use your theme, but the paired AMP version uses a completely different theme provided by the AMP plugin.

    Fixing Validation Issues

    When you first install AMP and turn on Native mode, it will run a few pages through the validator. You’ll likely have errors from your theme and plugins loading JavaScript.

    When I converted my site to AMP, I started by going through the plugins and figuring out what changes were necessary to ensure AMP compatibility.

    I then went through the JavaScript files in my theme to determine what I could live without (Recommended Reading on the right no longer floats next to content), what I’d leave in place as invalid pages (live search on Code Snippets), and what I needed to rebuild without Javascript (my mobile menu).

    Plugins without JavaScript

    Any time you install a plugin, AMP re-runs the validation on a few key pages to see if it broke anything. You’ll often be notified of validation errors because the plugin included JavaScript.

    When a validation error occurs, you have a few options to consider:

    Reject validation error, leave page invalidated

    If it only affects a few pages, you should consider the costs & benefits of leaving the page invalidated.

    By default, the AMP plugin will sanitize validation errors, removing the JavaScript, but you can “Reject” the validation error from being sanitized.

    The page will technically no longer be an AMP page (the amp attribute is removed from the HTML element), but you can still benefit from all the performance improvements and AMP components already on the page.

    It won’t hurt your SEO or make the site inaccessible to users.

    A contact form is a good example. Many contact form plugins use JavaScript for field validation, conditional logic, spam protection, and more. Your contact page will never appear in a news carousel, so the cost of your contact page being invalidated is minimal. The benefit of including a form on your contact page is high.

    However, if your form appears in every post (like my Newsletter Signup form) you will need to use an AMP-compatible form plugin like WPForms.

    Disable JS in the plugin

    Many plugins that include JavaScript will provide some method of disabling it using a filter. Before you disable the JavaScript, I recommend reading through it to see exactly what it does so you know what features won’t work without it.

    I use Shared Counts for social sharing buttons on articles, which loads a JavaScript file for the following features:

    • Resizing the popup windows. When you click the Twitter icon, the new window is small and sized appropriately for the tweet.
    • Email Sharing. If you use the “Share by Email” button, it creates a popup with an email sharing form.
    • Social Tracking in Google Analytics. When someone clicks a sharing button, it triggers a Google Analytics event so you can track your social shares in GA.

    I’m not using the Email Sharing button, and the other JS feature aren’t super important to me. Here’s how to disable JavaScript in Shared Counts (more information):

    add_filter( 'shared_counts_load_js', '__return_false' );

    Note: Shared Counts will soon be AMP compatible, so you’ll get all the technical benefits (email sharing, social tracking…) without having to make any code changes on your AMP site.

    On a client site, I used a similar approach to disable JavaScript in Yoast Local SEO:

    add_filter( 'wpseo_local_load_jquery', '__return_false' );

    On another client website we used the Page Links To plugin for a few redirects. It loads a global JavaScript file for opening a redirect in a new window if you have that selected in the backend. We disabled the JavaScript because we didn’t need that feature.

    I’m using my helper function ea_is_amp() to only disable it if AMP is active.

     * Remove scripts
    function ea_amp_remove_scripts() {
    	if( ! ea_is_amp() )
    	wp_dequeue_script( 'page-links-to' );
    add_action( 'wp_enqueue_scripts', 'ea_amp_remove_scripts', 20 );

    Build the functionality without a plugin

    You can’t always remove the JavaScript from a plugin – it’s often vital to the functionality.

    If you’re building a Native AMP website, be prepared to custom build features that you otherwise might have used a plugin for.

    There’s a huge number of AMP components you can use to build your own custom functionality. For instance, in the upcoming Shared Counts release for AMP compatibility we are using amp-lightbox, amp-form, amp-moustache, and amp-recaptcha-input to build the email sharing popup and form.

    You might find it easier to build functionality with AMP than without because you can leverage all of these pre-built components. To make a modal popup window, simply enqueue the amp-lightbox script and wrap your modal content in <amp-lightbox layout="nodisplay">

    Navigation without JavaScript

    My typical mobile menu toggles CSS classes when a user clicks the mobile menu icon or a submenu expanding icon.

    You can replicate this functionality using AMP.setState(). Start by building your element in the default state. In this case, I have a mobile menu toggle button:

    <button class="mobile-menu-toggle"></button>

    We’ll then use [class] to specify how the class should change based on a variable mobileMenuActive. If that variable is true then the button should have a class of mobile-menu-toggle active, and if it is false it should have a class of mobile-menu-toggle.

    <button [class]="mobileMenuActive ? 'mobile-menu-toggle active' : 'mobile-menu-toggle'" class="mobile-menu-toggle"></button>

    Then when the button is tapped, set the value of mobileMenuActive to the opposite of its current value. In other words, if the mobile menu is inactive, on tap it should become active.

    <button [class]="mobileMenuActive ? 'mobile-menu-toggle active' : 'mobile-menu-toggle'" class="mobile-menu-toggle" on="tap:AMP.setState({mobileMenuActive: !mobileMenuActive})"></button>

    Finally, let’s toggle a class on the menu itself, adding a class of active if mobileMenuActive is true.

    <nav [class]="mobileMenuActive ? 'nav-primary nav-menu active' : 'nav-primary  nav-menu'" class="nav-primary nav-menu"></nav>

    This can get a bit confusing, so I wrote some helper functions to simplify the code (see below). My mobile menu toggle now looks like:

    echo '<button' . ea_amp_class( 'mobile-menu-toggle', 'active', 'mobileMenuActive' ) . ea_amp_toggle( 'mobileMenuActive' ) . '></button>';

    And my nav menu looks like:

    echo '<nav' . ea_amp_class( 'nav-primary nav-menu', 'active', 'mobileMenuActive' ) . '></nav>';

    Submenu Toggles

    We use the same approach for submenu toggles, but each one updates a different variable so you know which submenu is open.

    I use walker_nav_menu_start_el to add my submenu toggle to any menu item that has children.

     * Add a dropdown icon to top-level menu items.
     * @param string $output Nav menu item start element.
     * @param object $item   Nav menu item.
     * @param int    $depth  Depth.
     * @param object $args   Nav menu args.
     * @return string Nav menu item start element.
    function ea_nav_add_dropdown_icons( $output, $item, $depth, $args ) {
    	// Only add to 'primary' menu.
    	if ( ! isset( $args->theme_location ) || 'primary' !== $args->theme_location ) {
    		return $output;
     	// Only add to items with children
    	if ( in_array( 'menu-item-has-children', $item->classes, true ) ) {
    		// Add SVG icon to parent items.
    		$icon = ea_icon( array( 'icon' => 'drop-down', 'size' => 24 ) );
    		$output .= sprintf(
    			'<button' . ea_amp_nav_dropdown( $args->theme_location, $depth ) . ' tabindex="-1">%s</button>',
    	return $output;
    add_filter( 'walker_nav_menu_start_el', 'ea_nav_add_dropdown_icons', 10, 4 );

    I’m also using a helper function (shown below) for generating all the AMP code with a unique variable based on the theme location, submenu index, and depth.

    When a submenu toggle is clicked, its class changes to .submenu-expand.expanded. We then display the sub-menu with the following CSS:

    .submenu-expand.expanded + .sub-menu {
    	display: block;

    Add Fallback JavaScript

    For simple yet important features like navigation, it’s a good idea to provide a JavaScript fallback in case AMP is ever disabled.

    You can see the global.js file in my starter themes for the JavaScript I am using. I only load global.js if ! ea_is_amp().

    If the AMP plugin is disabled, the JavaScript file loads and the navigation still works.

    Genesis makes it easier

    This is one of the areas where Genesis simplifies AMP development. Genesis 3.0 includes AMP integration as a key feature.

    Rather than adding all the code above yourself, you can use the genesis_register_responsive_menus() to tell Genesis to add it for you.

    // Registers the responsive menus.
    		'script' => array(
    			'menuIconClass'       => 'ionicons-before ion-ios-menu',
    			'menuIconOpenedClass' => 'ionicons-before ion-ios-menu',
    			'subMenuIconClass'    => 'ionicons-before ion-ios-arrow-down',
    			'menuClasses'         => array(
    				'combine' => array(
    				'others'  => array(),

    You can use this feature right now if you’re using the latest version of Genesis.

    Google Analytics with AMP

    Analytics with AMP is tricky. You can’t simply drop your Google Analytics code in your site because it’s JavaScript.

    There’s an AMP component built specifically for this, amp-analytics, which can be used for Google Analytics or any other analytics tool. You specify your analytics settings and triggering events in a JSON object.

    If you have the AMP for WordPress plugin installed, go to AMP > Analytics to configure (screenshot).

    Alternatively, you can use Monster Insights Pro which is 100x easier. Their AMP addon will automatically generate the correct analytics code for you, and it will include all of the additional tracking they provide (ex: form conversions, file downloads, outbound links…).

    Monster Insights also includes Session Unification for when a visitor goes from an AMP page to a non-AMP page on your site, if you’re using Transitional or Reader mode.

    I use Monster Insights for my website and recommend it to my clients.

    AMP Helper Functions

    To simplify the readability and re-usability of the AMP-specific code on this website, I built the following helper functions. These can be found in /inc/amp.php in both of my starter themes.

    • ea_is_amp() A conditional to use when applying AMP-specific changes. For instance, for enqueuing a JavaScript file if ea_is_amp() is false.
    • ea_amp_class( $default, $active, $state ) This adds all the code for toggling CSS classes based on a state variable.
    • ea_amp_toggle( $state, $disable ) This adds the state variable toggle code, so when a button is clicked the variable toggles true/false. You can also pass an array of other state variables to set to false on click (ex: close the search form when opening the mobile nav)
    • ea_amp_nav_dropdown( $theme_location, $depth ) Generates a unique variable name for submenu toggles, then uses it with ea_amp_class() and ea_amp_toggle().
     * AMP functionality
     * @package      EAStarter
     * @author       Bill Erickson
     * @since        1.0.0
     * @license      GPL-2.0+
     * Is AMP?
     * Conditional tag
    function ea_is_amp() {
    	return function_exists( 'is_amp_endpoint' ) && is_amp_endpoint();
     * Generate a class attribute and an AMP class attribute binding.
     * @param string $default Default class value.
     * @param string $active  Value when the state enabled.
     * @param string $state   State variable to toggle based on.
     * @return string HTML attributes.
    function ea_amp_class( $default, $active, $state ) {
    	$output = '';
    	if( ea_is_amp() ) {
    		$output .= sprintf(
    			' [class]="%s"',
    					'%s ? \'%s\' : \'%s\'',
    					$default . ' ' . $active,
    	$output .= sprintf( ' class="%s"', esc_attr( $default ) );
    	return $output;
     * Add the AMP toggle 'on' attribute.
     * @param string $state State to toggle.
     * @param array $disable, list of states to disable
     * @return string The 'on' attribute.
    function ea_amp_toggle( $state = '', $disable = array() ) {
    	if( ! ea_is_amp() )
    	$settings = sprintf(
    		'%1$s: ! %1$s',
    		esc_js( $state )
    	if( !empty( $disable ) ) {
    		foreach( $disable as $disableState ) {
    			$settings .= sprintf(
    				', %s: false',
    				esc_js( $disableState )
    	return sprintf(
    		' on="tap:AMP.setState({%s})"',
     * AMP Nav Dropdown toggle and class attributes.
     * @param string $theme_location Theme location.
     * @param int    $depth          Depth.
     * @return string The class and on attributes.
    function ea_amp_nav_dropdown( $theme_location = false, $depth = 0 ) {
    	$key = 'nav';
    	if( !empty( $theme_location ) )
    		$key .= ucwords( $theme_location );
    	global $submenu_index;
    	$key .= 'SubmenuExpanded' . $submenu_index;
    	if( 1 < $depth )
    		$key .= 'Depth' . $depth;
    	return ea_amp_toggle( $key ) . ea_amp_class( 'submenu-expand', 'expanded', $key );
  • Access block data with PHP using parse_blocks() and render_block()

    The Gutenberg block editor organizes your content into discrete blocks, and WordPress includes functions for easily accessing the individual blocks within a post.

    Here’s a quick summary of how the functions work, and examples of them in use.

    Jump to Section:

    1. parse_blocks()
    2. render_block()
    3. Display blockquote from post
    4. Table of contents from headings
    5. ACF block data


    This function converts the HTML comments and markup stored in post_content into an array of parsed block objects.

    Usage: $blocks = parse_blocks( $post->post_content );

    The post_content of my Style Guide looks like:

    <!-- wp:paragraph {"fontSize":"large"} -->
    <p class="has-large-font-size">Lorem <strong>ipsum</strong> dolor <em>sit amet</em>, consectetur <a class="map-link" href="https://maps.google.com/?q=Houston,+TX">adipiscing elit</a>, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
    <!-- /wp:paragraph -->
    <!-- wp:heading {"level":1} -->
    <h1>Heading 1</h1>
    <!-- /wp:heading -->

    After running it through parse_blocks(), it looks like:

        [0] => Array
                [blockName] => core/paragraph
                [attrs] => Array
                        [fontSize] => large
                [innerBlocks] => Array
                [innerHTML] => 
    <p class="has-large-font-size">Lorem <strong>ipsum</strong> dolor <em>sit amet</em>, consectetur <a class="map-link" href="https://maps.google.com/?q=Houston,+TX">adipiscing elit</a>, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
                [innerContent] => Array
                        [0] => 
    <p class="has-large-font-size">Lorem <strong>ipsum</strong> dolor <em>sit amet</em>, consectetur <a class="map-link" href="https://maps.google.com/?q=Houston,+TX">adipiscing elit</a>, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
        [1] => Array
                [blockName] => 
                [attrs] => Array
                [innerBlocks] => Array
                [innerHTML] => 
                [innerContent] => Array
                        [0] => 
        [2] => Array
                [blockName] => core/heading
                [attrs] => Array
                        [level] => 1
                [innerBlocks] => Array
                [innerHTML] => 
    <h1>Heading 1</h1>
                [innerContent] => Array
                        [0] => 
    <h1>Heading 1</h1>


    This function takes a single parsed block object and returns the rendered HTML for that block.

    Usage: echo render_block( $block );

    Display blockquote from post

    On my portfolio archive page, every 4th project includes a blockquote. You could pull this directly from the content of the portfolio item by looping through the blocks and displaying the core/quote block, if one is found.

    Add this function to your theme, then call be_display_post_blockquote() to display the quote.

    Table of contents from headings

    In a previous tutorial I showed how to dynamically build a table of contents by parsing the HTML.

    Converting the HTML to blocks makes this much easier. You loop through the blocks and find the core/heading ones.

    The example below makes a simple ordered list of all the headings in the page. You could take this a step further and use $block['attrs']['level'] to make a nested list based on the heading level (ex: h3 is subordinate to h2).

    * List post headings
    * @see https://www.billerickson.net/access-gutenberg-block-data/
    function be_list_post_headings() {
    global $post;
    $blocks = parse_blocks( $post->post_content );
    $headings = array();
    foreach( $blocks as $block ) {
    if( 'core/heading' === $block['blockName'] )
    $headings[] = wp_strip_all_tags( $block['innerHTML'] );
    if( !empty( $headings ) ) {
    echo '<ol class="table-of-contents">';
    foreach( $headings as $heading )
    echo '<li>' . $heading . '</li>';
    echo '</ol>';
    view raw functions.php hosted with ❤ by GitHub

    Unfortunately WordPress does not store the heading’s ID as an anchor attribute, so you’ll still need to manipulate the HTML if you want to create anchor links (example).

    ACF block data

    If you have built a custom block with Advanced Custom Fields, you can easily access all the block data using this method.

    ACF generates dynamic blocks. Rather than storing actual HTML, it stores all of the block data in the HTML comments and then dynamically renders it using the PHP file or function you specify.

    I’m working on a site that includes a “Service” block. It has fields for title, anchor link, content, and “success story” (a linked post from elsewhere).

    The homepage includes links to the top-level pages, as well as anchor links to each individual service block. I’m using parse_blocks() to find all of the acf/service blocks, then building the links using the $block['attrs'] data.

    * Get service sections
    * @see https://www.billerickson.net/access-gutenberg-block-data/
    function ea_get_service_sections() {
    $sections = array();
    global $post;
    $blocks = parse_blocks( $post->post_content );
    foreach( $blocks as $block ) {
    if( 'acf/service' !== $block['blockName'] )
    $title = $anchor = '';
    if( !empty( $block['attrs']['data']['title'] ) )
    $title = $block['attrs']['data']['title'];
    if( !empty( $block['attrs']['data']['anchor'] ) )
    $anchor = $block['attrs']['data']['anchor'];
    if( empty( $anchor ) )
    $anchor = $title;
    $sections[] = '<a href="' . get_permalink() . '#' . sanitize_title_with_dashes( $anchor ) . '">' . esc_html( $title ) . '</a>';
    if( empty( $sections ) )
    echo '<ul class="service-sections">';
    foreach( $sections as $section )
    echo '<li>' . $section . '</li>';
    echo '</ul>';
    view raw functions.php hosted with ❤ by GitHub
  • Don’t “start the engine” in your Genesis child theme

    Written by Gary Jones, originally published on GaryJones.io, and reposted here with permission.

    There’s a line in the Genesis Sample theme, commented with “Start the engine”. All of the child themes by StudioPress have it. So do most child themes in the community.

    However, it’s not needed.

    More specifically, it doesn’t have to be needed.

    It seems innocuous enough, so what’s the problem?

    This article looks at the drawbacks of this start the engine approach while exploring some of the advantages an alternative setup function may provide.

    Start The Engine

    Let’s start at the beginning.

    Child themes for the Genesis Framework by StudioPress, typically have a functions.php file that attempts to Start the engine:

    //* Start the engine
    include_once( get_template_directory() . '/lib/init.php' );

    This line has been present since the very first version of Sample, and as such, it’s in every single StudioPress theme, and by a de facto standard, many child themes in the Genesis community.

    Most people who have a passing understanding of PHP within WordPress themes should understand the code snippet above. Line one takes us into PHP mode, line two is a single-line comment, and line three is where the fun starts. It includes, once, a file called init.php within the lib/ sub-directory of the template directory (that’s the parent theme, aka Genesis, to you and me).

    What Does init.php Do?

    The init.php file within Genesis is the initialisation file. Here’s what it does (in Genesis 2.6 at least):

    • Sets up an autoloader for Genesis class files.
    • Calls a genesis_pre action hook.
    • Defines a genesis_i18n() function for loading Genesis language files, and hooks it into the genesis_init hook.
    • Defines a genesis_theme_support() function for adding theme support for Genesis and WordPress features, and hooks it into the genesis_init() hook.
    • Defines a genesis_post_type_support() function for enabling certain Genesis functionality for Posts and Pages, and hooks it into the genesis_init() hook.
    • Defines a genesis_post_type_support_post_meta() function for enabling Genesis functionality for post meta on public post types that are not Pages, and hooks it into the genesis_init() hook.
    • Defines a genesis_constants() function for defining PHP constants that Genesis uses within itself, and available to plugins and themes, and hooks it into the genesis_init() hook.
    • Defines a genesis_load_framework() function for loading in all the files, and hooks it into the genesis_init() hook.
    • Calls a genesis_init action hook, which therefore triggers calls to each function referenced above.
    • Calls a genesis_setup action hook.

    Basically, that start the engine line makes every single bit of Genesis Framework load up straight away.

    Advantages of the Start the Engine Approach

    Let’s briefly discuss what some might say are the advantages of this method:

    • It’s one line. It’s easy to copy and paste, and even to remember, but you should know that lines of code are not an indicator of the quality of the code.
    • It loads all of Genesis up-front. The rest of the code in the functions.php file can reference Genesis functions, and the theme author doesn’t have to worry whether they are defined or not.

    That’s probably about it.

    Before going through the disadvantages, I want to introduce an alternative, so that we can make a comparison.

    The Alternative: A Setup Function

    The alternative is a setup function. Credit where credit is due, I call this the Bill Erickson Approach, as it was Genesis legend Bill Erickson who I first saw suggest this (though he may have got it elsewhere).

    [Note from Bill: I got the idea from the default WordPress theme at the time, TwentyTwelve]

    Here’s a trimmed example from Utility Pro by Carrie Dils. We’ll go through the reasoning in a moment:

    add_action( 'genesis_setup', 'utility_pro_setup', 15 );
    * Theme setup.
    * Attach all of the site-wide functions to the correct hooks and filters. All
    * the functions themselves are defined below this setup function.
    * @since 1.0.0
    function utility_pro_setup() {
    define( 'CHILD_THEME_NAME', 'utility-pro' );
    define( 'CHILD_THEME_URL', 'https://store.carriedils.com/utility-pro' );
    define( 'CHILD_THEME_VERSION', '1.0.0' );
    // Add HTML5 markup structure.
    add_theme_support( 'html5', array( 'caption', 'comment-form', 'comment-list', 'gallery', 'search-form' ) );
    // Add viewport meta tag for mobile browsers.
    add_theme_support( 'genesis-responsive-viewport' );
    // Add support for custom background.
    add_theme_support( 'custom-background', array( 'wp-head-callback' => '__return_false' ) );
    // Add support for accessibility features
    add_theme_support( 'genesis-accessibility', array( 'skip-links', 'headings' ) );
    // Add support for three footer widget areas.
    add_theme_support( 'genesis-footer-widgets', 3 );
    // Add support for additional color style options.
    'utility-pro-purple' => __( 'Purple', 'utility-pro' ),
    'utility-pro-green' => __( 'Green', 'utility-pro' ),
    'utility-pro-red' => __( 'Red', 'utility-pro' ),
    // Add support for structural wraps (all default Genesis wraps unless noted).
    'footernav', // Custom.
    'menu-footer', // Custom.
    'home-gallery', // Custom.
    // Add support for two navigation areas (theme doesn't use secondary navigation).
    'primary' => __( 'Primary Navigation Menu', 'utility-pro' ),
    'footer' => __( 'Footer Navigation Menu', 'utility-pro' ),
    // Add custom image sizes.
    add_image_size( 'feature-large', 960, 330, true );
    // Unregister secondary sidebar.
    unregister_sidebar( 'sidebar-alt' );
    // Unregister layouts that use secondary sidebar.
    genesis_unregister_layout( 'content-sidebar-sidebar' );
    genesis_unregister_layout( 'sidebar-content-sidebar' );
    genesis_unregister_layout( 'sidebar-sidebar-content' );
    // Register the default widget areas.
    // Load files in admin.
    if ( is_admin() ) {
    // Add suggested plugins nag.
    include get_stylesheet_directory() . '/includes/suggested-plugins.php';
    // Add theme license (don't remove, unless you don't want theme support).
    include get_stylesheet_directory() . '/includes/theme-license.php';
    view raw functions.php hosted with ❤ by GitHub

    Now that we’ve seen an example, let’s explain what it is.

    What is a Setup Function?

    setup function is one (or more) function(s) that includes much of the theme’s setup code, such as adding or removing theme support, adding or removing post type support, adding image sizes, defining menus for Genesis, registering widget areas, etc.

    It’s not just for child themes, either. All of the default Twenty* themes have their own setup function (here’s TwentySeventeen’s), and Justin Tadlock has written about creating a theme setup function for themes in general.

    For Genesis, we don’t call the setup function immediately, rather we hook it into the action hook that only fires after Genesis has finished loading, genesis_setup. Since Genesis itself also hooks into genesis_setup, we use a later priority of 15, to make sure the child theme adjustments come last.

    While the example shows a single function, it would be equally possible to have multiple functions that organise the code into logical groups of function calls, and hook them all into the same hook and priority.

    Advantages of the Setup Function Approach

    There are a few advantages with this suggested approach that are worthy of explanation. These are:

    • Not including a file from another code base.
    • Honouring the default WordPress load order.
    • Fewer function calls in the global scope / everything is hooked in.
    • Allows functionality to be hooked in before Genesis loads.

    Not including a file from another code base

    Our code is inherently coupled to the file name of another code base (Genesis), and this is a bad idea. If it ever gets renamed (and with Genesis 3.x, that is a possibility), your code will throw a fatal error. You could do a file_exists() check, but if doesn’t exist, how will the rest of your code behave? Your code is still coupled to looking for that specific file name.

    The solution brings us to our next point.

    Honouring the Default WordPress Load Order

    The whole point of child themes historically requiring that Genesis file, is so all of Genesis loads up first and we can use the Genesis functions.

    But here’s a little secret.

    The default behaviour for WordPress is to load the parent theme after the child theme. It will happen automatically. We should honour that behaviour.

    The reason for letting this happen is simple. We stick to the expected behaviour for parent and child themes. We let WordPress load the Genesis functions.php file, which in turn loads the file we would have tried to load in our code.

    If we don’t call any Genesis functions in the global scope (i.e. outside of a function hooked into a hook), then we don’t need Genesis to load first.

    Fewer Function Calls in the Global Scope

    PHP has scopes. Here, we’re considering two scopes: function scope and global scope.

    Function scope is where we assign a variable, or make a function call, from inside a function.

    Global scope is where the same thing might happen, but outside of a function.

    function foobar() {
     // This is in the function scope.
    // This is in the global scope.

    Any function call in the global scope is called when the file is first loaded – that is, required or included by another file. In the case of a child theme functions.php file, that would be when WordPress is loading the (child) theme.

    So why is this bad?

    As plugin developers, we might want to alter how a theme runs, but if the theme makes a function call before we have a chance to intercept, we can’t make those changes.

    However, if the critical function calls are all inside another (setup) function, which is hooked in to be called at a known time later, we can write plugin code to handle that; we can unhook that (setup) function, or have the plugin code run at an earlier or later priority as needed.

    With a setup function, the theme itself could also handle specific edge cases that need a different setup i.e. for landing page templates.

    It’s worth noting that this setup function approach isn’t for performance optimisation, but it certainly won’t noticeably impact performance either. The increase in executed byte-code is negligible within the request.

    Allows Functionality to be Hooked in Before Genesis Loads

    Delaying our code until after Genesis loads is all well, but can we use it to our advantage? Yes, we can.

    As an example, let’s consider widget areas. By default, WordPress admin shows widget areas in the order they were registered, so all of our custom widget areas would typically appear below Header Right, Primary Sidebar and Secondary Sidebar.

    If we create another setup function, but hook that into an earlier hook, or with a different priority, we can set things up before Genesis does. For example, if we display a Utility Bar widget area at the top of the site’s front-end, we could register it to appear in the Widgets admin page before Header Right. This may be more intuitive for users since the widget areas then follow the order they do on the front end.

    Utility Bar widget area registered before the Header Right widget area from Genesis.

    What Should You Put Into Your Setup Function?

    So, should you put all of your global function calls into the setup function?

    No, and here’s why.

    Action and Filter Additions

    Most add_action() and add_filter() can stay outside. There’s nothing special about these functions that has anything to do with Genesis, even if the callback they reference does. Keeping these by the callback function definitions makes code considerably easier to read, rather than having the function in one place and the context (hook) under which that function is called several hundred lines away or in a different file.

    Could we move the add_action() call and the function definition into the setup function? This would create nested functions.

    Nested Functions

    Technically, PHP allows nested user-defined function. That is:

    function foo() {
     // ...
     function bar() {
     // ...

    However, we should generally avoid nested functions, since you can’t call foo() more than once. Calling it a second time would make PHP attempt to define bar() again. Since a function by that name already exists from when foo() was first called, this would cause a fatal error.

    In this case, it stops functions from being re-usable and should be avoided.

    Action And Filter Removals

    Unlike their add counterparts, the global remove_action() and remove_filter() calls may need to be in the setup function. One of the common gotchas is calling a remove_action() in the global scope, before Genesis has had a chance to add_action() in the first place. By including the remove_action() in the setup function, which in turn is not called until we know Genesis has finished setting up, the remove_action() intent will work.

    WordPress Functions

    WordPress functions like add_image_size()add_theme_support() and setting the global $content_width don’t need to be in the setup function, but equally, it makes sense to have them there, since they are part of setting up the theme behaviour.


    Since the majority of customisations in Genesis Framework child themes require a hooked-in function, it makes perfect sense to add some of those global calls into one or more functions and hook them in as well.

    Next time you’re editing a theme, consider not starting the engine by tying your code to Genesis internal file structure. Instead, take advantage of the WordPress event system and hook in your changes after WordPress automatically loads Genesis.

  • Disable Genesis Customizer Redirect

    A guide on how to access the old Genesis Theme Settings page.

    The most recent Genesis release (2.10) moved the theme settings to the Customizer. If you try accessing the old theme settings page, it redirects you to the customizer with the Theme Settings panel open.

    Here are a few reasons why you may want to access the old theme settings page:

    The redirect does not apply to additional settings pages that you have created, or those added by plugins like Genesis Grid and Genesis 404.

    These approaches will only work with the 2.x branch of Genesis (ex: Genesis 2.10 and any other 2.x releases that come later). Genesis 3.0 will remove these pages completely, not simply redirect them.

    There are two ways you can access the old theme settings page.

    Include &noredirect in URL

    If you only need to access the settings page once, this is the simplest approach.

    Hover over Genesis > Theme Settings, right click and select “Copy Link Address”. Paste that URL in the address bar, then add &noredirect to the end of it. This tells Genesis to bypass the redirect.

    The URL will be: https://yoursite.copm/wp-admin/admin.php?page=genesis&noredirect

    If you access theme settings often, you might prefer a more permanent solution.

    Disable the redirect

    It only takes a few lines of code to disable Genesis from ever redirecting to the customizer. You can then access the theme settings the way you always have, by clicking Genesis > Theme Settings.

    Install the plugin

    I’ve packaged this code into a plugin so you can easily install it without having to edit code. Go to Plugins > Add New > Upload and upload this zip file.

    Download Now

    * Disable Genesis Customizer Redirect
    * @see https://www.billerickson.net/disable-genesis-customizer-redirect
    function be_disable_genesis_customizer_redirect() {
    global $_genesis_admin_settings, $_genesis_admin_seo_settings;
    $_genesis_admin_settings->redirect_to = '/wp-admin/admin.php?page=genesis&noredirect';
    $_genesis_admin_seo_settings->redirect_to = '/wp-admin/admin.php?page=seo-settings&noredirect';
    add_action( 'genesis_admin_menu', 'be_disable_genesis_customizer_redirect', 11 );
    view raw functions.php hosted with ❤ by GitHub
  • Limit Modified Date, WordPress Plugin

    I often update my older posts with additional information to keep them fresh and accurate. I include both the “Published” date and “Last Updated” date at the top of posts so users know the history and accuracy of the article.

    The WordPress function get_the_modified_date() can be used to list this date, but there’s one small problem. It changes for any update, no matter how minor.

    If I’m fixing a typo or adjusting the categories on the post, I don’t want the modified date to change. So I built a plugin.

    Limit Modified Date

    This plugin works with both the Gutenberg block editor and classic editor.

    View Plugin

    This plugin adds a checkbox to the Publish box that lets you prevent the modified date from updating. Leave this checked while you’re making your minor content changes.

    When it’s time for a notable content update, uncheck the box and save the post.

    Gutenberg Compatible

    Whether you’re using the Classic Editor or the new Gutenberg block editor, this plugin will work great for you.

  • Shared Counts 1.3: Twitter counts, Google Analytics integration, and more

    We just released version 1.3 of Shared Counts, which includes some great new features and improvements.

    Twitter counts using TwitCount

    Our previous provider of Twitter share counts shut down, so we’ve added support for the new leader in Twitter counts, TwitCount.

    Sign up for free on the TwitCount website, then on your website go to Settings > Shared Counts and check “Include Twitter Counts”.

    Social Interaction tracking with Google Analytics

    When someone clicks one of your Shared Counts buttons, we now trigger a Google Analytics event for social interaction tracking. You can access this data in Acquisition > Social > Plugins.

    Stats in the Admin Bar

    When viewing a post, the admin bar now includes a button showing the total shares and when the cache was last updated. Hovering over it will display the breakdown by service. Clicking the button will refresh the share counts on that page immediately.

    Pinterest Addon Updates

    The Shared Counts – Pinterest Image addon has also been updated. Previously the separate Pinterest image would only be used when clicking a Shared Counts button. Now the image also appears in other tools like the Pinterest browser extension and Tailwind.

    Full Changelog


    • Filter for changing services used by location: shared_counts_display_services, see #69.
    • Admin bar stats, see #32.
    • Support for Twitter counts using TwitCount.com, props @robert-gillmer, see #62.
    • Automatic social share tracking with Google Analytics, see #50.
    • Add support for fastcgi_finish_request when updating counts, see #12.
    • Specific services can be defined in shortcode via services attribute (comma separated), see #69.


    • Pass post_id to needs_updating method, see #74.
    • Removed support for Google+ and StumbleUpon (RIP).
    • Default letter-spacing to normal on button labels, see #56.
    • Hide Total Counts button if empty and “Hide Empty Counts” setting is enabled, see #44.


    • Pinterest JS API conflict, see #34.
    • Multiple spaces between some CSS classes inside markup, see #64.
    • Twitter URL encoding issue with special characters in text, see #54.
    • Email counts not tracking, props @thartl, see #67.
    • Showing “Preserve HTTP Counts” setting when Count Source is None.
    • Enabling various settings by default on initial save.
    • When sorting posts by share count in the admin, posts with zero shares are now included, see #76.
    • Data attributes are filterable, see #73

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