In the Roots theme we’re taking several steps to ensure that a visitor to your website won’t know that you’re using WordPress:
- Cleaning up the output of
wp_headand removing the generator from RSS feeds - Hiding
/wp-content/by rewriting static theme assets (CSS, JS, and images), rewriting the plugins directory, and also changing the location of WordPress uploads - Cleaning up the output of navigation menus with a custom walker
- Bonus: root relative URLs
Cleaning up the output of wp_head
You’re probably used to viewing the source of your WordPress site and seeing some not-so-pretty code:
<!DOCTYPE html> <!--[if IE 6]> <html id="ie6" dir="ltr" lang="en-US"> <![endif]--> <!--[if IE 7]> <html id="ie7" dir="ltr" lang="en-US"> <![endif]--> <!--[if IE 8]> <html id="ie8" dir="ltr" lang="en-US"> <![endif]--> <!--[if !(IE 6) | !(IE 7) | !(IE 8) ]><!--> <html dir="ltr" lang="en-US"> <!--<![endif]--> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width" /> <title>Home | Example | Just another WordPress site</title> <link rel="profile" href="http://gmpg.org/xfn/11" /> <link rel="stylesheet" type="text/css" media="all" href="http://example.com/wp-content/themes/twentyeleven/style.css" /> <link rel="pingback" href="http://example.com/xmlrpc.php" /> <!--[if lt IE 9]> <script src="http://example.com/wp-content/themes/twentyeleven/js/html5.js" type="text/javascript"></script> <![endif]--> <link rel="alternate" type="application/rss+xml" title="Example » Feed" href="http://example.com/feed/" /> <link rel="alternate" type="application/rss+xml" title="Example » Comments Feed" href="http://example.com/comments/feed/" /> <link rel="alternate" type="application/rss+xml" title="Example » Home Comments Feed" href="http://example.com/home/feed/" /> <script type='text/javascript' src='http://example.com/wp-includes/js/l10n.js?ver=20101110'></script> <script type='text/javascript' src='http://example.com/wp-includes/js/comment-reply.js?ver=20090102'></script> <link rel="EditURI" type="application/rsd+xml" title="RSD" href="http://example.com/xmlrpc.php?rsd" /> <link rel="wlwmanifest" type="application/wlwmanifest+xml" href="http://example.comwp-includes/wlwmanifest.xml" /> <meta name="generator" content="WordPress 3.2.1" /> </head>
WordPress’s tagline is “Code is Poetry” but usually after taking a quick look at the output of the default theme you might think otherwise. It’s probably much worse once you’ve activated some plugins which likely enqueue CSS and scripts all over the place.
If you look at the output of a default Roots theme setup then you’re going to be looking at almost an exact match of HTML5 Boilerplate’s:
<!doctype html>
<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en"> <![endif]-->
<!--[if IE 7]> <html class="no-js ie7 oldie" lang="en"> <![endif]-->
<!--[if IE 8]> <html class="no-js ie8 oldie" lang="en"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<title>Home | Example</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/css/blueprint/screen.css">
<link rel="stylesheet" href="/css/style.css">
<!--[if lt IE 8]><link rel="stylesheet" href="/css/blueprint/ie.css"><![endif]-->
<link rel="alternate" type="application/rss+xml" title="Example Feed" href="http://example.com/feed/">
<script src="/js/libs/modernizr-2.0.6.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="/js/libs/jquery-1.6.4.min.js"><\/script>')</script>
<link rel="canonical" href="http://example.com/">
<script defer src="/js/plugins.js"></script>
<script defer src="/js/script.js"></script>
</head>
Here’s what we do to clean up wp_head:
Remove all unnecessary functions
remove_action('wp_head', 'feed_links', 2);
remove_action('wp_head', 'feed_links_extra', 3);
remove_action('wp_head', 'rsd_link');
remove_action('wp_head', 'wlwmanifest_link');
remove_action('wp_head', 'index_rel_link');
remove_action('wp_head', 'parent_post_rel_link', 10, 0);
remove_action('wp_head', 'start_post_rel_link', 10, 0);
remove_action('wp_head', 'adjacent_posts_rel_link_wp_head', 10, 0);
remove_action('wp_head', 'wp_generator');
remove_action('wp_head', 'wp_shortlink_wp_head', 10, 0);
remove_action('wp_head', 'noindex', 1);
// remove WordPress version from RSS feeds
function roots_no_generator() { return ''; }
add_filter('the_generator', 'roots_no_generator');
Remove unnecessary scripts
if (!is_admin()) {
wp_deregister_script('l10n');
}
Remove unnecessary CSS
// remove CSS from recent comments widget
function roots_remove_recent_comments_style() {
global $wp_widget_factory;
if (isset($wp_widget_factory->widgets['WP_Widget_Recent_Comments'])) {
remove_action('wp_head', array($wp_widget_factory->widgets['WP_Widget_Recent_Comments'], 'recent_comments_style'));
}
}
add_action('wp_head', 'roots_remove_recent_comments_style', 1);
// remove CSS from gallery
function roots_gallery_style($css) {
return preg_replace("!<style type='text/css'>(.*?)</style>!s", '', $css);
}
add_filter('gallery_style', 'roots_gallery_style');
Hide /wp-content/
Rewrite static theme assets and plugins directory
// rewrite /wp-content/themes/theme-name/css/ to /css/
// rewrite /wp-content/themes/theme-name/js/ to /js/
// rewrite /wp-content/themes/theme-name/img/ to /img/
// rewrite /wp-content/plugins/ to /plugins/
function roots_flush_rewrites() {
global $wp_rewrite;
$wp_rewrite->flush_rules();
}
function roots_add_rewrites($content) {
$theme_name = next(explode('/themes/', get_stylesheet_directory()));
global $wp_rewrite;
$roots_new_non_wp_rules = array(
'css/(.*)' => 'wp-content/themes/'. $theme_name . '/css/$1',
'js/(.*)' => 'wp-content/themes/'. $theme_name . '/js/$1',
'img/(.*)' => 'wp-content/themes/'. $theme_name . '/img/$1',
'plugins/(.*)' => 'wp-content/plugins/$1'
);
$wp_rewrite->non_wp_rules += $roots_new_non_wp_rules;
}
add_action('admin_init', 'roots_flush_rewrites');
function roots_clean_assets($content) {
$theme_name = next(explode('/themes/', $content));
$current_path = '/wp-content/themes/' . $theme_name;
$new_path = '';
$content = str_replace($current_path, $new_path, $content);
return $content;
}
function roots_clean_plugins($content) {
$current_path = '/wp-content/plugins';
$new_path = '/plugins';
$content = str_replace($current_path, $new_path, $content);
return $content;
}
add_action('generate_rewrite_rules', 'roots_add_rewrites');
if (!is_admin()) {
add_filter('plugins_url', 'roots_clean_plugins');
add_filter('bloginfo', 'roots_clean_assets');
add_filter('stylesheet_directory_uri', 'roots_clean_assets');
add_filter('template_directory_uri', 'roots_clean_assets');
add_filter('script_loader_src', 'roots_clean_plugins');
add_filter('style_loader_src', 'roots_clean_plugins');
}
Change location of WordPress uploads
Default WordPress settings will have all of your uploads going into /wp-content/uploads/ and also organized by date which can be changed in the admin under Settings > Media.
In the Roots theme we’re using a directory in the root of your WordPress install called /assets/ without the date organization by default.
This is currently done automatically during part of the activation with the following code:
update_option('uploads_use_yearmonth_folders', 0);
update_option('upload_path', 'assets');
Cleaning up the output of navigation menus
Here’s the clean walker from Roots:
class roots_nav_walker extends Walker_Nav_Menu {
function start_el(&$output, $item, $depth, $args) {
global $wp_query;
$indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
$slug = sanitize_title($item->title);
$class_names = $value = '';
$classes = empty( $item->classes ) ? array() : (array) $item->classes;
$classes = array_filter($classes, 'roots_check_current');
$class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item ) );
$class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
$id = apply_filters( 'nav_menu_item_id', 'menu-' . $slug, $item, $args );
$id = strlen( $id ) ? ' id="' . esc_attr( $id ) . '"' : '';
$output .= $indent . '
To use the custom walker you’ll need to modify the call to your menu in your theme files:
<?php wp_nav_menu(array('theme_location' => 'primary_navigation', 'walker' => new roots_nav_walker())); ?>
Now the output of your navigation menus will look like:
<nav id="nav-main" role="navigation">
<ul id="menu-primary-navigation" class="menu">
<li id="menu-home" class="current-menu-item"><a href="/">Home</a></li>
<li id="menu-sample-page"><a href="/sample-page/">Sample Page</a></li>
</ul>
</nav>
Root relative URLs in WordPress
After implementing the code below into your theme you’ll notice that instead of seeing http://example.com/ all over the place you’ll see mostly just /.
function roots_root_relative_url($input) {
$output = preg_replace_callback(
'!(https?://[^/|"]+)([^"]+)?!',
create_function(
'$matches',
// if full URL is site_url, return a slash for relative root
'if (isset($matches[0]) && $matches[0] === site_url()) { return "/";' .
// if domain is equal to site_url, then make URL relative
'} elseif (isset($matches[0]) && strpos($matches[0], site_url()) !== false) { return $matches[2];' .
// if domain is not equal to site_url, do not make external link relative
'} else { return $matches[0]; };'
),
$input
);
return $output;
}
// workaround to remove the duplicate subfolder in the src of JS/CSS tags
// example: /subfolder/subfolder/css/style.css
function roots_fix_duplicate_subfolder_urls($input) {
$output = roots_root_relative_url($input);
preg_match_all('!([^/]+)/([^/]+)!', $output, $matches);
if (isset($matches[1]) && isset($matches[2])) {
if ($matches[1][0] === $matches[2][0]) {
$output = substr($output, strlen($matches[1][0]) + 1);
}
}
return $output;
}
if (!is_admin() && !in_array($GLOBALS['pagenow'], array('wp-login.php', 'wp-register.php'))) {
add_filter('bloginfo_url', 'roots_root_relative_url');
add_filter('theme_root_uri', 'roots_root_relative_url');
add_filter('stylesheet_directory_uri', 'roots_root_relative_url');
add_filter('template_directory_uri', 'roots_root_relative_url');
add_filter('script_loader_src', 'roots_fix_duplicate_subfolder_urls');
add_filter('style_loader_src', 'roots_fix_duplicate_subfolder_urls');
add_filter('plugins_url', 'roots_root_relative_url');
add_filter('the_permalink', 'roots_root_relative_url');
add_filter('wp_list_pages', 'roots_root_relative_url');
add_filter('wp_list_categories', 'roots_root_relative_url');
add_filter('wp_nav_menu', 'roots_root_relative_url');
add_filter('the_content_more_link', 'roots_root_relative_url');
add_filter('the_tags', 'roots_root_relative_url');
add_filter('get_pagenum_link', 'roots_root_relative_url');
add_filter('get_comment_link', 'roots_root_relative_url');
add_filter('month_link', 'roots_root_relative_url');
add_filter('day_link', 'roots_root_relative_url');
add_filter('year_link', 'roots_root_relative_url');
add_filter('tag_link', 'roots_root_relative_url');
add_filter('the_author_posts_link', 'roots_root_relative_url');
}
// remove root relative URLs on any attachments in the feed
function roots_root_relative_attachment_urls() {
$roots_options = roots_get_theme_options();
if (!is_feed() && $roots_options['root_relative_urls']) {
add_filter('wp_get_attachment_url', 'roots_root_relative_url');
add_filter('wp_get_attachment_link', 'roots_root_relative_url');
}
}
add_action('pre_get_posts', 'roots_root_relative_attachment_urls');
Thanks
Huge thanks to Scott Walkinshaw for many of the code examples above and to all of the other Roots contributors.