Cleaning up WordPress head tag

How to clean up WordPress head tag?

Hi there folks! Sometimes when I look at WordPress <head> tag contents I get headaches 🤯. For one of the projects I decided to clean it up from the things that I don't really need. If you are like me and you want to have only the things you want in <head> tag, this article might be helpful.

Default <head> tag on clean installation (before)

For the purpose of this article I installed a clean WordPress 5.2 with default theme Twenty Nineteen 1.4.

Here is how the <head> tag looks like for fresh version of WordPress:

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="profile" href="https://gmpg.org/xfn/11" />
    <title>Clean wp-head</title>
    <link rel='dns-prefetch' href='//s.w.org' />
    <link rel="alternate" type="application/rss+xml" title="Clean wp-head » Feed" href="https://clean-wp-head.dev/feed" />
    <link rel="alternate" type="application/rss+xml" title="Clean wp-head » Comments Feed" href="https://clean-wp-head.dev/comments/feed" />
    <script type="text/javascript">black
        window._wpemojiSettings = {"baseUrl":"https:\/\/s.w.org\/images\/core\/emoji\/12.0.0-1\/72x72\/","ext":".png","svgUrl":"https:\/\/s.w.org\/images\/core\/emoji\/12.0.0-1\/svg\/","svgExt":".svg","source":{"concatemoji":"https:\/\/clean-wp-head.dev\/wp-includes\/js\/wp-emoji-release.min.js?ver=5.2"}};
        !function(a,b,c){function d(a,b){var c=String.fromCharCode;l.clearRect(0,0,k.width,k.height),l.fillText(c.apply(this,a),0,0);var d=k.toDataURL();l.clearRect(0,0,k.width,k.height),l.fillText(c.apply(this,b),0,0);var e=k.toDataURL();return d===e}function e(a){var b;if(!l||!l.fillText)return!1;switch(l.textBaseline="top",l.font="600 32px Arial",a){case"flag":return!(b=d([55356,56826,55356,56819],[55356,56826,8203,55356,56819]))&&(b=d([55356,57332,56128,56423,56128,56418,56128,56421,56128,56430,56128,56423,56128,56447],[55356,57332,8203,56128,56423,8203,56128,56418,8203,56128,56421,8203,56128,56430,8203,56128,56423,8203,56128,56447]),!b);case"emoji":return b=d([55357,56424,55356,57342,8205,55358,56605,8205,55357,56424,55356,57340],[55357,56424,55356,57342,8203,55358,56605,8203,55357,56424,55356,57340]),!b}return!1}function f(a){var c=b.createElement("script");c.src=a,c.defer=c.type="text/javascript",b.getElementsByTagName("head")[0].appendChild(c)}var g,h,i,j,k=b.createElement("canvas"),l=k.getContext&&k.getContext("2d");for(j=Array("flag","emoji"),c.supports={everything:!0,everythingExceptFlag:!0},i=0;i<j.length;i++)c.supports[j[i]]=e(j[i]),c.supports.everything=c.supports.everything&&c.supports[j[i]],"flag"!==j[i]&&(c.supports.everythingExceptFlag=c.supports.everythingExceptFlag&&c.supports[j[i]]);c.supports.everythingExceptFlag=c.supports.everythingExceptFlag&&!c.supports.flag,c.DOMReady=!1,c.readyCallback=function(){c.DOMReady=!0},c.supports.everything||(h=function(){c.readyCallback()},b.addEventListener?(b.addEventListener("DOMContentLoaded",h,!1),a.addEventListener("load",h,!1)):(a.attachEvent("onload",h),b.attachEvent("onreadystatechange",function(){"complete"===b.readyState&&c.readyCallback()})),g=c.source||{},g.concatemoji?f(g.concatemoji):g.wpemoji&&g.twemoji&&(f(g.twemoji),f(g.wpemoji)))}(window,document,window._wpemojiSettings);
    </script>
    <style type="text/css">
        img.wp-smiley,
        img.emoji {
            display: inline !important;
            border: none !important;
            box-shadow: none !important;
            height: 1em !important;
            width: 1em !important;
            margin: 0 .07em !important;
            vertical-align: -0.1em !important;
            background: none !important;
            padding: 0 !important;
        }
    </style>
    <link rel='stylesheet' id='wp-block-library-css'  href='https://clean-wp-head.dev/wp-includes/css/dist/block-library/style.min.css?ver=5.2' type='text/css' media='all' />
    <link rel='stylesheet' id='wp-block-library-theme-css'  href='https://clean-wp-head.dev/wp-includes/css/dist/block-library/theme.min.css?ver=5.2' type='text/css' media='all' />
    <link rel='stylesheet' id='twentynineteen-style-css'  href='https://clean-wp-head.dev/wp-content/themes/twentynineteen/style.css?ver=1.4' type='text/css' media='all' />
    <link rel='stylesheet' id='twentynineteen-print-style-css'  href='https://clean-wp-head.dev/wp-content/themes/twentynineteen/print.css?ver=1.4' type='text/css' media='print' />
    <link rel='https://api.w.org/' href='https://clean-wp-head.dev/wp-json/' />
    <link rel="EditURI" type="application/rsd+xml" title="RSD" href="https://clean-wp-head.dev/xmlrpc.php?rsd" />
    <link rel="wlwmanifest" type="application/wlwmanifest+xml" href="https://clean-wp-head.dev/wp-includes/wlwmanifest.xml" />
    <meta name="generator" content="WordPress 5.2" />
</head>

Lot of stuff, right? I usually remove like 80% of this code! Things that I leave unchanged are:

  • meta tags for setting charset and viewport
  • title tag
  • links to theme stylesheets

Remove or set up DNS prefetch links

By default WordPress is prefetching s.w.org domain. This is only used for emojis script in WordPress. Since emojis are working fine in most browsers I remove the DNS prefetch attribute or I add the things that I really use.

There is a way to remove all the resource hints from the <head>:

remove_action( 'wp_head', 'wp_resource_hints', 2 );

However I prefer slightly different and safer approach:

/**
 * Alter dns-prefetch links in <head>
 */
add_filter('wp_resource_hints', function (array $urls, string $relation): array {
    // If the relation is different than dns-prefetch, leave the URLs intact
    if ($relation !== 'dns-prefetch') {
        return $urls;
    }

    // Remove s.w.org entry
    $urls = array_filter($urls, function (string $url): bool {
        return strpos($url, 's.w.org') === false;
    });

    // List of domains to prefetch:
    $dnsPrefetchUrls = [
        'fonts.googleapis.com', // Google fonts,
        'any.other.website.url.you.need...'
    ];
    return array_merge($urls, $dnsPrefetchUrls);
}, 10, 2);

The first method (removing everything) might interfere with some plugins that could actually add something helpful. So I leave everything as is, except of removing the s.w.org entry and adding the things I want:)

Remove XML feeds

There are two entries to for the XML feed:

<link rel="alternate" type="application/rss+xml" title="Clean wp-head » Feed" href="https://clean-wp-head.dev/feed" />
<link rel="alternate" type="application/rss+xml" title="Clean wp-head » Comments Feed" href="https://clean-wp-head.dev/comments/feed" />

In most cases I don't want to have it in my<head> tag. I also like to have it disabled entirely. Even if you remove the entries from <head> the URLs in href attribute will still be accessible.

First of all, let's disable the feeds by redirecting the URLs to homepage:

/**
* Disable RSS feeds by redirecting their URLs to homepage
*/
foreach (['do_feed_rss2', 'do_feed_rss2_comments'] as $feedAction) {
add_action($feedAction, function (): void {
// Redirect permanently to homepage
wp_redirect(home_url(), 301);
exit;
}, 1);
}

Now it's safe to remove the tags from the <head> section:

/**
* Remove the feed links from <head>
*/
remove_action('wp_head', 'feed_links', 2);

Remove emojis

DNS prefetch entry for emojis is already removed. Now it's time to get rid of the entire code for emojis that remains in head section:

<script type="text/javascript">black
        window._wpemojiSettings = {"baseUrl":"https:\/\/s.w.org\/images\/core\/emoji\/12.0.0-1\/72x72\/","ext":".png","svgUrl":"https:\/\/s.w.org\/images\/core\/emoji\/12.0.0-1\/svg\/","svgExt":".svg","source":{"concatemoji":"https:\/\/clean-wp-head.dev\/wp-includes\/js\/wp-emoji-release.min.js?ver=5.2"}};
        !function(a,b,c){function d(a,b){var c=String.fromCharCode;l.clearRect(0,0,k.width,k.height),l.fillText(c.apply(this,a),0,0);var d=k.toDataURL();l.clearRect(0,0,k.width,k.height),l.fillText(c.apply(this,b),0,0);var e=k.toDataURL();return d===e}function e(a){var b;if(!l||!l.fillText)return!1;switch(l.textBaseline="top",l.font="600 32px Arial",a){case"flag":return!(b=d([55356,56826,55356,56819],[55356,56826,8203,55356,56819]))&&(b=d([55356,57332,56128,56423,56128,56418,56128,56421,56128,56430,56128,56423,56128,56447],[55356,57332,8203,56128,56423,8203,56128,56418,8203,56128,56421,8203,56128,56430,8203,56128,56423,8203,56128,56447]),!b);case"emoji":return b=d([55357,56424,55356,57342,8205,55358,56605,8205,55357,56424,55356,57340],[55357,56424,55356,57342,8203,55358,56605,8203,55357,56424,55356,57340]),!b}return!1}function f(a){var c=b.createElement("script");c.src=a,c.defer=c.type="text/javascript",b.getElementsByTagName("head")[0].appendChild(c)}var g,h,i,j,k=b.createElement("canvas"),l=k.getContext&&k.getContext("2d");for(j=Array("flag","emoji"),c.supports={everything:!0,everythingExceptFlag:!0},i=0;i<j.length;i++)c.supports[j[i]]=e(j[i]),c.supports.everything=c.supports.everything&&c.supports[j[i]],"flag"!==j[i]&&(c.supports.everythingExceptFlag=c.supports.everythingExceptFlag&&c.supports[j[i]]);c.supports.everythingExceptFlag=c.supports.everythingExceptFlag&&!c.supports.flag,c.DOMReady=!1,c.readyCallback=function(){c.DOMReady=!0},c.supports.everything||(h=function(){c.readyCallback()},b.addEventListener?(b.addEventListener("DOMContentLoaded",h,!1),a.addEventListener("load",h,!1)):(a.attachEvent("onload",h),b.attachEvent("onreadystatechange",function(){"complete"===b.readyState&&c.readyCallback()})),g=c.source||{},g.concatemoji?f(g.concatemoji):g.wpemoji&&g.twemoji&&(f(g.twemoji),f(g.wpemoji)))}(window,document,window._wpemojiSettings);
    </script>
    <style type="text/css">
        img.wp-smiley,
        img.emoji {
            display: inline !important;
            border: none !important;
            box-shadow: none !important;
            height: 1em !important;
            width: 1em !important;
            margin: 0 .07em !important;
            vertical-align: -0.1em !important;
            background: none !important;
            padding: 0 !important;
        }
    </style>

There is both a JS code and some inline styles. Here is the code to remove both of them:

/**
 * Remove emoji script and styles from <head>
 */
remove_action('wp_head', 'print_emoji_detection_script', 7);
remove_action('wp_print_styles', 'print_emoji_styles');

First line removes the JS code, second one removes the inline CSS styles.

Disable WordPress REST API

If you don't plan to build SPA or make Ajax calls to WordPress REST API you can safely disable it. I only leave it enable for admin user. Some of the plugins uses the new REST API for their functionalities in wp-admin so it's good to have it working there:

/**
* Disable REST-API for all users except of admin
*/
add_filter('rest_authentication_errors', function ($access) {
if (!current_user_can('administrator')) {
return new WP_Error('rest_cannot_access', 'Only authenticated users can access the REST API.', ['status' => rest_authorization_required_code()]);
}
return $access;
});

This code will disable the endpoint for all users that are not administrators of your page. If you try to visit it for instance as not logged in user, you will get 401 error.

In order to remove the entry from <head> section:

<link rel='https://api.w.org/' href='https://clean-wp-head.dev/wp-json/' />

use following code:

/**
 * Remove REST-AI link from <head>
 */
remove_action('wp_head', 'rest_output_link_wp_head');

Disable XML-RPC

New WordPress REST-API most probably will replace the old XML-RPC protocol. I like to keep it disabled by using following code:

/**
* Disable XML-RPC
*/
add_filter('xmlrpc_enabled', function (): bool {
return false;
});

With this code when you try to send request to xmlrpc.php file you will get following message: XML-RPC services are disabled on this site.

But still, in <head> tag there is an entry to the XML-RPC:

<link rel="EditURI" type="application/rsd+xml" title="RSD" href="https://clean-wp-head.dev/xmlrpc.php?rsd" />

Remove it with following code:

/**
 * Remove XML-RPC link from <head>
 */
remove_action('wp_head', 'rsd_link');

Remove Windows Live Writer manifest

If you don't use Windows Live Writer you can easily remove the manifest link from <head> section:

<link rel="wlwmanifest" type="application/wlwmanifest+xml" href="https://clean-wp-head.dev/wp-includes/wlwmanifest.xml" />

In order to remove it, use following code:

/**
 * Remove Windows Live Writer manifest from <head>
 */
remove_action('wp_head', 'wlwmanifest_link');

Remove info about generator

WordPress adds the information about the version of currently installed WP:

<meta name="generator" content="WordPress 5.2" />

You can easily remove it with this code:

/**
* Remove info about WordPress version from <head>
*/
remove_action('wp_head', 'wp_generator');

Bonus 1 - remove Gutenberg default styles

If you don't use Gutenberg or you plan to override the default styles that comes with this editor, you can easily remove the styles from <head> tag:

<link rel='stylesheet' id='wp-block-library-css'  href='https://clean-wp-head.dev/wp-includes/css/dist/block-library/style.min.css?ver=5.2' type='text/css' media='all' />
<link rel='stylesheet' id='wp-block-library-theme-css'  href='https://clean-wp-head.dev/wp-includes/css/dist/block-library/theme.min.css?ver=5.2' type='text/css' media='all' />

In order to remove these links, use following code:

/**
 * Remove Gutenberg default styles
 */
add_action('wp_print_styles', function (): void {
    wp_dequeue_style('wp-block-library');
    wp_dequeue_style('wp-block-library-theme');
});

Bonus 2 - remove unnecessary attributes from link stylesheets

That might be crazy, but if id and type attributes in link styles are annoying, you can do something about it like I do:

/**
 * Remove unnecessary attributes from style tags
 */
add_filter('style_loader_tag', function (string $tag, string $handle): string {
    // Remove ID attribute
    $tag = str_replace("id='${handle}-css'", '', $tag);

    // Remove type attribute
    $tag = str_replace(" type='text/css'", '', $tag);

    // Change ' to " in attributes:
    $tag = str_replace('\'', '"', $tag);

    // Remove trailing slash
    $tag = str_replace(' />', '>', $tag);

    // Remove double spaces
    return str_replace('  ', '', $tag);
}, 10, 2);

Moreover I don't really like that the attributes are wrapped in ' instead of " so this code changes it too:)

Bonus 3 - Remove XFN meta data profile

Just right below meta tags there is a particular line:

<link rel="profile" href="https://gmpg.org/xfn/11" />

It is a link for XFN which is XHTML Friends Network. This is the way of describing relationships between users in machine-readable way. You can read more about how to use it on WordPress website here.

This tag is added directly in header.php file and it needs to be removed manually. There is no hook for removing it. Simply edit the file and remove this tag from <head> section.

<head> tag after the changes

The contents of <head> tag after some additional clean-ups in header.php file looks like this:

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Clean wp-head</title>
    <link rel="stylesheet" href="https://clean-wp-head.dev/wp-content/themes/twentynineteen/style.css?ver=1.4" media="all" >
    <link rel="stylesheet" href="https://clean-wp-head.dev/wp-content/themes/twentynineteen/print.css?ver=1.4" media="print" >
</head>

Quite a difference, isn't it? From 35 lines just down to 7 😁

Can I get full code, please?

I putted everything together in single file on GitHub Gist.

You can include this code in your plugin or functions.php. You can also copy the entire thing (without the <?php tag of course!) into the functions.php. However the best place to keep it is inside of a plugin:)