TechEarl

Clean Up WordPress wp_head: Emojis, Feeds and Discovery Tags

Remove unused WordPress head output with a configurable plugin. Keep feeds and consumed embeds working, preserve cache-busting, and test each change.

Ishan Karunaratne⏱️ 7 min readUpdated
Share thisCopied
What WordPress outputs in wp_head by default, which tags and scripts are safe to remove, and a single must-use plugin that strips the emoji, shortlink, feed, oEmbed, and generator bloat.

To clean up WordPress's head output, remove only the discovery tags and compatibility features your site does not use. Keep the canonical tag, required block styles, and asset versions. Removing a tag is different from disabling the feature behind it.

I use a small must-use plugin so these decisions survive a theme switch. The configurable example below covers emojis, shortlinks, feed discovery, oEmbed discovery, and generator tags. It leaves feed URLs and embedded content working unless I explicitly choose otherwise.

Choose only the features your site does not use

FeatureWhat removal changesCheck before removing
Emoji supportDetection, optional polyfill, styling and email/feed substitutionEmoji display on the browsers and operating systems you support
Shortlink discoveryHTML tag and HTTP Link headerClients that consume shortlinks; ?p=123 still works
Feed discoveryRSS/Atom <link> tagsSubscribers and integrations; feed endpoints remain live
oEmbed discoveryOther sites' discovery of your post embedsWhether other publishers embed your posts
Generator outputVersion metadataThis does not secure an outdated installation

The savings depend on what a page actually loads. A removed tag saves HTML bytes; a conditional script can also save a request when it would otherwise load. Measure the Network panel and page timing before claiming a speed improvement. Feeds and embeds are useful features, and their existence alone is not evidence of an SEO problem.

One configurable must-use plugin

Save this as wp-content/mu-plugins/te-head-cleanup.php. Create the directory if necessary. Change the booleans to match the site's requirements; the conservative default removes shortlink and generator discovery only.

php
<?php
/**
 * Plugin Name: TE Head Cleanup
 * Plugin URI: https://techearl.com/clean-up-wordpress-wp-head
 * Description: Selective cleanup of WordPress discovery output.
 * Version: 2.0.0
 * Author: Ishan Karunaratne
 * Author URI: https://techearl.com
 * License: GPL-2.0-or-later
 */
defined( 'ABSPATH' ) || exit;

add_action( 'init', function () {
    $features = array(
        'emojis'           => false,
        'shortlinks'       => true,
        'feed_links'       => false,
        'oembed_discovery' => false,
        'generator'        => true,
        'rsd'              => false,
    );

    if ( $features['emojis'] ) {
        remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
        remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
        remove_action( 'wp_enqueue_scripts', 'wp_enqueue_emoji_styles' );
        remove_action( 'admin_enqueue_scripts', 'wp_enqueue_emoji_styles' );
        // Compatibility with installations using the pre-6.4 callbacks.
        remove_action( 'wp_print_styles', 'print_emoji_styles' );
        remove_action( 'admin_print_styles', 'print_emoji_styles' );
        remove_filter( 'the_content_feed', 'wp_staticize_emoji' );
        remove_filter( 'comment_text_rss', 'wp_staticize_emoji' );
        remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
        add_filter( 'emoji_svg_url', '__return_false' );
        add_filter( 'tiny_mce_plugins', function ( $plugins ) {
            return is_array( $plugins )
                ? array_values( array_diff( $plugins, array( 'wpemoji' ) ) )
                : $plugins;
        } );
    }
    if ( $features['shortlinks'] ) {
        remove_action( 'wp_head', 'wp_shortlink_wp_head', 10 );
        remove_action( 'template_redirect', 'wp_shortlink_header', 11 );
    }
    if ( $features['feed_links'] ) {
        remove_action( 'wp_head', 'feed_links', 2 );
        remove_action( 'wp_head', 'feed_links_extra', 3 );
    }
    if ( $features['oembed_discovery'] ) {
        add_filter( 'oembed_discovery_links', '__return_empty_string' );
    }
    if ( $features['generator'] ) {
        remove_action( 'wp_head', 'wp_generator' );
        add_filter( 'the_generator', '__return_empty_string' );
    }
    if ( $features['rsd'] ) {
        remove_action( 'wp_head', 'rsd_link' );
    }
}, 100 );

The priority passed to remove_action() must match the original registration. For example, emoji detection uses priority 7, feed links use 2 and 3, and the shortlink header uses 11. Register the removal after core has registered the callback and before the callback runs.

Emoji scripts and styles

WordPress tests emoji support and may load a compatibility script when needed. Removing it does not remove Unicode characters from your posts, but it gives rendering responsibility to the visitor's browser and fonts. “Modern browser” does not guarantee support for every newer emoji sequence.

The plugin removes both the current enqueue callbacks and the older style-printing callbacks. print_emoji_styles() was deprecated in WordPress 6.4. The TinyMCE filter applies to the classic editor; it is not a block-editor setting. The feed and email filters control conversion to image-based emoji in those outputs, so include them only if that is the intended behavior.

These are separate outputs:

php
remove_action( 'wp_head', 'wp_shortlink_wp_head', 10 );
remove_action( 'template_redirect', 'wp_shortlink_header', 11 );

Removing discovery does not disable https://example.com/?p=123. WordPress can still resolve that query to the post. Do not remove that access path merely to hide the tag.

bash
curl -sS https://example.com/sample-post/ | grep -i shortlink
curl -sSI https://example.com/sample-post/ | grep -i shortlink
curl -sSI 'https://example.com/?p=123'

Expect no shortlink output from the first two when those options are enabled; inspect the third separately for the site's normal redirect or response.

Feed tags, comments feeds and feed URL behavior

Removing feed_links and feed_links_extra removes discovery, not /feed/, /comments/feed/, or ?feed=rss2. To keep the main feeds advertised but remove contextual links, remove only feed_links_extra at priority 3. To suppress the global comments-feed tag while keeping the posts-feed tag:

php
add_filter( 'feed_links_show_comments_feed', '__return_false' );

That filter also changes discovery only. Existing subscribers can still request the feed.

If the site has deliberately retired all feeds and has no equivalent replacement, a separate optional handler can return 410 Gone. Keep the rewrite rules so WordPress recognizes both pretty and query-string feed requests:

php
add_action( 'template_redirect', function () {
    if ( ! is_feed() ) {
        return;
    }
    nocache_headers();
    wp_die( 'This feed has been retired.', 'Feed unavailable', array( 'response' => 410 ) );
}, 1 );

For a moved feed, use a permanent redirect to the exact replacement feed instead. A homepage is not a replacement for every feed. Do not remove feed rewrite rules and then expect is_feed() to identify the now-unmatched URLs. Do not derive a redirect by merely stripping /feed/; query-string feeds need explicit handling and can otherwise redirect to themselves.

oEmbed provider cleanup without breaking pasted embeds

WordPress can both provide embeds of its posts and consume embeds from elsewhere. Hiding its provider discovery links is straightforward:

php
add_filter( 'oembed_discovery_links', '__return_empty_string' );

This filter has existed since WordPress 4.4. It avoids relying on a particular head-hook priority.

For an intentional provider shutdown, the following optional code removes only the provider REST route and redirects recognized post-embed documents to their actual published post. It keeps the consumer proxy and host script available:

php
add_filter( 'rest_endpoints', function ( $routes ) {
    unset( $routes['/oembed/1.0/embed'] );
    return $routes;
} );

add_action( 'template_redirect', function () {
    if ( ! is_embed() ) {
        return;
    }
    $post = get_queried_object();
    if ( $post instanceof WP_Post && is_post_publicly_viewable( $post ) ) {
        wp_safe_redirect( get_permalink( $post ), 301 );
        exit;
    }
}, 1 );

Removing a route from rest_endpoints makes it unavailable; it does not merely hide it from the API index. This is intentional for the provider route above.

Leave embed_oembed_discover unchanged if pasted embeds from arbitrary providers must continue working. Leave wp_oembed_add_host_js registered: WordPress uses its presence when deciding whether to enqueue wp-embed for consumed WordPress post embeds. Removing all wp_oembed_register_route registration would also remove the consumer proxy route.

Existing consumed embeds have per-post caches. Provider discovery cleanup does not require deleting those caches. If you change consumer behavior, use a scoped cache-maintenance procedure for affected posts and verify the available commands with wp help embed; never use wp post meta delete --all as an embed-cache shortcut, because that option concerns all metadata for a specified object.

Generator tags and asset versions

remove_action( 'wp_head', 'wp_generator' ) removes the HTML generator. The the_generator filter also suppresses generator strings in other supported output formats, including feeds. Neither patches a vulnerability or reliably hides a WordPress version. Keep core, plugins and themes updated.

Leave asset ?ver= parameters alone. They help invalidate browser/CDN caches when an asset changes. For a stylesheet you own, supply its real version when enqueueing it:

php
add_action( 'wp_enqueue_scripts', function () {
    $file = get_stylesheet_directory() . '/assets/site.css';
    if ( is_file( $file ) ) {
        wp_enqueue_style(
            'te-site',
            get_stylesheet_directory_uri() . '/assets/site.css',
            array(),
            (string) filemtime( $file )
        );
    }
} );

Do not replace versions on arbitrary plugin assets or use the current time as the version on every request.

Flush rewrite rules once and test removed and retained behavior

The examples here keep rewrite rules in place, so they need no rewrite flush. If you adapt the code to add or remove rules, rebuild the stored rule map once after deployment with wp rewrite flush or Settings → Permalinks. Repeat once after removing that rule-changing code. Do not flush on every request; must-use plugins have no activation hook.

After purging page caches, check a logged-out post, the homepage, a post containing a video and a WordPress-post embed, the editor, and any feed consumers. Match expectations to the enabled switches:

bash
curl -sS https://example.com/sample-post/ | grep -iE 'emoji|shortlink|oembed|generator|application/rss'
curl -sSI https://example.com/sample-post/
curl -sSI https://example.com/feed/
curl -sSI 'https://example.com/?feed=rss2'
curl -sSI https://example.com/sample-post/embed/

An empty grep result is not a universal pass condition: retained features should still appear, and a word such as “emoji” can occur in ordinary article text. Inspect the actual tags, HTTP status and browser behavior. Keep canonical tags intact.

For larger performance work, continue with ACF profiling, WooCommerce optimization, or the separately tested Dashicons, jQuery Migrate and block CSS decisions.

Sources

Authoritative references this article was fact-checked against.

TagsWordPressPerformancePHPwp_headPage Speedfunctions.php

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Systems and Network Architect · Chief Technology Officer

Systems and network architect and Chief Technology Officer with more than two decades designing, building, and running production software, cloud and network architecture, Linux systems, and the bare metal underneath them, and lately working AI into the stack. A US Army veteran who served in Operation Iraqi Freedom. What I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts

Hosting for WordPress agency clients by traffic tier and workload type: shared, managed WordPress, managed VPS, self-managed. Agency-side implications.

A WordPress Hosting Decision Tree for Agencies

Hosting choices for WordPress agency clients are operational decisions, not pricing decisions. The decision tree by traffic tier and workload type: shared, managed WordPress, managed VPS, self-managed VPS. Plus the agency-side implications of each.