5

I've coded a plugin that creates a shortcode, among other things. That shortcode expects a post_id parameter, it extracts the content of that post and it returns the filtered content of that post (e.g. it applies all filters so that any other shortcode in that content is done). Here is a cut down version of the code:

public static function myshortcode($atts = null, $content = "")
{
  global $wp_filter, $merged_filters, $wp_current_filter, $post;

  if (is_array($atts))
    extract($atts, EXTR_OVERWRITE);

  $save_wp_filter = $wp_filter;
  $save_merged_filters = $merged_filters;
  $save_wp_current_filter = $wp_current_filter;
  $post = get_post($post_id);
  setup_postdata($post);
  $content = $post->post_content;
  $content = apply_filters('the_content', $content);        
  $content = str_replace(']]>', ']]>', $content);
  $wp_filter = $save_wp_filter;
  $merged_filters = $save_merged_filters;
  $wp_current_filter = $save_wp_current_filter;

  return $content;
}

All works great, except that other plugins shortcodes fail to include their own CSS files (e.g. Ultimate Addons for Visual Composer has its own CSS files to include, but it does not include them when called this way). I suspect the problem is that when WP executes the "the_content" hook for my shortcode (not the one I call explicitly, but the hook that WP itself calls in the first place and that leads to my shortcode execution) it is already too late to include CSS files in the page (e.g. the <head> tag is already closed), and Wordpress, at that point, has given a chance to include CSS files only to the plugins it sees as needed for the page (e.g. only my plugin, because the page initially contains only my shortcode).

I could use wp_enqueue_style to explicitly add the required third party CSS files, but the problem is I want to include them in a portable way. Today the problem is Ultimate Addons for VC, tomorrow will be Content Egg or something else. The best solution would be to call the function of WP which makes the decision about what are the required plugins for the page and calls them, handing that function the the actual content the page will have after my shortcode will be executed, instead of the content the page has before my shortcode is done.

Does such WP function exist?

EDIT:

For clarification after comments, yes, my shortcode is like [embed_page id="123"], except I need it the other way around, e.g. [embed_post id="123"] in a page. There's no risk of looping because it is meant to be used only to embed posts into pages, and it's not meant to be used in posts. And finally yes, the embedded content does include other third party shortcodes that should automatically enqueue their own CSS, JS, whatever.

EDIT AFTER THE RECEIVED ANSWERS:

The short answer is no, there doesn't exist such function in Wordpress. The long answer is below. I'd like to reward both @majick and @cjbj for their extensive and detailed answers, but I have to choose one...

  • I'm not sure I fully understand your question, but one point to note is that your shortcode will mess with the main query unless you include a call to wp_reset_postdata() before your function returns. It's possible that that will fix any broken behaviour. – Andy Macaulay-Brook May 19 '16 at 16:28
  • Ah - do you mean that you have, for example, a shortcode called embed_page? If I make a new page and put in [embed_page id="123"] then when my page is viewed, your shortcode is filled by the properly filtered content of page 123? And if so, page 123 may have shortcodes within it that only queue their JS & CSS when the plugins detect that the current page's content contains those shortcodes? – Andy Macaulay-Brook May 19 '16 at 16:34
  • @Andy. The way I read it page 123 may even contain [embed_page id="456"], setting off a recursive train that at one point may contain [embed_page id="123"] again - and you're in an endless loop. – cjbj May 19 '16 at 19:03
  • @cjbj I hadn't looked that far ahead yet. – Andy Macaulay-Brook May 19 '16 at 19:51
  • Checking against developer.wordpress.org/reference/functions/… should help that – jgraup May 20 '16 at 11:04
4

I think you could get around this by pre-running the shortcodes on the page by applying the content filters before the header is output. This should allow any internal shortcodes run inside the included post to add any action hooks properly and thus any needed stylesheets/resources.

add_action('wp_loaded','maybe_prerun_shortcodes');
function maybe_prerun_shortcodes() {
    if (is_page()) {
        global $post; 
        if (has_shortcode($post->post_content,'embed_post')) {
            $content = apply_filters('the_content',$post->post_content);
        }
    }
}

EDIT The above will only enqueue stylesheets and resources that are enqueued within any shortcode calls in the embedded post (which, although it is better practice, is actually not done very often.) It should work for those cases because applying the filters to the current page content will run the embed shortcode which then applies filters for the embedded post (in your existing function.)

However, To get the stylesheets for a particular post you would have to fetch that particular post (which is why the iframe method works, well kind of, given the problems already discussed about that.)

My suggested workaround in that case is to store the style resources loaded on a post when the post is loaded...

add_filter('style_loader_tag','custom_store_style_tags',11,2)
function custom_store_style_tags($tag,$handle) {
    global $styletags; $styletags[$handle] = $tag;}
}

add_action('wp_footer','custom_save_style_tags');
function custom_save_style_tags() {
    global $embedstyles;
    if (is_single()) {
        global $styletags, $post; 
        update_post_meta($post->ID,'styletags',$styletags);
    } elseif (is_array($embedstyles)) {
        global $styletags;
        foreach ($embedstyles as $handle => $tag) {
            if (!array_key_exists($handle,$styletags)) {echo $tag;}
        }
    }
}

Then you can use the shortcode to retrieve the style tags to be retrieved and the second part of the above function will output them in the footer:

public static function myshortcode($atts = null, $content = "")
{
  // EXISTING CODE SNIPPED

  global $embedstyles;
  $embedstyle = get_post_meta($post_id,'styletags',true);
  if (is_array($embedstyle)) {
      if (!is_array($embedstyles)) {$embedstyles = array();}
      $embedstyles = array_merge($embedstyle,$embedstyles);
  }

  return $content;
}

And you could do the same for scripts via script_loader_tag filter and simply changing the variables to match...

Of course there is still the possibility some body classes may not be targeted by styles as already mentioned by @cjbj... or some scripts may initialize in the footer even if they are enqueued properly.

...and it does not account for inline styles/scripts printed (not enqueued) on the page yet... but it's a good start... depends how far you want to go with it.

  • I've upvoted this because it seemed a great idea. However I've now tried implementing it but nothing particular happens as far as enqueued CSS are concerned. I assume that calling apply_filters in a wp_loaded hook would make other plugins enqueue their CSS, but it did not happen, Can you please add any details about how this is supposed to work? – Lucio Crusca May 23 '16 at 11:38
  • added some clarification plus a different approach... – majick May 25 '16 at 1:04
3
+50

This may be a silly idea, but it might work:

1 Let your shortcode explode to an iframe with a custom query_var (tutorial). Like this:

<iframe src="http://www.example.com/?p=123&my_query_var=content_only"></iframe>

2 In your single.php detect the query_var and in that case skip visual header, sidebars, footer - anything not connected to the post content itself.

In this way you load the post in the usual way and avoid all problems with other shortcodes not completely doing what they should do.

EDIT - CLARIFICATION AFTER COMMENTS

The philosophy behind this approach is that you have no idea what third party plugins do, perhaps even independent of the shortcode execution. So, if you try to catch what the shortcode does, you may still miss things. For instance, a shortcode may lead to the generation of html and the inclusion of a css file, but the plugin may also add a body_class upon which the css depends. There is no way of knowing. So, to make sure that the external plugin/shortcode behaves as expected, the best way is to have WP do a full page load. Hence the iframe.

Now, this leaves you with the problem that you get a full page, including header, menu, sidebars and so on. So, you need a template that only runs the_title() and the_content() and maybe other parts of the post. In some way you have to tell WP that you don't want this. WP must know if it is supposed to generate a normal single.php or a bare bones one. This is where the query_var comes in. By adding it to the url as generated in your shortcode, you give WP a clue that this is not a normal call to single.php. This is still relatively easy, because you can set up a custom query_var with just a few lines of code.

Things become complicated if you want your shortcode in a plugin, because then you also don't know what the theme does. You will have to overrule the theme and have to insert your bare bones single.php. Since you don't know how the theme is built, you won't be sure if the post has the same format as in the theme. You could also have your plugin analyse single.php and remove undesired elements, but that is a wild guess. Also there may be custom templates like single-custompage.php. So if you follow this path, it may be best to include an option page that lets users set the styles of posts that are included in pages.

  • Is step 2 dependent on a particular theme or is it possible to accomplish that in a portable way, regardless of the specific theme being used? – Lucio Crusca May 20 '16 at 16:27
  • As I wrote it down it depends on the theme. But it is also possible to create a template with a plugin (tutorial). In that plugin you would have to include a filter that intercepts the normal way single.php is called (codex) – cjbj May 20 '16 at 16:36
  • Seems to me you have clear ideas about what to do, but I fail to understand the whole process. In particular I don't understand how the php code of a template that shows only the content can be theme independent. I understand I can write that code in the plugin, thanks to the tutorial. I don't understand what functions I should call in order to get only the post content, but with all styles applied, in a theme independent way. Could you please elaborate on that? – Lucio Crusca May 21 '16 at 7:15
  • I've added a clarification to the original answer. If you do not control the theme it is quite impossible to know what classes and css it applies. The css that is relevant to the content() may be attached to the body or some other div that you exclude if you only run the_content(). For instance the theme may set the styles for <h2> on the body, the main column, the article or the post content level. So, if you generate the bare bones single.php you need, you cannot foresee the css classes you need to apply. – cjbj May 21 '16 at 10:51
2
  • Load visual compose css

    function get_visual_composer_style($id) {
        return '<style>' . get_post_meta( $id, '_wpb_shortcodes_custom_css', true  ) .                 '</style>';
    }
    
  • Load css based on shortcode

    function custom_shortcode_scripts() {
        global $post;
        if( is_a( $post, 'WP_Post' ) && has_shortcode( $post->post_content, 'custom-shortcode') ) {
           wp_enqueue_script( 'custom-script');
        }
    }
    add_action( 'wp_enqueue_scripts', 'custom_shortcode_scripts');
    
  • Is this solution portable or Visual Composer specific? I need portable code, independent on any particular plugin, – Lucio Crusca May 20 '16 at 20:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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