41

I released a plugin that creates a shortcode and requires a JavaScript file and a CSS file to load on any page that contains that shortcode. I could just make the script/style load on all pages, but that's not the best practice. I only want to load the files on pages that call the shortcode. I've found two methods of doing this, but both have problems.

Method 1 sets a flag to true inside the shortcode handler function, and then checks that value inside a wp_footer callback. If it's true, it uses wp_print_scripts() to load the JavaScript. The problem with this is that it only works for JavaScript and not CSS, because CSS should be declared inside <head>, which you can only do during an early hook like init or wp_head.

Method 2 fires early and "peeks ahead" to see if the shortcode exists in the current page content. I like this method much better than the first, but the problem with it it won't detect if the template calls do_shortcode().

So, I'm leaning towards using the second method and then trying to detect if a template is assigned, and if so, parse it for the shortcode. Before I do that, though, I wanted to check if anyone knows of a better method.

Update: I've integrated the solution into my plugin. If anyone is curious to see it fleshed out in a live environment you can download it or browse it.

Update 2: As of WordPress 3.3, it's now possible to call wp_enqueue_script() directly inside a shortcode callback, and the JavaScript file will be called within the document's footer. That's technically possible for CSS files as well, but should be considered a bad practice because outputting CSS outside the <head> tag violates W3C specs, can case FOUC, and may force the browser to re-render the page.

10
  • I've used a variation of Method 1 in the past. Load JS for the shortcode in the footer, load CSS for the shortcode in-line. It works, but is hackish. Looking forward to a better solution.
    – EAMann
    Jun 23, 2011 at 15:14
  • The main problems with inline CSS are that it is difficult to override with other styles, and it doesn't use the wp_enqueue_styles() method.
    – Ian Dunn
    Jun 23, 2011 at 16:05
  • How much css do you have?
    – mfields
    Jun 23, 2011 at 16:35
  • How many lines of javascript?
    – mfields
    Jun 23, 2011 at 16:36
  • 3
    The javascript is the easy part. Best wat to do this is under "Jedi" here: scribu.net/wordpress/optimal-script-loading.html
    – mfields
    Jun 23, 2011 at 16:55

7 Answers 7

11

Based on my own experience, I've used a combination of method 1 & 2 - the architecture and footer scripts of 1, and the 'look-ahead' technique of 2.

For the look-ahead though, I use regex in place of stripos; personal preference, faster, and can check for 'malformed' shortcode;

preg_match( '#\[ *shortcode([^\]])*\]#i', $content );

If you're concerned about authors using do_shortcode manually, I would opt to instruct them to use an action call enqueue your pre-registered style manually.

UPDATE: For the lazy author who never RTFM, output a message to highlight the error of their ways ;)

function my_shortcode()
{
    static $enqueued;
    if ( ! isset( $enqueued ) )
        $enqueued = wp_style_is( 'my_style', 'done' ); // cache it so we don't repeat if called over and over

    // do shortcode
    $output = '';

    if ( ! $enqueued )
        // you can output the message on first occurence only by wrapping it in the previous if
        $output .= <<<HTML
<p>Attention! You must enqueue the shortcode stylesheet yourself if calling <code>do_shortcode()</code> directly!</p>
<p>Use <code>wp_enqueue_style( 'my_style' );</code> before your <code>get_header()</code> call inside your template.</p>
HTML;

    return $output;
}
9
  • That's a good point. Ideally I'd like it to work without them having to do anything extra -- because half the time they probably won't read the FAQ first, so they'll just assume it's broken -- but I might end up doing that. I could register the scripts on every page, but only enqueue them if I detect a shortcode. Then, users could hook into init and call the enqueue functions in specific templates where needed, assuming it's not already too late in the execution at that point. Also, WP has get_shortcode_regex() built-in.
    – Ian Dunn
    Jun 23, 2011 at 18:54
  • 3
    If users are adept at implementing do_shortcode(), is it not reasonable to assume that they are likewise adept at following instructions for enqueueing the shortcode style? Jun 23, 2011 at 19:16
  • 1
    True, but it'll get the regex for all shortcodes, not just yours ;) "I could register the scripts on every page" Probably the better method too! Note they don't need to hook into init, just anywhere before wp_head. For the lazy developer, check wp_style_is( 'my_style_handle', 'done' ) inside your shortcode. If it's false, print out a visible error that instructs them what to do. Jun 23, 2011 at 19:17
  • @Chip - I'm not worried that they're not capable of following the instructions, just that they won't know they're supposed to, since 99% of the time you don't need to do anything extra.
    – Ian Dunn
    Jun 23, 2011 at 22:05
  • 1
    @Ian my thinking was that adding do_shortcode() to the template is already "do[ing] something extra" - and users that would do that something-extra would either already know about the need to enqueue the style, or else would be more willing/likely to follow special instructions. Jun 23, 2011 at 23:03
9

I'm late answering this question but since Ian started this thread on the wp-hackers list today it made me think it worth answering especially considering I've been planning to add such a feature to some plugins I've been working on.

An approach to consider is to check on the first page load to see if the shortcode is actually used and then save the shortcode usage status to a post meta key. Here's how:

Step-by-Step How-To

  1. Set a $shortcode_used flag to 'no'.
  2. In the shortcode function itself set the $shortcode_used flag to 'yes'.
  3. Set a 'the_content' hook priority 12 which is after WordPress has processed shortcodes and check post meta for a '' using the key "_has_{$shortcode_name}_shortcode". (A value of '' is returned when a post meta key doesn't exist for the post ID.)
  4. Use a 'save_post' hook to delete the post meta clearing the persistent flag for that post in case the user changes shortcode usage.
  5. Also in the 'save_post' hook use wp_remote_request() to send a non-blocking HTTP GET to the post's own permalink to trigger to first page load and the setting of the persistent flag.
  6. Lastly set a 'wp_print_styles' and check post meta for a value of 'yes', 'no' or '' using the key "_has_{$shortcode_name}_shortcode" . If the value is 'no' don't serve the external. If the value is 'yes' or '' go ahead and serve the external.

And that should do it. I've written and tested an example plugin to show how this all works.

Example Plugin Code

The plugin wakes up on a [trigger-css] shortcode which sets the <h2> elements on the page to white-on-red so you can easily see it working. It assumes a css subdirectory containing style.css file with this CSS in it:

/*
 * Filename: css/style.css
 */
h2 {
  color: white;
  background: red;
}

And below is the code in a working plugin:

<?php
/**
 * Plugin Name: CSS on Shortcode
 * Description: Shows how to conditionally load a shortcode
 * Author: Mike Schinkel <[email protected]>
 */
class CSS_On_Shortcode {

  /**
   * @var CSS_On_Shortcode
   */
  private static $_this;

  /**
   * @var string 'yes'/'no' vs. true/false as get_post_meta() returns '' for false and not found.
   */
  var $shortcode_used = 'no';

  /**
   * @var string
   */
  var $HAS_SHORTCODE_KEY = '_has_trigger-css_shortcode';
  /**
   *
   */
  function __construct() {
    self::$_this = $this;
    add_shortcode( 'trigger-css', array( $this, 'do_shortcode' ) );
    add_filter( 'the_content', array( $this, 'the_content' ), 12 ); // AFTER WordPress' do_shortcode()
    add_action( 'save_post', array( $this, 'save_post' ) );
    add_action( 'wp_print_styles', array( $this, 'wp_print_styles' ) );
  }

  /**
   * @return CSS_On_Shortcode
   */
  function this() {
    return self::$_this;
  }

  /**
   * @param array $arguments
   * @param string $content
   * @return string
   */
  function do_shortcode( $arguments, $content ) {
    /**
     * If this shortcode is being used, capture the value so we can save to post_meta in the 'the_content' filter.
     */
    $this->shortcode_used = 'yes';
    return '<h2>THIS POST WILL ADD CSS TO MAKE H2 TAGS WHITE ON RED</h2>';
  }

  /**
   * Delete the 'has_shortcode' meta value so that it can be regenerated
   * on first page load in case shortcode use has changed.
   *
   * @param int $post_id
   */
  function save_post( $post_id ) {
    delete_post_meta( $post_id, $this->HAS_SHORTCODE_KEY );
    /**
     * Now load the post asynchronously via HTTP to pre-set the meta value for $this->HAS_SHORTCODE_KEY.
     */
    wp_remote_request( get_permalink( $post_id ), array( 'blocking' => false ) );
  }

  /**
   * @param array $args
   *
   * @return array
   */
  function wp_print_styles( $args ) {
    global $post;
    if ( 'no' != get_post_meta( $post->ID, $this->HAS_SHORTCODE_KEY, true ) ) {
      /**
       * Only bypass if set to 'no' as '' is unknown.
       */
      wp_enqueue_style( 'css-on-shortcode', plugins_url( 'css/style.css', __FILE__ ) );
    }
   }

  /**
   * @param string $content
   * @return string
   */
  function the_content( $content ) {
    global $post;
    if ( '' === get_post_meta( $post->ID, $this->HAS_SHORTCODE_KEY, true ) ) {
      /**
       * This is the first time the shortcode has ever been seen for this post.
       * Save a post_meta key so that next time we'll know this post uses this shortcode
       */
      update_post_meta( $post->ID, $this->HAS_SHORTCODE_KEY, $this->shortcode_used );
    }
    /**
     * Remove this filter now. We don't need it for this post again.
     */
    remove_filter( 'the_content', array( $this, 'the_content' ), 12 );
    return $content;
  }

}
new CSS_On_Shortcode();

Example Screenshots

Here's a series of screenshots

Basic Post Editor, No Content

Post Display, No Content

Basic Post Editor with [trigger-css] Shortcode

Post Display with [trigger-css] Shortcode

Not Sure if it's 100%

I believe the above should work in almost all cases but as I just wrote this code I can't be 100% sure. If you can find situations where it doesn't work I'd really like to know so I can fix the code in some plugins I just added this to. Thanks in advance.

6
  • So five plugins using your approach will trigger five remote requests each time a post is saved? I’d rather use a regex on post_content. And what about shortcodes in widgets?
    – fuxia
    Jan 2, 2013 at 7:02
  • @toscho The triggering of post loading is actually optional; it's only there to ensure a user doesn't have to see the first page load with the externals loaded. It's also a non-blocking call so in theory you shouldn't notice it. For our code we are doing in a base class so the base class can handles doing it only once. We could hook the 'pre_http_request' hook and disable multiple calls to the same URL while the 'save_post' hook is active, but I'd want to wait until w really saw a need for that, no? As for Widgets it could be enhanced to handle but it's not a use-case I've looked at yet. Jan 2, 2013 at 7:59
  • 1
    @toscho - Also you can't be sure that the shortcode is there as another hook may clear it. The only way you can be sure is if the shortcode function actually fires. So the regex approach isn't 100% reliable. Jan 2, 2013 at 8:03
  • I know. There is no bullet-proof way to inject CSS for shortcodes (except using <style>).
    – fuxia
    Jan 2, 2013 at 8:05
  • I'm working with an implementation based on this solution and I just wanted to point out that this method will not enqueue for a post/page preview.
    – mor7ifer
    Aug 2, 2015 at 14:45
6

Googling found me a potential answer. I say "potential" as in it looks good, should work, but I'm not 100% convinced it's the best way to do it:

add_action( 'wp_print_styles', 'yourplugin_include_css' );
function yourplugin_include_css() {
    // Check if shortcode exists in page or post content
    global $post;

    // I removed the end ' ] '... so it can accept args.
    if ( strstr( $post->post_content, '[yourshortcode ' ) ) {
        echo $csslink;
    }
}

This should be able to check if the current post is using a shortcode and add a stylesheet to the <head> element appropriately. But I don't think it will work for an index (i.e. multiple posts in the loop) page ... It's also from a 2-yr old blog post, so I'm not even sure it will work with WP 3.1.X.

11
  • 1
    This wont't work if the shortcode has arguments. If you really want to go this way, which is slow, use WP's get_shortcode_regex() for searching. Jun 23, 2011 at 15:39
  • Like I said, "potential" answer that I haven't tested yet ... :-)
    – EAMann
    Jun 23, 2011 at 15:39
  • That's basically the same as method 2, but it still doesn't check the template for do_shortcode() calls.
    – Ian Dunn
    Jun 23, 2011 at 15:45
  • Why it would need to do that? If you call do_shortcode() manually in the template then you already know you will be running the shortcode Jun 23, 2011 at 15:50
  • I'm not the one calling the shortcode, the user is. This is for a distributed plugin, not a private one.
    – Ian Dunn
    Jun 23, 2011 at 16:06
2

Using a combination of TheDeadMedic's answer and the get_shortcode_regex() documentation (which actually didn't find my shortcodes), I created a simple function used to enqueue scripts for multiple shortcodes. Since the wp_enqueue_script() in shortcodes only adds to the footer, this can be helpful as it can handle both header and footer scripts.


function add_shortcode_scripts() {
    global $wp_query;   
    $posts = $wp_query->posts;
    $scripts = array(
        array(
            'handle' => 'map',
            'src' => 'http://maps.googleapis.com/maps/api/js?sensor=false',
            'deps' => '',
            'ver' => '3.0',
            'footer' => false
        ),
        array(
            'handle' => 'contact-form',
            'src' => get_template_directory_uri() . '/library/js/jquery.validate.min.js',
            'deps' => array( 'jquery' ),
            'ver' => '1.11.1',
            'footer' => true
        )   
    );

    foreach ( $posts as $post ) {
        foreach ( $scripts as $script ) {
            if ( preg_match( '#\[ *' . $script['handle'] . '([^\]])*\]#i', $post->post_content ) ) {
                // enqueue css and/or js
                if ( wp_script_is( $script['handle'], 'registered' ) ) {
                    return;
                } else {
                    wp_register_script( $script['handle'], $script['src'], $script['deps'], $script['ver'], $script['footer'] );
                    wp_enqueue_script( $script['handle'] );
                }
            }
        }
    }
}
add_action( 'wp', 'add_shortcode_scripts' );
1

Finally I also found a solution for conditional css loading which works for my plugin www.mapsmarker.com and I´d like to share with you. It checks if my shortcode is used within the current template file and header/footer.php and if yes, enqueues the needed stylesheet in the header:

  function prefix_template_check_shortcode( $template ) {
    $searchterm = '[mapsmarker';
    $files = array( $template, get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'header.php', get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'footer.php' );
    foreach( $files as $file ) {
        if( file_exists($file) ) {
            $contents = file_get_contents($file);
            if( strpos( $contents, $searchterm )  ) {
                wp_enqueue_style('
leafletmapsmarker', LEAFLET_PLUGIN_URL . 'leaflet-dist/leaflet.css');
                  break; 
            }
        }
    }
  return $template;
  }  
  add_action('template_include','prefix_template_check_shortcode' );
1
  • bit of an aside but doesn't this assume that people are using header.php and footer.php. What about theme wrapping methods such those described by scribu.net/wordpress/theme-wrappers.html ? or themes like roots that keep their template parts elsewhere?
    – orionrush
    Mar 3, 2013 at 12:32
1

For my plugin I found that sometimes users have a theme builder that has shortcode stored in post meta data. Here is what I am using to detect whether my plugin shortcode is present in current post or post meta data:

function abcd_load_my_shorcode_resources() {
       global $post, $wpdb;

       // determine whether this page contains "my_shortcode" shortcode
       $shortcode_found = false;
       if ( has_shortcode($post->post_content, 'my_shortcode') ) {
          $shortcode_found = true;
       } else if ( isset($post->ID) ) {
          $result = $wpdb->get_var( $wpdb->prepare(
            "SELECT count(*) FROM $wpdb->postmeta " .
            "WHERE post_id = %d and meta_value LIKE '%%my_shortcode%%'", $post->ID ) );
          $shortcode_found = ! empty( $result );
       }

       if ( $shortcode_found ) {
          wp_enqueue_script(...);
          wp_enqueue_style(...);
       }
}
add_action( 'wp_enqueue_scripts', 'abcd_load_my_shorcode_resources' );
0

because CSS should be declared inside <head>

For CSS files you can load them within your shortcode output:

<style type="text/css">
  @import "path/to/your.css"; 
</style>

Set a constant or something after this, like MY_CSS_LOADED (only include the CSS if the constant is not set).

Both your methods are slower than going this way.

For JS files you can do the same if the script you're loading is unique and doesn't have any outside dependencies. If this is not the case load it inside the footer, but use the constant to determine if needs to be loaded or not...

14
  • 3
    Loading CSS outside the <head> element isn't proper markup. True, validation is just a guideline, but if we're trying to stick to that guideline it makes loading the stylesheet within the shortcode output a bad idea.
    – EAMann
    Jun 23, 2011 at 15:29
  • Inline CSS blocks are valid markup, even in XHTML from what I remember. There's no reason not to use them when you have no other acceptable alternative Jun 23, 2011 at 15:32
  • 1
    According to W3C's validation tool: <style type="text/css"> The element named above was found in a context where it is not allowed. This could mean that you have incorrectly nested elements -- such as a "style" element in the "body" section instead of inside "head". So inline styles (<element style="..."></element>) are valid, but inline <style> elements aren't.
    – EAMann
    Jun 23, 2011 at 15:36
  • 1
    make it filterable and any other plugin or theme can do as they like with it. If they configure the filter to return an empty string - nothing will be printed.
    – mfields
    Jun 23, 2011 at 16:51
  • 1
    You didn't mention any objective reasons against this practice. Anyway it doesn't matter; I only see two options here: Always load CSS/scripts (optimize them for size), or conditional inline styles Jun 23, 2011 at 17:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.