WordPress filter to modify final html output
Asked Answered
D

13

88

WordPress has great filter support for getting at all sorts of specific bits of content and modifying it before output. Like the_content filter, which lets you access the markup for a post before it's output to the screen.

I'm trying to find a catch-all filter that gives me one last crack at modifying the final markup in its entirety before output.

I've browsed the list of filters a number of times, but nothing jumps out at me: https://codex.wordpress.org/Plugin_API/Filter_Reference

Anyone know of one?

Ditto answered 21/4, 2009 at 13:10 Comment(1)
here is the smartest solution I've spotted https://mcmap.net/q/242934/-how-to-edit-the-html-of-frontend-code-before-output-on-wordpressPassepartout
B
22

AFAIK, there is no hook for this, since the themes uses HTML which won't be processed by WordPress.

You could, however, use output buffering to catch the final HTML:

<?php
// example from php.net
function callback($buffer) {
  // replace all the apples with oranges
  return (str_replace("apples", "oranges", $buffer));
}
ob_start("callback");
?>
<html><body>
<p>It's like comparing apples to oranges.</p>
</body></html>
<?php ob_end_flush(); ?>
/* output:
   <html><body>
   <p>It's like comparing oranges to oranges.</p>
   </body></html>
*/
Bikaner answered 21/4, 2009 at 13:20 Comment(2)
You can use php register_shutdown_function to end buffering, and retrieve html.Cockpit
This has a one drawback, you cannot call ob_start, ob_clean, .. inside the callback, which is needed for particular caching logic. php.net/manual/en/…Paleontology
F
90

WordPress doesn't have a "final output" filter, but you can hack together one. The below example resides within a "Must Use" plugin I've created for a project.

Note: I haven't tested with any plugins that might make use of the "shutdown" action.

The plugin works by iterating through all the open buffer levels, closing them and capturing their output. It then fires off the "final_output" filter, echoing the filtered content.

Sadly, WordPress performs almost the exact same process (closing the open buffers), but doesn't actually capture the buffer for filtering (just flushes it), so additional "shutdown" actions won't have access to it. Because of this, the below action is prioritized above WordPress's.

wp-content/mu-plugins/buffer.php

<?php

/**
 * Output Buffering
 *
 * Buffers the entire WP process, capturing the final output for manipulation.
 */

ob_start();

add_action('shutdown', function() {
    $final = '';

    // We'll need to get the number of ob levels we're in, so that we can iterate over each, collecting
    // that buffer's output into the final output.
    $levels = ob_get_level();

    for ($i = 0; $i < $levels; $i++) {
        $final .= ob_get_clean();
    }

    // Apply any filters to the final output
    echo apply_filters('final_output', $final);
}, 0);

An example of hooking into the final_output filter:

<?php

add_filter('final_output', function($output) {
    return str_replace('foo', 'bar', $output);
});

Edit:

This code uses anonymous functions, which are only supported in PHP 5.3 or newer. If you're running a website using PHP 5.2 or older, you're doing yourself a disservice. PHP 5.2 was released in 2006, and even though Wordpress (edit: in WP version < 5.2) STILL supports it, you should not use it.

Filefish answered 2/4, 2014 at 16:54 Comment(19)
Note: I've only tested this in version 3.8.Filefish
Thank you very much, this is the most elegant solution.Velda
Awesome, definitely by far most elegant!Vector
I suggest to change the very last parameter of add_action from 0 to something bigger like 999 (from the codex: Lower numbers correspond with earlier execution (...)) to be sure to be the very last function to manipulate the output.Vector
@Robert, if you do this, then WP's default action will run for the shutdown action, which will flush all of the ob levels. The default action does not capture the output, just flushes it, so the final_output filter will capture nothing. I believe, at least.Filefish
@Robert, sorry, were you talking about add_action('shutdown') or add_action('final_output')? If the latter, then yes, increasing it would not be a bad idea.Filefish
I'm talking about add_action('shutdown'). But you're right: something bigger than 0 won't be executed anymore.Vector
Why count(ob_get_level()) ? actually ob_get_level() returns the level (int). Try while(ob_get_level()) $final .= ob_get_clean();Tourbillion
Thanks @Wiliam, nice catch. I removed the count(), as that was a mistake, since ob_get_level() returns the actual count. Moving to a while block is unnecessary, and will incur additional calls to ob_get_level() with no added benefitFilefish
Note: Works with 4.2.4Reactive
Since posting, I've used this on many more project, and with all version of WP since 3.8. I haven't run into any problems.Filefish
Note that slight modification of this code can very effectively help you to get rid of "extra space" bug, it is when some php outputs have an extra trailing space; sometimes this bug can lead to very odd errors, especially in cases when it occurs in AJAX requests. Just append to $final only in case the text piece has no trailing space, that's it. This is literally the only solution of a trailing space bug in case when infected piece of codes are in DB.Holpen
Worked like a charm - 4.9.8Squally
Works fine but broke cache (Comet Cache)Squally
This broke functionality of login in my site.Tubercular
Breaks "wp super cache", pages are not cached. Apart from that, it's a nice idea that needs some refinements and can come in handy for certain development pourposes.Systematize
Haven't tested this in the wild yet, but on my local environment I had it up and running in a second! Awesome!Coccid
When I tried this solution, my page was returned out of order. I needed to use $final = ob_get_clean() . $final; in order to get things to come out in the correct order.Desdemona
If you're going to run code like this, at the very least make sure it only runs on the front endEvolutionary
Q
30

The question is may be old, but I have found a better way to do it:

function callback($buffer) {
  // modify buffer here, and then return the updated code
  return $buffer;
}

function buffer_start() { ob_start("callback"); }

function buffer_end() { ob_end_flush(); }

add_action('wp_head', 'buffer_start');
add_action('wp_footer', 'buffer_end');

Explanation This plugin code registers two actions – buffer_start and buffer_end.

buffer_start is executed at the end of the header section of the html. The parameter, the callback function, is called at the end of the output buffering. This occurs at the footer of the page, when the second registered action, buffer_end, executes.

The callback function is where you add your code to change the value of the output (the $buffer variable). Then you simply return the modified code and the page will be displayed.

Notes Be sure to use unique function names for buffer_start, buffer_end, and callback, so they do not conflict with other functions you may have in plugins.

Quantum answered 4/7, 2013 at 14:25 Comment(5)
You can prefix or suffix your examples: buffer_start_so_772510 or so_772510_callback (I prefer to suffix, as it's easier to read). This way, when the code surfaces somewhere else, we know where it came from ;)Occlusive
this doesn't work when the things that you want to modify or remove exists after the footer elementMagnitude
I recommend using kwoodfriend's solution, because it's way much safer (for example you can be sure to be the very last one to manipulate the output).Vector
http://www.dagondesign.com/articles/wordpress-hook-for-entire-page-using-output-buffering/ - this is the link, that this answer got copied from.Disown
Is there a way to include headers in the code, so that the code can replace headers too?Linear
B
22

AFAIK, there is no hook for this, since the themes uses HTML which won't be processed by WordPress.

You could, however, use output buffering to catch the final HTML:

<?php
// example from php.net
function callback($buffer) {
  // replace all the apples with oranges
  return (str_replace("apples", "oranges", $buffer));
}
ob_start("callback");
?>
<html><body>
<p>It's like comparing apples to oranges.</p>
</body></html>
<?php ob_end_flush(); ?>
/* output:
   <html><body>
   <p>It's like comparing oranges to oranges.</p>
   </body></html>
*/
Bikaner answered 21/4, 2009 at 13:20 Comment(2)
You can use php register_shutdown_function to end buffering, and retrieve html.Cockpit
This has a one drawback, you cannot call ob_start, ob_clean, .. inside the callback, which is needed for particular caching logic. php.net/manual/en/…Paleontology
F
18

@jacer, if you use the following hooks, the header.php also gets included.

function callback($buffer) {      
    $buffer = str_replace('replacing','width',$buffer);
    return $buffer; 
}

function buffer_start() { ob_start("callback"); } 
function buffer_end() { ob_end_flush(); }

add_action('after_setup_theme', 'buffer_start');
add_action('shutdown', 'buffer_end');
Findley answered 25/10, 2014 at 23:10 Comment(3)
Try not to make a post that references another answer (and requires reading the other answer to get the context); the more separated your answer gets from the answer you're referencing, the harder it is to understand what you're saying. Also, it's not necessarily bad to post an answer that builds off of the others, as long as you give credit where credit is due.Angelaangele
http://www.dagondesign.com/articles/wordpress-hook-for-entire-page-using-output-buffering/ - this is the link, that this answer got copied from.Disown
This solution does not work well. buffer_start is called twice and buffer_end never. I checked it with debug_log. Better stick to Kos' answerNorsworthy
O
8

I was using the top solution of this post (by kfriend) for a while. It uses an mu-plugin to buffer the whole output.

But this solution breaks the caching of wp-super-cache and no supercache-files are generated when i upload the mu-plugin.

So: If you are using wp-super-cache, you can use the filter of this plugin like this:

add_filter('wp_cache_ob_callback_filter', function($buffer) {
    $buffer = str_replace('foo', 'bar', $buffer);
    return $buffer;
});
Olin answered 4/7, 2018 at 10:51 Comment(1)
I'm trying to use wp super cache filter like this but its not working.. Also wp-rocket have similar filter called rocket_buffer and that is also not working. when i use filter name rocket_buffer, my plugin works fine, wp-rocket can minify html and css but cached html files are not generated inside /cache directory (same for wp super cache. no file get generated). Any suggestion please? I would really appreciate help on this.Minni
H
7

Modified https://stackoverflow.com/users/419673/kfriend answer.

All code will be on functions.php. You can do whatever you want with the html on the "final_output" filter.

On your theme's 'functions.php'

//we use 'init' action to use ob_start()
add_action( 'init', 'process_post' );

function process_post() {
     ob_start();
}


add_action('shutdown', function() {
    $final = '';

    // We'll need to get the number of ob levels we're in, so that we can iterate over each, collecting
    // that buffer's output into the final output.
    $levels = ob_get_level();

    for ($i = 0; $i < $levels; $i++) {
        $final .= ob_get_clean();
    }

    // Apply any filters to the final output
    echo apply_filters('final_output', $final);
}, 0);

add_filter('final_output', function($output) {
    //this is where changes should be made
    return str_replace('foo', 'bar', $output); 
});
Hildegaard answered 18/9, 2018 at 14:20 Comment(1)
Would you mind adding a sentence for the less experienced saying what your snipped does exactly?Hanukkah
I
6

To simplify previous answers, just use this in functions.php:

ob_start();
add_action('shutdown', function () {
    $html = ob_get_clean();
    // ... modify $html here
    echo $html;
}, 0);
Impenitent answered 28/2, 2021 at 19:57 Comment(0)
P
4

You might try looking in the wp-includes/formatting.php file. For example, the wpautop function. If you are looking for doing something with the entire page, look at the Super Cache plugin. That writes the final web page to a file for caching. Seeing how that plug-in works may give you some ideas.

Pleasantry answered 21/4, 2009 at 13:23 Comment(0)
R
4

Indeed there was a discusussion recently on the WP-Hackers mailing list about the topic of full page modification and it seems the consensus was that output buffering with ob_start() etc was the only real solution. There was also some discussion about the upsides and downsides of it: http://groups.google.com/group/wp-hackers/browse_thread/thread/e1a6f4b29169209a#

To summarize: It works and is the best solution when necessary (like in the WP-Supercache plugin) but slows down overall speeds because your content isn't allowed to be sent to the browser as its ready, but instead has to wait for the full document to be rendered (for ob_end() ) before it can be processed by you and sent to the browser.

Reichenberg answered 29/4, 2009 at 21:31 Comment(0)
C
4

I've been testing the answers here now for a while, and since the cache breaking thing is still an issue, I came up with a slightly different solution. In my tests no page cache broke. This solution has been implemented into my WordPress plugin OMGF (which has 50k+ users right now) and no issues with page cache breaking has been reported.

First, we start an output buffer on template redirect:

add_action('template_redirect', 'maybe_buffer_output', 3);
function maybe_buffer_output()
{
    /**
     * You can run all sorts of checks here, (e.g. if (is_admin()) if you don't want the buffer to start in certain situations.
     */

    ob_start('return_buffer');
}

Then, we apply our own filter to the HTML.

function return_buffer($html)
{
    if (!$html) {
        return $html;
    }

    return apply_filters('buffer_output', $html);
}

And then we can hook into the output by adding a filter:

add_filter('buffer_output', 'parse_output');
function parse_output($html)
{
    // Do what you want. Just don't forget to return the $html.

    return $html;
}

Hope it helps anyone.

Coccid answered 20/3, 2022 at 16:17 Comment(0)
B
2

I have run into problems with this code, as I end up with what seems to be the original source for the page so that some plugins has no effect on the page. I am trying to solve this now - I haven't found much info regarding best practises for collecting the output from WordPress.

Update and solution:

The code from KFRIEND didnt work for me as this captures unprocessed source from WordPress, not the same output that ends up in the browser in fact. My solution is probably not elegant using a globals variable to buffer up the contents - but at least I know get the same collected HTML as is delivered to the browser. Could be that different setups of plugins creates problems but thanks to code example by Jacer Omri above I ended up with this.

This code is in my case located typically in functions.php in theme folder.

$GLOBALS['oldschool_buffer_variable'] = '';
function sc_callback($data){
    $GLOBALS['final_html'] .= $data;
    return $data;
}
function sc_buffer_start(){
    ob_start('sc_callback');
}
function sc_buffer_end(){
    // Nothing makes a difference in my setup here, ob_get_flush() ob_end_clean() or whatever
    // function I try - nothing happens they all result in empty string. Strange since the
    // different functions supposedly have very different behaviours. Im guessing there are 
    // buffering all over the place from different plugins and such - which makes it so 
    // unpredictable. But that's why we can do it old school :D
    ob_end_flush();

    // Your final HTML is here, Yeeha!
    $output = $GLOBALS['oldschool_buffer_variable'];
}
add_action('wp_loaded', 'sc_buffer_start');
add_action('shutdown', 'sc_buffer_end');
Bacolod answered 12/10, 2016 at 8:24 Comment(0)
S
0

If you want to modify the output, you can use template_include:

add_filter( 'template_include', static function ( $template ) {
    if ( basename( $template ) === 'foo-template.php' ) {
      echo str_replace( 'foo', 'bar', file_get_contents( $template ) );
    }

    return null;
} );

If instead you want to override the output completely, you can use the action template_redirect.

add_action( 'template_redirect', static function () {
    wp_head();
    echo 'My output.';
    wp_footer();
    exit;
} );
Squally answered 21/2, 2023 at 18:47 Comment(0)
R
0

I have used the kfriend's solution in a plugin that needs to modify the final output of the wp process before send it to browser, so this plugin installs the mu-plugin on activation and does the filtering job.

Now I have had to fight with an unexpected situation: another (regular) plugin needs to modify the buffer before it sends it to the browser and the plugin uses a callback on ob_start( 'callback' ) that is kept "open" without calling ob_flush(). kfriend's solution calls ob_get_clean() on that buffer level, so the callback is not called when the level is "parsed" neither it is called on final ob_flush().

With output_buffering turned on, before the mu-plugin buffer level there will be another a level managed by "default output handler"; ( you can check it by calling ob_get_status( true ); ).

So, let's suppose that on shutdown hook we have 6 levels:

  • 0 level will be the upper one;
  • 1 level will be the mu-plugin level opened by ob_start();
  • 2-5 levels will be other levels that could need to call their callbacks on ob_flush.

Don't flushing buffers on levels 2 to 5 (by pushing content to the upper level) will let us deal with a content different from the one that would be sent to the browser with the mu-plugin not in place. So, the only level we need to capture will be the level indexed as 1 in the array returned by ob_get_status( true );.

Because the $i in the for loop has inverted values, the $i value when we are dealing with 1 keyed level will be $levels - 2.

So, the proposed solution is:

/**
 * Output Buffering
 *
 * Buffers the entire WP process, capturing the final output for manipulation.
 */

ob_start();

add_action('shutdown', function() {
    $final = '';

    // We'll need to get the number of ob levels we're in, so that we can iterate over each, collecting
    // that buffer's output into the final output.
    $levels = ob_get_level();

    // This is the $i value when dealing with the mu-plugin buffer level
    $ini_ob = ini_get('output_buffering');
    // if output_buffering isset on Off in php.ini
    if ( '0' === $ini_ob || '' === $ini_ob ) {
        $mu_plugin_i_value = $levels - 1;
    } else {
        $mu_plugin_i_value = $levels - 2;
    }

    // ob_flush() the lower levels and capture the mu-plugin (and the first) one
    for ($i = 0; $i < $levels; $i++) {
        if ( $i !== $mu_plugin_i_value ) {
            ob_end_flush();
        } else {
            $final .= ob_get_clean();
        }
    }

    // Apply any filters to the final output
    echo apply_filters('final_output', $final);
}, 0);
Rehearing answered 12/5 at 23:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.