Conditionally Loading JavaScript/CSS for Shortcodes

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.

Read More

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.

Related posts

7 comments

  1. 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;
    }
    
  2. 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 <mike@newclarity.net>
     */
    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.

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

  4. 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' );
    
  5. Finally I also found a solution for conditional css loading which works for my plugin http://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' );
    
  6. 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' );
    
  7. 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…

Comments are closed.