Open your site’s source code (Ctrl+U on Windows, Cmd+Option+U on a Mac) and scroll to the <head> section. Between charset, the viewport and the theme CSS there are often a dozen <link>, <meta> and <script> lines nobody on the editorial team asked for: comment feeds, an RSD discovery link, an emoji settings block, a shortlink with ?p=123 and a generator meta tag with the core version.
These artefacts come from wp_head(). Core attaches callbacks to that hook, and remove_action() takes back out what your project does not need. This guide covers every typical artefact, a safe removal list checked against wp-includes/default-filters.php in WordPress 7.1.3, the things to leave alone, the difference between the emoji script and the emoji styles, the shortlink versus the canonical, HTTP-level headers, and the choice between a must-use plugin and the theme’s functions.php.
The job is less noise in the HTML and in the network panel. Removing these tags will not move a Lighthouse score on its own, but it makes the document readable again and closes a few small information leaks. Learn more about WordPress speed optimization if the goal is load time.
What wp_head prints in the WordPress header
wp_head is an action hook. Classic themes call wp_head() in header.php; block themes get the call from core’s template canvas. Core and plugins register callbacks on it with priorities. Removing a callback removes only that callback’s output; the hook itself keeps running.
Typical core callbacks in the front-end <head> of WordPress 7.1, with the priority from default-filters.php:
| Callback | Priority | What it prints | Verdict |
|---|---|---|---|
feed_links | 2 | <link rel="alternate"> for the posts feed and the comments feed | Posts feed often still useful, comments feed rarely |
feed_links_extra | 3 | Feeds for categories, tags, authors, searches, post comments | Mostly noise |
wp_oembed_add_discovery_links | 4 | oEmbed JSON and XML discovery links on singular views | Keep if other sites embed your posts |
print_emoji_detection_script | 7 | Schedules the emoji settings and loader (printed in the footer since 6.9) | Modern browsers draw emoji natively |
rsd_link | 10 | <link rel="EditURI"> to xmlrpc.php?rsd | Legacy |
wp_generator | 10 | meta name="generator" with the core version | Leak with no benefit |
rel_canonical | 10 | Canonical URL of the current view | Keep |
wp_shortlink_wp_head | 10 | <link rel="shortlink"> with ?p=ID | The canonical covers SEO |
rest_output_link_wp_head | 10 | Discovery link to the REST API | Keep when the editor, apps or a headless front end run |
wp_site_icon | 99 | Favicon and touch icons | Keep |
wp_custom_css_cb | 101 | Additional CSS from the Customizer | Keep |
Two rows that older snippets still mention are gone from this list. wlwmanifest_link was deprecated in WordPress 6.3 and is no longer hooked, and adjacent_posts_rel_link_wp_head has not been used by core since 5.6. The emoji styles moved from print_emoji_styles to wp_enqueue_emoji_styles in 6.4. Each change is covered below.
The table replaces guesswork. Before the first remove_action, view the source of a staging page and compare it with this list.
How to clean the WordPress header with remove_action
The function below is the core of the cleanup. It runs on init, so the callbacks you remove have already been registered. The priority you pass must match the original registration: feed_links is registered at 2, feed_links_extra at 3, the emoji detection script at 7. remove_action() takes three parameters (hook, callback, priority); the fourth argument many snippets pass is ignored.
/**
* Clean up the WordPress head section.
* Removes legacy links, the generator, emoji assets and shortlinks.
*/
function wppoland_cleanup_head() {
// RSS: main and extra feed links out of the head.
remove_action( 'wp_head', 'feed_links', 2 );
remove_action( 'wp_head', 'feed_links_extra', 3 );
// Really Simple Discovery (EditURI link).
remove_action( 'wp_head', 'rsd_link' );
// Generator meta tag and the generator in feeds.
remove_action( 'wp_head', 'wp_generator' );
add_filter( 'the_generator', '__return_empty_string' );
// Emoji: detection script, styles, feed and mail conversion.
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
remove_action( 'embed_head', 'print_emoji_detection_script' );
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' );
// Shortlink in the HTML head and in the HTTP Link header.
remove_action( 'wp_head', 'wp_shortlink_wp_head', 10 );
remove_action( 'template_redirect', 'wp_shortlink_header', 11 );
}
add_action( 'init', 'wppoland_cleanup_head' );Notes on using it:
- If newsletter tools or external aggregators read your main feed, keep
feed_linksand switch off only the feeds you do not need with the filters described in the RSS section below. rsd_linkcan go on practically every public site. Remote publishing over XML-RPC is a separate risk topic anyway.the_generatoralso applies to feed XML. An empty string removes the version number there too.
Which remove_action lines do nothing on WordPress 7
Cleanup snippets copied from forums carry lines that no longer match anything in core. They do not break a site, because remove_action() simply returns false when the callback is not registered. They do mislead the next developer into thinking the line is doing work.
remove_action( 'wp_head', 'wlwmanifest_link' ): the Windows Live Writer manifest link was deprecated in WordPress 6.3. The function now only raises a deprecation notice and core no longer hooks it. Keep the line only if you still support sites below 6.3.remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head', 10 ): the function still exists, but its own docblock says it has not been used in core since 5.6. If you seerel="prev"andrel="next"on single posts, a theme or plugin adds them.start_post_rel_link,index_rel_link,parent_post_rel_link: deprecated in WordPress 3.3. No modern install hooks them.remove_filter( 'wp_resource_hints', 'wp_dependencies_emoji_styles' ): there is no such function in core. The DNS prefetch tos.w.orgthat this line was meant to stop was removed fromwp_resource_hints()in WordPress 6.1.add_filter( 'rest_enabled', '__return_false' ): the filter has been deprecated since 4.7. Core points torest_authentication_errorsinstead, and the REST API can no longer be switched off this way.
Deleting these lines makes the file shorter and the review easier. The source of default-filters.php in 7.1.3 is the reference to compare against.
Which wp_head entries you should not remove
Cleanup ends where core and the editor still need to work. Four classes belong on the keep list.
REST API discovery
rest_output_link_wp_head writes a link to /wp-json/, and rest_output_link_header sends the same link as an HTTP header. The block editor, the site editor, many form plugins and headless WordPress front ends depend on the REST API. Removing the discovery link breaks nothing immediately, but clients find the endpoint less easily and you solve a non-problem. To secure the REST API, restrict authentication and routes instead of hiding the link.
Theme and plugin CSS
Stylesheets arrive through wp_enqueue_style and are printed by wp_print_styles at priority 8. Emptying wp_head completely or killing all styles destroys the layout. Cleanup targets legacy core callbacks, not wp_enqueue_*. The same goes for scripts enqueued by the theme and plugins: deregister them only when you know the dependency.
Canonical, Open Graph and meta tags
rel_canonical stays. Without a canonical you risk duplicate-content signals on variants with query strings, pagination or tracking parameters. Open Graph and social tags from SEO plugins stay untouched as long as the plugin sets them correctly.
Title, robots, icons and custom CSS
_wp_render_title_tag and wp_robots run at priority 1 and print the <title> and the robots meta. wp_site_icon prints the favicon at 99, and wp_custom_css_cb prints the Customizer’s additional CSS at 101. None of them is bloat.
In short: extra feeds, RSD, the generator, emoji assets and shortlinks are candidates. REST, CSS, the canonical, the title, robots and icons are infrastructure.
How to disable WordPress emojis without a plugin
Many snippets remove only the detection script and forget the styles, or the other way round. Core keeps the two paths separate, and both changed in recent releases.
print_emoji_detection_script is hooked to wp_head at priority 7 and to admin_print_scripts. Since WordPress 6.9 it no longer prints inline in the head. It schedules _print_emoji_detection_script on wp_print_footer_scripts, which outputs a JSON settings block (id="wp-emoji-settings") and the emoji loader as a type="module" script before </body>. The loader tests whether the browser draws emoji natively and stores the result in sessionStorage. Only when the test fails does it add wp-emoji-release.min.js, which swaps emoji for images from s.w.org. On a current browser the cost is the inline settings and the loader code (the unminified loader source is about 13 KB), not a full polyfill download.
The styles took a different route. print_emoji_styles was deprecated in 6.4 and replaced by wp_enqueue_emoji_styles, which enqueues an inline style under the wp-emoji-styles handle. For backwards compatibility core still hooks print_emoji_styles to wp_print_styles, and the new function returns early if that hook was removed. So remove_action( 'wp_print_styles', 'print_emoji_styles' ) still works on 7.1. An equivalent and more direct line is remove_action( 'wp_enqueue_scripts', 'wp_enqueue_emoji_styles' ).
The filters wp_staticize_emoji (on the_content_feed and comment_text_rss) and wp_staticize_emoji_for_email (on wp_mail) convert emoji into images in feeds and mail. On editorial sites without emoji-heavy newsletters you can remove them too. If your notification mails rely on emoji, keep the mail filter and clean only the front end.
Remove the admin hooks as well, or the emoji assets stay in the dashboard. A front-end-only cleanup saves bytes for visitors, not for editors.
How to host WordPress emoji images yourself
Removal is one option. If you want to keep emoji support for old browsers but avoid requests to a third-party domain, point the image base URLs at your own server. Core exposes two filters for that: emoji_url for the PNG set and emoji_svg_url for the SVG set. In WordPress 7.1 the defaults are https://s.w.org/images/core/emoji/17.0.2/72x72/ and https://s.w.org/images/core/emoji/17.0.2/svg/.
add_filter( 'emoji_url', function () {
return content_url( '/emoji/17.0.2/72x72/' );
} );
add_filter( 'emoji_svg_url', function () {
return content_url( '/emoji/17.0.2/svg/' );
} );The version segment in the path changes when core updates its emoji set, so a self-hosted copy has to be refreshed with major releases. wp_staticize_emoji uses the same emoji_url filter for feeds, so one change covers both places.
How to remove the WordPress shortlink but keep the canonical
wp_shortlink_wp_head writes <link rel="shortlink" href="https://example.com/?p=42" />. In addition, wp_shortlink_header runs on template_redirect at priority 11 and sends an HTTP header Link: <...>; rel=shortlink. Both are a convenience for old sharing tools. Search engines follow the canonical, not the shortlink.
rel_canonical points to the canonical permalink. The shortlink and the canonical do not contradict each other directly, but two URL signals for the same resource add noise. Shortlink out, canonical in: a clear preference for the pretty permalink.
If a redirect plugin or a CDN maintains short URLs, check the response headers once after the cleanup in DevTools (Network, then the document request, then Response Headers). The Link header with rel=shortlink should be gone. The canonical in the HTML stays visible.
How to remove X-Pingback and other WordPress HTTP headers
The <head> is only half of what WordPress adds. Three response headers come from core.
X-Pingback: since WordPress 4.4,WP::send_headers()adds it only on singular views of posts that accept pings. It points toxmlrpc.php.Link: <...>; rel=shortlink: sent bywp_shortlink_header, removed with the shortlink code above.Link: <...>; rel="https://api.w.org/": sent byrest_output_link_header. Keep it for the same reasons as the REST link in the HTML.
All headers collected by WP::send_headers() pass through the wp_headers filter (available since 2.8), so X-Pingback can be removed there:
add_filter( 'wp_headers', function ( $headers ) {
unset( $headers['X-Pingback'] );
return $headers;
} );If pings are closed on a post, the header is not sent in the first place. Core does not hook a <link rel="pingback"> tag to wp_head; when you see one in the source, it comes from the theme’s header.php.
Should header cleanup go in an mu-plugin or functions.php
Both places work. The operational question is survival.
functions.php in a child theme is quick: SFTP or the theme editor, save, done. The downside: a theme switch or a new child theme deletes the logic. Agencies that swap client themes lose the cleanup silently.
A must-use plugin at wp-content/mu-plugins/wppoland-clean-head.php is loaded automatically, without an activation click. A theme switch changes nothing. Deployment through Git or rsync stays simple because the file lives outside the theme. Create the folder once and version the file with the rest of the project.
Recommendation for production sites: an mu-plugin. For one-off experiments on staging a child theme is acceptable, as long as you migrate the function later. Avoid editing the parent theme; updates overwrite it.
A skeleton for the mu-plugin:
<?php
/**
* Plugin Name: WPPoland Clean Head
* Description: Removes legacy wp_head artefacts.
* Author: wppoland.com
* Version: 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
require_once __DIR__ . '/includes/cleanup-head.php';The actual wppoland_cleanup_head function then lives in includes/cleanup-head.php. The bootstrap file stays short and testable. Add a comment next to each removal that says why it is there:
// Remove emoji detection: every supported browser renders emoji natively.
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );Clean wp_head only on the front end or for headless
Some removals belong only in specific contexts. The example below keeps the REST link when a constant of your own marks the site as headless.
function wppoland_conditional_head_cleanup() {
// WPPOLAND_HEADLESS is a project constant set in wp-config.php.
if ( ! defined( 'WPPOLAND_HEADLESS' ) || ! WPPOLAND_HEADLESS ) {
remove_action( 'wp_head', 'rest_output_link_wp_head', 10 );
}
}
add_action( 'init', 'wppoland_conditional_head_cleanup' );Make each part of the cleanup switchable
On sites where integrations differ between environments, wrap each group of removals in a constant from wp-config.php. A group runs unless its constant is defined as false:
/**
* Configure in wp-config.php, for example:
* define( 'WPPCLEANUP_FEED_LINKS', false );
*/
function wppoland_advanced_cleanup_head() {
if ( ! defined( 'WPPCLEANUP_FEED_LINKS' ) || WPPCLEANUP_FEED_LINKS ) {
remove_action( 'wp_head', 'feed_links', 2 );
remove_action( 'wp_head', 'feed_links_extra', 3 );
}
if ( ! defined( 'WPPCLEANUP_RSD' ) || WPPCLEANUP_RSD ) {
remove_action( 'wp_head', 'rsd_link' );
}
if ( ! defined( 'WPPCLEANUP_GENERATOR' ) || WPPCLEANUP_GENERATOR ) {
remove_action( 'wp_head', 'wp_generator' );
add_filter( 'the_generator', '__return_empty_string' );
}
if ( ! defined( 'WPPCLEANUP_EMOJIS' ) || WPPCLEANUP_EMOJIS ) {
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
}
if ( ! defined( 'WPPCLEANUP_SHORTLINKS' ) || WPPCLEANUP_SHORTLINKS ) {
remove_action( 'wp_head', 'wp_shortlink_wp_head', 10 );
remove_action( 'template_redirect', 'wp_shortlink_header', 11 );
}
}
add_action( 'init', 'wppoland_advanced_cleanup_head' );Staging can then keep the feeds while production drops them, without two versions of the plugin file.
How to check the cleaned header in the page source
After the deploy, open a public URL in a private window and check three layers.
- Page source (
Ctrl+U): search forEditURI,emoji,shortlink,generatorandalternate. Since WordPress 6.9 the emoji settings sit before</body>, so search the whole document, not only the head. The legacy hits should be missing. The canonical, stylesheets and required plugin tags remain. - DevTools Network: filter for
s.w.organdwp-emoji. After the emoji cleanup no request should appear. Compare head requests before and after the change on the same URL. - Response headers: no
Linkheader withrel=shortlink, and noX-Pingbackif you removed it. Some security plugins set their own headers; those stay unaffected.
Checklist for the acceptance run:
- Test staging and production separately; flush the object cache and the page cache.
- Check a single post, an archive page and the home page (extra feeds behave differently on each).
- Check a WooCommerce product page separately: plugins often add tags there that do not come from this snippet.
- Open the block editor once and save a post: the REST API has to answer.
- Optionally run PageSpeed Insights or WebPageTest and compare HTML bytes and third-party domains. Expect less noise, not a new score.
If an emoji script still shows up after the cleanup, check which plugin loads it. remove_action on core hooks does not stop a plugin that enqueues its own copy, so read the source rather than trusting the snippet.
How to remove RSS feed links from the WordPress head
RSS is not dead. Podcasts, newsletter imports and monitoring tools still read /feed/. Comment feeds and per-taxonomy feeds, on the other hand, rarely bring traffic and fill the head on archive pages.
Pragmatic levels:
- Comment feeds only (
feed_links_show_comments_feedreturns false). The main feed stays. - Extra feeds off (remove
feed_links_extra). Categories and tags lose autodiscovery in the head; the feed URLs keep working for anyone who knows them. - All feed links out of the head (
feed_linksandfeed_links_extra). The endpoints stay reachable, only autodiscovery goes.
Level 1 or 2 is enough for most company sites. Level 3 only if nobody subscribes to the feed and you want that on purpose.
feed_links() prints nothing unless the theme declares add_theme_support( 'automatic-feed-links' ). If a site shows no feed links at all, check the theme before adding removal code.
Keep the main feed and drop only some extra feeds
Removing feed_links_extra is all or nothing. Core offers finer switches. feed_links_show_posts_feed and feed_links_show_comments_feed exist since WordPress 4.4. WordPress 6.1 added one filter per extra feed type: feed_links_extra_show_post_comments_feed, feed_links_extra_show_post_type_archive_feed, feed_links_extra_show_category_feed, feed_links_extra_show_tag_feed, feed_links_extra_show_tax_feed, feed_links_extra_show_author_feed and feed_links_extra_show_search_feed. Since 6.7 feed_links_args filters the separator and the titles of the feed links.
// Keep the posts feed and category feeds, drop the rest of the extras.
add_filter( 'feed_links_show_comments_feed', '__return_false' );
add_filter( 'feed_links_extra_show_tag_feed', '__return_false' );
add_filter( 'feed_links_extra_show_author_feed', '__return_false' );
add_filter( 'feed_links_extra_show_search_feed', '__return_false' );feed_links_extra_show_post_comments_feed takes the value of feed_links_show_comments_feed as its default, so switching off the global comments feed also removes the per-post comments feed link unless you return true for it separately. The reference entry for feed_links_extra_show_category_feed lists the parameters.
How to hide the WordPress version in the source
wp_generator writes the core version into the HTML. That is not a breach, but scanners prioritise versions with known CVEs. Remove the meta tag and empty the_generator. The version also leaks in other places:
- Asset query strings: core styles and scripts carry
?ver=with the core version, for example onwp-emoji-release.min.js. - readme.html: the file sits in the WordPress root (
/readme.html), not inwp-content, and is restored by every core update. Block it at the web server if it matters to you. - Plugins: many plugins print their own version in comments or asset URLs.
Head cleanup alone is not a security programme; it removes one signal. Updating core and plugins is what closes the CVEs a scanner looks for.
Remove the ver parameter from CSS and JS URLs
The style_loader_src and script_loader_src filters receive every enqueued asset URL, so the ver argument can be stripped there:
function wppoland_remove_version_from_assets( $src ) {
if ( $src && strpos( $src, 'ver=' ) !== false ) {
$src = remove_query_arg( 'ver', $src );
}
return $src;
}
add_filter( 'style_loader_src', 'wppoland_remove_version_from_assets', 9999 );
add_filter( 'script_loader_src', 'wppoland_remove_version_from_assets', 9999 );The parameter also busts browser and CDN caches after an update. Without it, a changed file can be served from cache under the same URL, so keep this filter only when your deploy or CDN handles cache invalidation in another way.
Do rel prev and next still bring SEO benefits
rel="prev" and rel="next" between single posts once helped with series. For single posts core has not printed them since 5.6, so on a current site they come from the theme or an SEO plugin. For modern crawlers the effect is negligible. On magazine sites with long article chains you can keep them; on service and shop sites remove them in the plugin that adds them. Internal links in the content and sensible sitemaps weigh more.
Header cleanup with WP Rocket and a page cache
Autoptimize, FlyingPress, WP Rocket and similar tools change the order and combination of assets. Your remove_action snippet should act before the HTML cache, that is in PHP, not as a later string replace in the optimiser. If an optimiser re-injects emoji scripts or allows third-party domains, check its settings.
An object cache does not store wp_head output in a way that would make remove_action ineffective. A page cache does: old HTML copies keep the extra tags until the cache is emptied. Purging the cache after the deploy is mandatory.
Why remove_action in wp_head does not work
Wrong priority: remove_action( 'wp_head', 'feed_links' ) without priority 2 does not hit the callback. Core registers with a priority, and your removal has to carry the same number.
Hook too early: if you run remove_action at the top level of a plugin file, some callbacks are not registered yet. init or wp_loaded are safer than top-level code.
Parent theme edited: the update arrives, the cleanup is gone. Use a child theme or an mu-plugin.
REST removed from copy-paste lists: the editor breaks or headless apps cannot find /wp-json/. Lists from 2014 forum threads are risky without checking; compare them with the table in this article.
Stale callback names: the line targets a function core no longer hooks, as listed in the section on no-op lines. The tag you are trying to remove then comes from somewhere else.
Front end tested only: the admin keeps the emoji assets. Visitors do not notice, editors on slow connections do. Include the admin hooks if the team agrees.
Summary
wp_head collects core, theme and plugin output. Legacy artefacts (RSD, extra feeds, the generator, emoji assets, the shortlink) can be removed with remove_action and a few filters. REST discovery, enqueued CSS and JS, the title, robots, icons and the canonical stay. Treat the emoji script and the emoji styles separately, and know that the script prints in the footer since 6.9. Remove the shortlink in the HTML and in the HTTP header, keep the canonical. Drop the lines for wlwmanifest_link and the adjacent-post links on current versions. The logic belongs in an mu-plugin, not in the parent theme. Verification runs through a source search, the network panel and the response headers, plus a cache purge.
References from the Developer Handbook: wp_head, remove_action, feed_links, feed_links_extra, print_emoji_detection_script, wp_enqueue_emoji_styles, wp_shortlink_wp_head, wp_shortlink_header, wp_generator, rsd_link, wlwmanifest_link, rel_canonical, wp_headers.
Need help applying this on staging and production? A Core Web Vitals audit covers wp_head together with the rest of the critical rendering path.







