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.
OMG! It totally worked and it was so awesome! Can’t wait to see it as a kick-ass plugin.
One thing, in Roots I saw that it automatically included Boilerplate’s htaccess file. Isn’t that necessary for URL rewriting?
The H5BP htaccess isn’t necessary at all for the rewrites, WordPress already throws in the necessary lines to account for rewriting (RewriteEngine On)
Will most of these functions work if I apply them to my current non-roots theme?
Also, I seem to remember trying roots awhile back, but if memory serves me correct, I was not able to apply some of these tricks because I use subdomain multisites. Is this still the case?
You can use all of these outside of Roots, but currently the URL rewrites are still unsupported for child themes and MU setups
Thanks so much for your code, i’ve modify your code to use it on a buddypress theme, i use Buddypress v.1.5.1
I use the default theme with a child theme (in my usual ‘wp-content/themes/’ folder),
Here is the code (functions.php code and .htaccess file code:
http://chopapp.com/#4es8td00
i hope it will be usefull ;)
Thanks Ben! It’s great!
I was wondering if your method would still work if I update wordpress versions in the future?
Would this work with most of other wordpress plugins?
Yes, and it should
This is awesome! I was wondering if there’s any development in making the URL rewrites work for themes outside of Roots?
There’s been several people who’ve asked about it but it’s not something that has had any work put into it yet
Can’t wait to try! Thank you Ben!