Flexible WordPress Shortcodes with Locate Template
Christopher Davis has written this article. More details coming soon.
Underlying the entire WordPress template hierarchy is the locate_template function. locate_template pretty easy one to understand: it checks the child theme for the template, then the parent theme and either returns the template path it found or loads it for you.
locate_template.php
The idea is you pass it an array of template names with the more specific template names at the beginning. For instance, WordPress creates the array ["single-{$post->post_type}.php", "single.php"] when it loads single templates.<?php
function locate_template($template_names, $load = false, $require_once = true ) { $located = ''; foreach ( (array) $template_names as $template_name ) { if ( !$template_name ) continue; if ( file_exists(STYLESHEETPATH . '/' . $template_name)) { $located = STYLESHEETPATH . '/' . $template_name; break; } elseif ( file_exists(TEMPLATEPATH . '/' . $template_name) ) { $located = TEMPLATEPATH . '/' . $template_name; break; } } if ( $load && '' != $located ) load_template( $located, $require_once ); return $located; }
Shortcodes are a way to pull blocks of content (or whatever you want) into post content. We’ll make an imaginary shortcode that renders pulls in a little feature box for another post. You might use it like
[pmg_post_snippet id="123"]
and get something back like…
The code for this is pretty simple. We’ll wrap it up in a class to keep the hooks into init and callbacks for add_shortcode close by.
SnippetShortcode.php
<?php
class SnippetShortcode { public function connect() { add_action('init', [$this, 'addShortcode']); } public function disconnect() { remove_action('init', [$this, 'addShortcode']); } public function addShortcode() { add_shortcode('pmg_post_snippet', [$this, 'showSnippet']); } public function showSnippet($atts) { global $post; $atts = shortcode_atts([ 'id' => null, ], $atts, 'pmg_post_snippet'); if (!$atts['id']) { return; } $_post = get_post($atts['id']); if (!$_post || 'publish' !== $_post->post_status) { return; } $post = $_post; setup_postdata($post); ob_start(); ?> <a class="pmg-post-snippet" style="display: block"> <h2><?php the_title(); ?></h2> <?php the_excerpt(); ?> </a> <?php $out = ob_get_clean(); wp_reset_postdata(); return $out; } }
This small example exposes a big weakness to this approach: it’s nearly impossible for an end user to modify the content of the shortcode. Even if that end user will only ever be you, some flexibility is a good thing to build.
A way around this would be to add a filter that lets a user short circuit the display part of the shortcode callback.
SnippetShortcodeFilter.php
<?php
class SnippetShortcode { public function showSnippet($atts) { // shortcode_atts, get_post, etc all up here if (false !== $out = apply_filters('pmg_post_snippet_content', false, $_post)) { return $out; } // render as normal } }
This isn’t bad, but I think there’s a better approach that makes it easy for theme developers to integrate with third party shortcodes and make them look good.
Rather than hard coding the HTML into the shortcode, use locate_template to look for a template in the theme to render the shortcode content. Fall back to your own template if one isn’t found in the themes.
SnippetShortcodeLocate.php
<?php
class SnippetShortcode { public function showSnippet($atts) { global $post; // shortcode_atts, get_post, etc all up here $post = $_post; setup_postdata($post); $template = locate_template('pmg-post-snippet.php') ?: __DIR__.'/view.php'; ob_start(); self::safeInclude($template, $post); $out = ob_get_clean(); wp_reset_postdata(); return $out; } private static function safeInclude($template, $post) { require $template; } }
Now all a theme developer has to do is drop a pmg-post-snippet.php template in their theme to customize the output. As a nice side effect, it’s a great way to separate the view part of your shortcode from the logic aspect(s) — like checking to see if a post is published, etc.
The only thing of note in the example above is the safeInclude static method that’s used to avoid giving the included template access to the objects $this variable or any of the other variables in the showSnippet method.
Because we want to control the context passed to the included template. In the example above, I wanted to give the template access only to the $post variable, not the usual set of the global variables that load_template sets up.
get_template_part also doesn’t allow additional context (other variables) to be passed in. This isn’t a big deal in this example, but becomes more important with complicated shortcodes.
This same pattern works great for hiding re-usable theme components behind a function and when get_template_part won’t suffice for the reasons outlined above.
For example, we wrote a small abstractions around loading a template to render a grid of posts this week for a client.
example.php
Stay in touch
Subscribe to our newsletter
By clicking and subscribing, you agree to our Terms of Service and Privacy Policy
The custom query object meant get_template_part was not going to work without messing with the global $wp_query variable.<?php
function clientname_theme_postgrid(\WP_Query $query, callable $termsCb) { $template = locate_template('parts/frontpage-postgrid.php'); if ($template) { require $template; } }