Clean up and optimize WordPress for your next theme

WordPress developed into a general purpose content management system; as such it tends to add more code than you actually need. In this post I’m trying to get rid of most of the ballast to obtain a clean and fast scaffolding to build your next theme on.

WordPress is like a container of all we hate about PHP concentrated into a single piece of software (or at least all we hated about PHP4, because now it’s actually fine).

The codebase is old and inconsistent. I don’t blame plugin developers because working with such a convulsed foundation is incredibly frustrating.

Every time I have to deal with WP, the first thing I do is clean it up as much as possible. There are some plugins for that, but I try to keep the plugin count to the minimum and use custom code for the most trivial tasks.

As a kind of note-to-self, the following is what I do to tidy up the mess. Please note that this is not properly a post for copy-pasters but more for developers who want to know how WP works.

Head clean up

Lets start by removing some WP tags from the document header. To do that we hook to the after_setup_theme action which seems to be one of the first to be executed.

function cubiq_setup () {
    remove_action('wp_head', 'wp_generator');                // #1
    remove_action('wp_head', 'wlwmanifest_link');            // #2
    remove_action('wp_head', 'rsd_link');                    // #3
    remove_action('wp_head', 'wp_shortlink_wp_head');        // #4

    remove_action('wp_head', 'adjacent_posts_rel_link_wp_head', 10);    // #5

    add_filter('the_generator', '__return_false');            // #6
    add_filter('show_admin_bar','__return_false');            // #7

    remove_action( 'wp_head', 'print_emoji_detection_script', 7 );  // #8
    remove_action( 'wp_print_styles', 'print_emoji_styles' );
}
add_action('after_setup_theme', 'cubiq_setup');

cubiq_setup –which you can name whatever you like– will be our main setup function. Most of the filters/actions will be added there.

#1 removes the “generator” meta tag from the document header (we definitely don’t need to let the world know that we are using WordPress and actually we are rather ashamed about it).

#2 removes the “wlwmanifest” link. wlwmanifest.xml is the resource file needed to enable support for Windows Live Writer. Nobody on Earth needs that. Note that this command simply removes the link, if you want to completely disable the functionality you need to deny access to the file /wp-includes/wlwmanifest.xml probably from your .htaccess (but that’s not strictly needed).

#3 The RSD is an API to edit your blog from external services and clients. If you edit your blog exclusively from the WP admin console, you don’t need this.

#4 “wp_shortlink_wp_head” adds a “shortlink” into your document head that will look like http://example.com/?p=ID. No need, thanks.

#5 Removes a link to the next and previous post from the document header. This could be theoretically beneficial, but to my experience it introduces more problems than it solves. Please note that this has nothing to deal with the “next/previous” post that you may want to add at the end of each post.

#6 Removes the generator name from the RSS feeds.

#7 Removes the administrator’s bar and also the associated CSS styles. Especially during the development phase I find it very annoying.

#8 Removes WP 4.2 emoji styles and JS. Nasty stuff.

UPDATE 2015.05.18: Remove emoji support

remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'wp_print_styles', 'print_emoji_styles' );

RSS Feeds

This is another WordPress anomaly: the standard RSS feed must be explicitly activated with add_theme_support( 'automatic-feed-links' ); but the extra feeds for the single posts (like the list of comments) are generated automatically.

What I do is to completely disable automatic feeds and reintroduce the feed link manually. To do so just don’t use the ‘automatic-feed-links’ theme support and add the following to the setup function.

    remove_action('wp_head', 'feed_links_extra', 3);

Again, it is not necessary to also remove the “feed_links” action if you don’t add “automatic-feed-link” support.

To reintroduce basic RSS feed support just add the following to your header.php file

<link rel="alternate" type="application/rss+xml" title="<?php echo get_bloginfo('sitename') ?> Feed" href="<?php echo get_bloginfo('rss2_url') ?>">

This way you’ll always have a clean link to your main RSS feed which is what 99% want.

If you prefer to stay on the function.php file or you are working on a plugin, you can alternatively use the wp_head action, like so:

function add_rss_link() {
?>
<link rel="alternate" type="application/rss+xml" title="<?php echo get_bloginfo('sitename') ?> Feed" href="<?php echo get_bloginfo('rss2_url') ?>">
<?php
}
add_action( 'wp_head', 'add_rss_link' );

Personally I prefer to avoid adding function calls as much as possible and I’d suggest to add the link directly into the header file, but that’s up to you.

No cookies, please

Every time a user leaves a message WordPress adds a cookie to save the name, email and website. If you do not have a huge amount of returning visitors avoid setting useless cookies. Just add to the init function:

remove_action('set_comment_cookies', 'wp_set_comment_cookies');

Also note that tracking cookies are forbidden in Europe (without user’s consent). I don’t know if that pretty innocuous cookie would be indeed prohibited, but better safe than sorry.

Allowed tags

How many times your users used the del or abbr tag? Yeah, exactly, zero. Let’s remove some of the allowed tags in comments.

We can just add to the setup function the following

global $allowedtags;

unset($allowedtags['cite']);
unset($allowedtags['q']);
unset($allowedtags['del']);
unset($allowedtags['abbr']);
unset($allowedtags['acronym']);

It seems there’s no better ways than overriding the $allowedtags global. I’m shivering, but that seems to be what it takes. I hope someone could point me to a better solution.

Images and lightbox

Let’s now clean up a little the generated HTML code and add support for lightboxes.

First of all instruct WP to use (its own messy) HTML5 markup whenever possible. Add to the init

add_theme_support('html5',
    array('comment-list', 'comment-form', 'search-form', 'gallery', 'caption', 'widgets')
);

This helps especially in styling the comment form and the galleries.

Next we want to add a “lightbox” class to all our clickable images. While not strictly necessary (you could do with CSS selectors), I believe it is cleaner and faster to add a special class to the zoomable images. There are multiple ways to do that.

The most common approach is to add the class as soon as the image is placed on a post in the backend editor. I find that conceptually wrong, I don’t want to add a class that I may not need in the future to all my images. I prefer to do that on the fly hooking to the_content. This goes in the init:

add_filter( 'the_content', 'cubiq_the_content', 9999, 1 );

Note the very low priority set to 9999, so we are pretty sure that the function is called at the very last time.

The function that does the job looks like this:

function cubiq_the_content ( $content ) {
    // inline images
    $content = preg_replace('/<a href="([^"]*.(jpe?g|gif|png))"><img class="([^"]*)"/i', '<a class="lightbox 3" href="1"><img', $content);

    // galleries
    $content = preg_replace("/<a href='([^']*.(jpe?g|gif|png))'>/i", '<a class="lightbox" href="1">', $content);

    return $content;
}

The first regex converts this:

<a href="..."><img src="..." class="alignleft" width="x" height="y"></a>

to this:

<a href="..." class="lightbox alignleft"><img src="..." width="x" height="y"></a>

So it does not only add the “lightbox” class but it also swap the styling from the img to the containing a tag. I find it easier to style elements this way.

The second regex is required to also work on gallery images, because WordPress of course handles images in galleries differently. I’m pretty sure one day they’ll clean up the code and I will need to change my regex again… but that’s another story.

For some reason WordPress adds explict width to the figure tag. That inhibits our wonderful responsive layout and the only way around that is to rewrite the entire img_caption_shortcode function.

The action is:

add_filter( 'img_caption_shortcode', 'cubiq_img_caption_shortcode', 10, 3 );

The function:

function cubiq_img_caption_shortcode ( $empty, $attr, $content ) {
    $attr = shortcode_atts( array(
        'id'      => '',
        'align'   => 'alignnone',
        'width'   => '',
        'caption' => '',
        'class'   => ''
    ), $attr, 'caption' );

    if ( 1 > (int)$attr['width'] || empty( $attr['caption'] ) ) {
        return $content;
    }

    if ( $attr['id'] ) {
        $attr['id'] = 'id="' . esc_attr( $attr['id'] ) . '"';
    }

    $attr['class'] = 'class="' . esc_attr( trim( $attr['align'] . ' ' . $attr['class'] ) ) . '"';

    return '<figure ' . $attr['id'] . ' ' . $attr['class'] . '>'
        . do_shortcode( $content )
        . '<figcaption>' . $attr['caption'] . '</figcaption>'
        . '</figure>';
}

The code above overrides the default markup for galleries with a saner one and grants responsiveness back to our images.

Reduce the thumbnails JPEG quality

By default WP saves images at JPEG quality 90, let’s reduce that a little.

add_filter('jpeg_quality', create_function( '', 'return 85;' ) );

create_function is a nifty way to create an anonymous function on the fly. I usually try to avoid that, but in this case I’ll make an exception for the sake of a more compact code.

Just a quick note. With a lot of headache it would be possible to change the jpeg quality based on the image size (thumb, medium, large). After an hour of fighting with WordPress cheesy code I decided that the hassle is not worth the effort and settled on a fixed 85. Remember that every time WP saves a JPEG it adds a layer of compression. So for example if you use the internal image editor to crop a picture, the resulting JPG will be saved multiple times reducing the overall quality.

The bottom line: do not use the internal image editor and upload the image exactly the way you need it.

One last thing about images: we need to get rid of the attachment pages! Read on.

Redirecting and 404ing unnecessary pages

WP creates a lot of pages we don’t really need. First in the list of the unwanted is the attachment page. Each image you upload gets its very own personal web page. Also the archive by Author if you are a sole blogger is totally unneeded as the archive by date (except for travel diaries maybe).

I’ve seen people adding a redirect in a image.php page to get rid of the attachment pages, but there’s a better way.

Just add to the function.php

function cubiq_template_redirect () {
    global $wp_query, $post;

    if ( is_attachment() ) {
        $post_parent = $post->post_parent;

        if ( $post_parent ) {
            wp_redirect( get_permalink($post->post_parent), 301 );
            exit;
        }

        $wp_query->set_404();

        return;
    }

    if ( is_author() || is_date() ) {
        $wp_query->set_404();
    }
}
add_action( 'template_redirect', 'cubiq_template_redirect' );

If it is an attachment page (most likely an image) we redirect to the page that is referencing to that image. If the image is an orphan we just return 404.

We also return a page not found for author and date archives to improve SEO. You can expand and customize the code to your needs. For example if you don’t want tag archives, just check for is_tag(); use is_category() for category archives, and so on.

A piece of cake!

Removing/Configuring Thumbnails

WordPress has this arbitrary 3-thumbnail size configuration: thumb, medium, large. Of which only the thumbnail size can be cropped to exact dimensions.

Many times you don’t need the medium or the large size, so to prevent useless waste of resources we could instruct WP to ignore them.

Add to the setup function

add_filter('intermediate_image_sizes_advanced', 'cubiq_filter_image_sizes');

Then you need the following function

function cubiq_filter_image_sizes ($sizes) {
    unset( $sizes['large'] );

    return $sizes;
}

We just disabled the large size image. with $sizes['medium'] we would get rid of the medium size instead.

What if we want to crop the medium size image to exact dimensions instead (like the thumbnail does)?

Add to the init:

add_image_size( 'medium', 960, 540, true );

The last parameter (true) takes care of the cropping. Note that this will override any value you set in the WP control panel under Settings > Media.

Link to the “large” size instead of the original image

Another funky WP habit is to link to the original full size image instead of to the large size. Again, there are multiple ways of fixing that. I’m not sure which would be the best one this time. The easiest is to change the link the moment the image is added to the editor with the image_send_to_editor hook. Not a solution that I like since we are going to alter the code and save it on the database, but as always I try to find a mid-way between clean, easy and functionality.

First we add the filter to the init:

add_filter( 'image_send_to_editor', 'cubiq_add_lightbox_class', 10, 3 );

Then we add a new function:

function cubiq_add_lightbox_class( $html, $id ) {
    $url = wp_get_attachment_image_src($id, 'large');

    $html = preg_replace('/<a href="[^"]+.(jpe?g|gif|png)">/i', '<a href="' . $url[0] . '">', $html);

    return $html;
}

Boom! Done!

Videos

Responsive videos is something WordPress doesn’t care about, but we do.

There’s a very simple set of CSS rules to support responsive videos, but we need to enclose the iframe into a containing element first.

So, first of all hook to the oembed filter (always from our initial setup function):

add_filter('embed_oembed_html', 'cubiq_embed_oembed_html', 9999, 4);

Then add a simple function

function cubiq_embed_oembed_html($html, $url, $attr, $post_id) {
    return '<div class="video-wrapper">' . $html . '</div>';
}

That will wrap the video inside a div with “video-wrapper” class. To make the video actually responsive we can add something like this to the CSS:

.video-wrapper {
    position: relative;
    padding-bottom: 56.25%;
    padding-top: 25px;
    height: 0;
    margin-bottom: 1.2em;
}

.video-wrapper iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

While we are at it we could also change the video code a little.

function cubiq_embed_oembed_html($html, $url, $attr, $post_id) {
    $html = preg_replace('# src="https://www.youtube.com([^"]*)"#', ' src="https://www.youtube-nocookie.com1&rel=0&modestbranding=1"', $html);
    return '<div class="video-wrapper">' . $html . '</div>';
}

Here I’ve added a tiny regex for youtube videos that switch to youtube-nocookie domain (because privacy matters). If you are not aware, the “nocookie” is a functionality added by google few years ago and it prevents the embedded videos to set cookies before the user clicks play. It is in fact called delayed-cookie and it’s not –as the domain name may suggest– a cookie-free solution.

I also don’t want links to related videos at the end of playback (rel=0) and I reduced the youtube branding too (modestbranding=1).

We are all set with videos. Let’s boost performance a little.

Poor man’s CDN

It is called “Poor man’s CDN” but if correctly configured the performance boost is noticeable. The rationale is: a real load-balanced, geografically-distributed CDN is expensive, a subdomain virtually free.

The browser can open just a limited number of connections per domain so the trick is to serve static files (images, css, js, …) from a subdomain like static.example.com to increase the number of concurrent connections.

Not only that. If you configure your subdomain properly with a very aggressive cache and a lightweight webserver, your CDN will have nothing to envy to more expensive solutions!

The easiest way to tell WordPress that you are using a CDN is by defining a couple of globals in your config.php file.


define('WP_CONTENT_DIR', '/var/www/somedir');
define('WP_CONTENT_URL', 'http://cdn.example.com');

Put them at the very beginning of the config file otherwise they won’t work!

Those lines effectively change the path and the URL of your wp-content directory. WP_CONTENT_DIR is not technically needed, you could keep the original wp-content path and change the domain only.

If moving the whole wp-content directory seems overkill to you, there’s a way to only change the uploaded files directory path. Few WP versions ago it was very easy to do that, now the configuration is buried inside a hidden page.

Head to http://yourdomain/wp-admin/options.php and search for upload_path and upload_url_path. From those two fields you can change the path and URL of just the upload directory, which in certain scenarios might be desirable over changing the whole wp-content.

Get rid of revisions

As a lone blogger I’ve never needed WP revisioning. Every time you update an article WP saves a revision so you can go back and forth to a previous versions. While it might be useful in multi-user sites, I normally find it a waste of resources.

Since you have the config.php file open, just add

define('WP_POST_REVISIONS', 0);

and kiss the revisions goodbye.

Meta description

Another thing WordPress is not really good at is creating a fancy description for you page. That is not vital but still important from a SEO standpont.

A very simple not-complete-but-just-to-give-you-an-idea solution might be something like this

function cubiq_description () {
    $description = '';

    if ( is_home() || is_front() ) {
        $description = 'YOUR SITE DESCRIPTION';
    } elseif ( is_single() || is_page() ) {
        $description = trim( strip_tags( get_the_excerpt() ) );        // strip_tags shouldn't be needed

        if ( strlen($description) > 150 ) {
            $description = substr($description, 0, strpos($description, ' ', 150) );
        }
    } elseif ( is_category() || is_tag() ) {
        $description = 'List of articles published on YOUR_SITE_NAME filed under “' . single_cat_title('', false) . '” cateogry';
    }

    return esc_html($description);
}

In your header.php, inside the head tag you could then add:

<?php $desc = cubiq_description(); if ( $desc ): ?>

    <meta name="description" content="<?php echo $desc ?>">

<?php endif; ?>

It’s not a go-to solution that covers all the possible scenarios, but it’s a very simple way to have an approximately 150 characters long meta description for your main pages.

Update jQuery

WP comes with an old version of jQuery, it’s for compatibility with ≤IE8 mostly, if you don’t need it and you prefer a more recent branch just add:

function cubiq_scripts() {
    if ( !is_admin() ) {
        wp_deregister_script('jquery');
    }
}
add_action( 'wp_enqueue_scripts', 'cubiq_scripts' );

Now to your document footer (not the header!!) you may add a link to a decent jQuery version. Do not use wp_enqueue_script to add external scripts, WP will add a nasty version number lowering the benefit of a CDN (unless you create a function that removes the external file versioning of course).

If you also want support for inline comment reply you may add:

function cubiq_scripts() {
    if ( !is_admin() ) {
        wp_deregister_script('jquery');

        // inline comment reply
        if ( is_single() && comments_open() ) {
            wp_enqueue_script( 'comment-reply' );
        }
    }
}

Conclusions

It doesn’t seem much but it actually took me quite some time to collect all of the above and making sure that everything’s working through WP updates. I’m sure more can be done and probably something could be done better, but it’s something to get you started.

What do you think? What is your “WordPress clean up” procedure?

22 thoughts on “Clean up and optimize WordPress for your next theme”

  1. Great post thanks.

    I’ve picked up a few tricks here I didn’t know about. I’ll definitely be using them in my next WordPress project.

  2. Good work!

    Just a quick question. Is the source cod in section “Link to the ‘large’ size instead of the original image” correct? Seems like not for that purpose.

  3. Wow what a great article, thanks for sharing all your tipps. I found it while searching for a solution to never display original images but the large ones instead, but I would prefer a solution that is not getting in already at the editor and database, but only at the output level. You mentioned there are some more, would you share them or links to them or a search term, since I couldn’t find others. Thanks a lot.

    (By the way regarding the question of Jan, your code worked for me but I had to read twice to see that it only works when adding a new image. I copied into functions.php and reload the page and it still used the original size, so first I thought the code is not working. But with newly added media it does.)

    1. thanks for your comment.

      Changing the image on the fly from the content hook is a bit more complicated. I don’t have a link at hand but what you need to do is basically:

      1) perform a regex to find all image URLs
      2) find the image ID from image URL (you probably need to perform a custom query)
      3) from the resource ID, find the large image URL
      4) replace the original URL with the new one

      It’s not overly complicated, but still the method suggested in my post is way simpler 🙂

  4. I used most of your header cleanup for my theme as well some other cool tweaks. This post is very very informative! Thank you very much

  5. Thanks for the post. cubiq_setup(), it’s too useful. I will look through other codes.

    Post revisions for single user really useless in most cases, I agree. However, sometimes some situations make the new version of the post unusable and to look for old version of the post is obligatory. Because of this situation I think it’s better not to close post revisions. It can be cleaned periodically with an SQL query or a plugin instead of deactivate.

  6. Thanks for your post.

    I’m still confuse about cookies, honestly I don’t care about visitor cookies, and I don’t want to store it on my server, but if I turn off the cookies on my site, my site interface is broken, and lot of WP plugin doesn’t work.

    And what is the consequence if we’re not put a cookies popup notification? People in Europe will report it? Is privacy policy page enough or I need to add popup notification too?

    Thanks.

  7. Where do you put all this code at?. The functions.php? If you have a child theme do you put it in there

  8. After added “remove_action(‘wp_head’, ‘wlwmanifest_link’);” codes on functions.php I got errors on wordpress error.logs;

    Unknown: Failed opening required for xmlrpc.php

    is it normal or how can I solve this problem?

  9. Hi thanks for this tut,
    I want to draw your attention to this :
    Since WordPress 4.4.0 you can use the ‘feed_links_show_comments_feed filter’ to specifically remove comments feed link:

    add_filter( ‘feed_links_show_comments_feed’, ‘__return_false’ );

    So u might have to update the post.

    Thanks.

      1. remove_action(‘wp_head’, ‘wlwmanifest_link’);
        remove_action(‘wp_head’, ‘rsd_link’);

Leave a Reply

Your email address will not be published. Required fields are marked *