Posted in Javascript

While playing around with Frontity (a react framework for WordPress) I noticed the compiled bundle size for the default Mars theme was pretty huge clocking in at an unacceptable 390KB of JavaScript. That’s a lot to download before my site will appear. Below I detail how I knocked over 100KB off that size with no discernible difference in functionality simply by swapping out React with its lightweight drop in replacement Preact.

Extending WebPack

To replace React we need to modify Frontity’s webpack configuration. At the time of writing it’s not documented how to do that however a forum post by SantosGuillamot contains a link to a CodeSandbox showing how to do it.

In your root folder’s package.json add the following dependencies:

1
2
3
4
5
6
"dependencies": {
	...
	"change-config": "./packages/change-config",
	"preact": "^10.5.14",
	"preact-render-to-string": "^5.1.19"
}

Create the following files:

  1. /packages/change-config/package.json
  2. /packages/change-config/frontity.config.js

Here’s the contents of package.json:

1
2
3
4
5
6
7
8
9
10
11
{
  "name": "change-config",
  "version": "1.0.0",
  "description": "Frontity package created using the Frontity CLI.",
  "keywords": [
    "frontity",
    "frontity-theme"
  ],
  "license": "Apache-2.0",
  "dependencies": {}
}

And here is frontity.config.js:

1
2
3
4
5
6
export const webpack = ({ config, mode }) => {
    config.resolve.alias["react"] = "preact/compat";
    config.resolve.alias["react-dom/test-utils"] = "preact/test-utils";
    config.resolve.alias["react-dom"] = "preact/compat";
    config.resolve.alias["react/jsx-runtime"] = "preact/jsx-runtime";
};

Now back in your root folder open frontity.settings.js and set the following at the end of the packages section:

1
2
3
4
5
6
7
8
const settings = {
  ...
  "packages": [
    ...,
    "@frontity/tiny-router",
    "@frontity/html2react",
    "change-config"
  ]

That should be it. Run

1
2
npx frontity build
npx frontity serve

Here are the before and after filesizes:

Before:

Frontity base Mars theme bundle size

After:

Frontity Mars theme bundle size with React replaced with Preact

A saving of 110KB! Not too shabby.

Read More »

Posted in Javascript

Custom button added to Gutenberg top panel

Gutenberg may be the shiny new toy available for WordPress admin but it still feels very limiting at times – especially when trying to do things the developers don’t anticipate. I wanted to add an extra button at the top of the post edit page next to the Publish button before finding out there is no SlotFill for this but once again, React has us covered with a handy little helper createPortal. createPortal allows you to place a component anywhere in the DOM of your choosing – perfect for what I wanted to do here.

The Component

Without further adieu, component code below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
 * WordPress dependencies
 */
import { Button } from '@wordpress/components';
import { compose } from '@wordpress/compose';
import { createPortal, useEffect, useState } from '@wordpress/element';
import { withSelect } from '@wordpress/data';
 
const TopBtn = ( { postPreviewButton } ) => {
	// Empty div element our button will be placed into
	// This div will be inserted before the Publish button with useEffect()
	const [ container ] = useState( document.createElement( 'div' ) );
 
	// The button submit handler
	const onSubmit = () => {
		console.log( 'Button pressed!' );
	};
 
	// Do the actual placing/removing of the button in the DOM
	useEffect( () => {
		// Ensure we have a Preview button to place next to
		if ( ! postPreviewButton ) {
			return;
		}
 
		// Insert our button immediately after the post preview button.
		postPreviewButton.parentNode.insertBefore(
			container,
			postPreviewButton.nextSibling
		);
		// Remove our button on component unload
		return () => {
			postPreviewButton.parentNode.removeChild( container );
		};
	}, [ postPreviewButton ] );
 
	// Use createPortal to place our component into the <div/> container
	return createPortal(
		<>
			<Button
				isSecondary
				showTooltip={ true }
				label="My Custom Button"
				onClick={ onSubmit }
			>
				My Btn
			</Button>
			<span>&nbsp;</span>
		</>,
		container
	);
};
 
export default compose( [
	withSelect( () => {
		return {
			// Provide the Post preview button element so we'll know where to
			// place our component
			postPreviewButton: document.querySelector( '.editor-post-preview' ),
		};
	} ),
] )( TopBtn );

This setup is quite flexible allowing you to add components pretty much anywhere on the page you like. I’ve commented the component as best I could. If you have any questions feel free to ask in the comments section below.

Read More »

Posted (Updated ) in PHP

With the rise of Gutenberg and it’s million tiny JavaScript files loaded every time you go to write a post, it’s become more important than ever to utilise a CDN for static assets loaded through the WordPress admin.

W3 Total Cache is a great plugin for speeding up your site and it’s CDN feature is top notch. One seemingly missing feature however is serving static assets from the configured CDN when browsing around your sites admin. By default no CDN rewriting at all is done on your sites backend and if you have a lot of logged in users writing posts, this can cause a bunch of unnecessary load on your server as they each download all the files.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Automatically rewrite assets to CDN URLs in the backend
if (class_exists('W3TC\Dispatcher')) {
    add_action('admin_init', function () {
        $config = new W3TC\Config();
 
        if ($config->get('cdn.enabled')) {
            // CDN domain with trailing slash
            $cdn_domain = W3TC\Dispatcher::component('Cdn_Core')->get_cdn()->format_url('');
 
            /**
             * Filters the HTML link tag of an enqueued style.
             *
             * @since 2.6.0
             * @since 4.3.0 Introduced the `$href` parameter.
             * @since 4.5.0 Introduced the `$media` parameter.
             *
             * @param string $html   The link tag for the enqueued style.
             * @param string $handle The style's registered handle.
             * @param string $href   The stylesheet's source URL.
             * @param string $media  The stylesheet's media attribute.
             */
            add_filter('style_loader_tag', function ($tag, $handle, $href, $media) use ($cdn_domain) {
                return str_replace(site_url('/'), $cdn_domain, $tag);
            }, 10, 4);
 
            /**
             * Filters the HTML script tag of an enqueued script.
             *
             * @since 4.1.0
             *
             * @param string $tag    The script tag for the enqueued script.
             * @param string $handle The script's registered handle.
             * @param string $src    The script's source URL.
             */
            add_filter('script_loader_tag', function ($tag, $handle, $src) use ($cdn_domain) {
                return str_replace(site_url('/'), $cdn_domain, $tag);
            }, 10, 3);
        }
    });
}

I’d only recommend using this code with an Origin Pull CDN, as it doesn’t handle pushing any of these assets to the CDN – only serving from what’s already in there (which if using an Origin Pull CDN will be everything).

Read More »

Posted in PHP

WordPress offers no way out of the box to grab your sidebar widget settings and use them somewhere else in your site. Here’s a function to allow you to do it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
 * Find a given widget in a given sidebar and return its settings.
 * 
 * Example usage:
 * $options = [];
 * try {
 *    $options = get_sidebar_widget_options('sidebar-1', 'recent-comments');
 * } catch (Exception $e) {}
 *
 * @param $sidebar_id    The ID of the sidebar. Defined in your register_sidebar() call
 * @param $widget_type   Widget type specified in register_sidebar()
 * @return array         Saved options
 * @throws Exception     "Widget not found in sidebar" or "Widget has no saved options"
 */
function get_sidebar_widget_options($sidebar_id, $widget_type)
{
    // Grab the list of sidebars and their widgets
    $sidebars = wp_get_sidebars_widgets();
    // Just grab the widgets for our sidebar
    $widgets = $sidebars[$sidebar_id];
 
    // Get the ID of our widget in this sidebar
    $widget_id = 0;
    foreach ( $widgets as $widget_details )
    {
        // $widget_details is of the format $widget_type-$id - we just want the id part
        if ( preg_match("/^{$widget_type}\-(?P<id>\d+)$/", $widget_details, $matches) )
        {
            $widget_id = $matches['id'];
            break;
        }
    }
 
    // If we didn't find the given widget in the given sidebar, throw an error
    if ( !$widget_id )
        throw new Exception("Widget not found in sidebar");
 
    // Grab the options of each instance of our $widget_type from the DB
    $options = get_option('widget_' . $widget_type);
 
    // Ensure there are settings to return
    if ( !isset($options[$widget_id]) )
        throw new Exception("Widget has no saved options");
 
    // Grab the settings
    $widget_options = $options[$widget_id];
 
    return $widget_options;
}

Drop the function in your functions.php. Note that it will find the first occurrence of your given widget in the given sidebar and return its settings.

Read More »

Posted (Updated ) in PHP

If you want your WordPress site to handle large traffic spikes, loading as many assets as possible from a third party server is a no-brainer. One of the ways to do this is by pointing all asset URLs to a subdomain and having that subdomain load through a service like CloudFront using origin pull. This is done in 3 steps:

  1. Create the CloudFront distribution
  2. Add the subdomain to your DNS
  3. The use of a poorly documented WordPress constant.

In this tutorial I’ll be moving all my sites assets from www.flynsarmy.com to static.flynsarmy.com.

Read More »

Posted (Updated ) in Javascript, PHP

Imagine you’re running a WordPress Multisite subfolder instance (let’s call it MyNetwork) with a bunch of blogs like so:

mynetwork.com
mynetwork.com/foo
mynetwork.com/bar

Each blog needs to keep track of its page views (including the main blog) but you’d also like page view aggregated statistics for your entire network. This can be done and is surprisingly easy with Google Analytics views and filters. Just follow the below steps and you’ll be on your way.

Read More »

Posted (Updated ) in PHP

Update 2015-05-02: Now working for WP 4.2

In a project today we needed to attach a meta value to a comment based on the user commenting, then filter the comments list (both frontend and backend) by this value. The task turned out to be surprisingly difficult and the comment actions/filters are notoriously badly documented in WP so I thought I’d document the process here.

1. Attach a meta value as a visitor comments

A simple comment_post action (not documented) hook does the trick here.

1
2
3
4
5
6
/**
 * Adds a meta value to a comment as one is created
 */
add_action( 'comment_post', function($comment_id) {
	add_comment_meta( $comment_id, 'my_meta_key', 'my_meta_value' );
});

 

2. Filter the comments by meta (4.2+)

Until 4.2 this was done a little differently (See the bottom of this post for that code) however due to recent changes we’re now forced to use comments_clauses action like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
 * Limits displayed comments on front and backend to just those with the
 * specified meta key/value pair.
 *
 * @param array $pieces
 * @param WP_Comment_Query $query
 * @return array
 */
add_action('comments_clauses', function(array $pieces, WP_Comment_Query $query) {
    global $wpdb;
 
    $meta_query = new WP_Meta_Query();
    $meta_query->parse_query_vars([
        'meta_key' => 'my_meta_key',        'meta_value' => 'my_meta_value',    ]);
 
    if ( !empty($meta_query->queries) )
    {
        $meta_query_clauses = $meta_query->get_sql( 'comment', $wpdb->comments, 'comment_ID', $query );
 
        if ( !empty($meta_query_clauses) )
        {
            $pieces['join'] .= $meta_query_clauses['join'];
 
            if ( $pieces['where'] )
                $pieces['where'] .= ' AND ';
            // Strip leading 'AND'.
            $pieces['where'] .= preg_replace( '/^\s*AND\s*/', '', $meta_query_clauses['where'] );
 
            if ( !$query->query_vars['count'] )
            {
                $pieces['groupby'] = "{$wpdb->comments}.comment_ID";
            }
        }
    }
 
    return $pieces;
}, 10, 2);

 

3. Making get_comments_number() work

get_comments_number() method commonly used in your themes comments.php file will return the total number of comments associated with a post. We need it filtered by our above meta query. Here’s an inefficient but effective way to make that happen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * Filter get_comments_number() correctly by our meta query.
 *
 * @param int $count
 * @param int $post_id
 * @return int
 */
add_filter('get_comments_number', function($count, $post_id) {
    $query = new WP_Comment_Query(['post_id' => $post_id]);
 
    // Frontend users only see approved comments
    if ( !is_admin() )
        $comment_query['status'] = 'approve';
 
    return sizeof($query->get_comments());
}, 10, 2);

 

Conclusion

That should be it! If anything doesn’t work as expected let me know in the comments below.

 

Bonus: Filter the comments by meta (<4.2)

Filtering comments by meta in <4.2 is done in two steps. One for admin, and one for frontend.

Admin

This can be done with pre_get_comments action hook (again, not documented – but I did find a usage example here).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * Limit visible comments to just those for the current subdomain.
 * Due to a bug in WP this will work for all backend users but only for 
 * logged out users on the frontend.
 */
add_action('pre_get_comments', function($query) {
	// meta_query is already an instance of WP_Comment_Query but other 
	// query_vars may have already been added, so re-initialize it
	$query->meta_query = new WP_Comment_Query();
 
	$query->query_vars['meta_key'] = 'my_meta_key';
	$query->query_vars['meta_value'] = 'my_meta_value';
 
	$query->meta_query->parse_query_vars( $query->query_vars );
});

Frontend

As the pre_get_comments comment mentions, there is a bug in WP whereby logged in users will not have the above filter applied, so we need to use another hook for those.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * WP doesn't support pre_get_comments for logged in users, so for those
 * we need to manually remove all the comments that don't have  our meta
 * key
 *
 * See https://core.trac.wordpress.org/ticket/27018#ticket
 */
add_filter('comments_array', function($comments, $post_id) {
	global $wpdb, $user_ID;
 
	// If there's no user_ID and no commenter then the pre_get_comments filter
	// applied correctly and we don't need to do anything
	// See comments_template() method
	$commenter = wp_get_current_commenter();
	$comment_author = $commenter['comment_author']; // Escaped by sanitize_comment_cookies()
 
	if ( $user_ID || !empty($comment_author) )
		return;
 
	// Grab comment IDs for this current area
	$area_comment_ids = $wpdb->get_col($wpdb->prepare("
		SELECT $wpdb->comments.comment_ID
		FROM $wpdb->comments
		JOIN $wpdb->commentmeta ON $wpdb->commentmeta.comment_id=$wpdb->comments.comment_ID
		WHERE $wpdb->commentmeta.meta_key=%s AND $wpdb->commentmeta.meta_value=%s
	", 'my_meta_key', 'my_meta_value'));
 
	// Strip out any comments not belonging to the current area
	foreach ( $comments as $key => $comment )
		if ( !in_array($comment->comment_ID, $area_comment_ids) )
			unset($comments[$key]);
 
	return $comments;
}, 10, 2);

The above seems really messy and inefficient to me but I can’t think of a better way. It’ll grab all comments we want to show and filter out any others.

Read More »

Posted in PHP

We’ve been having no end to issues with image scaling in WordPress where an administrator uploads a ridiculously large image and it appears stretched vertically in Internet Explorer.

I looked into this and discovered WordPress was automatically adding width and height attachments onto the inserted image in the admin. This is generally fine as all modern browsers will scale accordingly for smaller windows…but then we have Internet Explorer which will just retardedly scale width but not height resulting in massively stretched images.

The approach I ended up taking to fix it was to scale the automatically generated width and height attributes on the image down before the HTML is inserted into the post in admin. This can be accomplished with the following snippet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
add_filter( 'post_thumbnail_html', 'flyn_apply_max_image_dimenions', 10 );
add_filter( 'image_send_to_editor', 'flyn_apply_max_image_dimenions', 10 );
function flyn_apply_max_image_dimenions( $html ) {
	ini_set('display_errors', true);
	preg_match('/width="(\d+)"\s/', $html, $wmatches);
	preg_match('/height="(\d+)"\s/', $html, $hmatches);
 
	if ( !empty($wmatches[1]) && !empty($hmatches[1]) )
	{
		list($width, $height) = boilerplate_scale_inside($wmatches[1], $hmatches[1], 630, 9999);
		$html = str_replace('width="'.$wmatches[1].'"', 'width="'.$width.'"', $html);
		$html = str_replace('height="'.$hmatches[1].'"', 'height="'.$height.'"', $html);
	}
 
	return $html;
}
 
function flyn_scale_inside( $width, $height, $into_width, $into_height )
{
	$ratio = $into_width / $width;
 
	$width = $into_width;
	$height *= $ratio;
 
	if ( $height > $into_height )
	{
		$ratio = $into_height / $height;
		$height = $into_height;
		$width *= $ratio;
	}
 
	return array($width, $height);
}

Set your maximum width however you please (I’m using 630px in the example above). I chose a height of 9999 so the image would always be scaled nicely to my themes maximum width.

Read More »

Posted in PHP

Customising your RSS feed in WordPress is relatively easy and comes in two methods: adding rows to the existing feed, or complete customisation. Each is detailed below along with example code snippets.

 

Adding Rows

If you want to add rows to the default RSS feed for each entry, the best way to do this is with the rss2_item action. In the example below I add an <image> row if my posts have featured images.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
add_action('rss2_item', function() {
	global $post;
	if ( $post->post_type != 'cctv_image' )
		return;
 
	// Only add rows to feed with post images
	if ( !has_post_thumbnail($post->ID) )
		return;
 
	// Get the featured image URL and dimensions
	$thumbnail_id = get_post_thumbnail_id( $post->ID );
	$result = wp_get_attachment_image_src( $thumbnail_id, 'full' );
 
	// Did an error occur?
	if ( !$result )
		return;
 
	$url = $result[0];
	$width = $result[1];
	$height = $result[2];
	echo "<image width='$width' height='$height'>$url</image>";
});

Now if a post has a featured image, it’s RSS entry will contain the following extra line:

<image width=”640″ height=”480″>http://mysite.com/wp-content/uploads/2013/10/my_image.jpg</image>

 

Complete Customisation

The above not enough for you? Do you want complete control over what is and isn’t in your feed? Add the following to your functions.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
remove_all_actions( 'do_feed_rss2' );
add_action( 'do_feed_rss2', function( $for_comments ) {
	if ( $for_comments )
		load_template( ABSPATH . WPINC . '/feed-rss2-comments.php' );
	else
	{
		if ( $rss_template = locate_template( 'feed-rss2.php' ) )
			// locate_template() returns path to file
			// if either the child theme or the parent theme have overridden the template
			load_template( $rss_template );
		else
			load_template( ABSPATH . WPINC . '/feed-rss2.php' );
	}
}, 10, 1 );

The above allows you to drop a feed-rss2.php file into your theme folder which will be loaded in place of the default RSS template when a visitor hits your RSS feed URL. Put whatever you like in there!

Read More »

Posted (Updated ) in PHP

Another quick set of utility functions. If you wish to restrict WordPress admin to only users with specifics sets of permissions (such as only those higher than subscriber) use the following two actions in your functions.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * Remove admin bar for users without adequate permissions
 */
add_action('after_setup_theme', function() { 
	if ( !current_user_can('edit_posts') )
		add_filter('show_admin_bar', '__return_false');	
});
 
/**
 * Redirect users without adequate permissions back to home page
 */
add_action('admin_init', function(){
	if ( !current_user_can('edit_posts') )
	{
		// Only redirect if not an AJAX request
		if ( empty($_SERVER['PHP_SELF']) || basename($_SERVER['PHP_SELF']) != 'admin-ajax.php' )
		{
			wp_redirect( site_url() );
			exit;
		}
	}
});

You can use any permission you like from the Roles and Capabilities section of the documentation to make admin accessible to just the user groups of your choice. There’s even a handy table showing which roles are available to which default user levels.

Read More »