How WordPress Handles Dashes and Hyphens

WordPress has always valued typography. Properly “curled” quotes, fancy dashes — like this — and more.

I want to look specifically at dashes, and talk about how WordPress handles the conversion of hyphens to dashes.

First, let’s talk about the three most common forms of horizontal strokes that exist. The first is the hyphen-minus. This is the key that is probably to the right of your “0” key on your keyboard. It is often called a “minus” or a “hyphen”. It is neither of these, and yet it is both. It is a special character that is somewhere in-between a minus sign and a hyphen. As such, it can serve either purpose. I’m just going to call that a “hyphen” for the rest of this post, just to save time.

An em-dash (—) is a wide dash — the width of the letter “m” being its guiding length. Em-dashes signify a thought break, rather like parenthesis, but with a stronger implied break.

An en-dash (–) is slightly shorter — the width of the letter “n” being its guiding length. En-dash use is probably one of the least known skills in everyday punctuation. The en-dash is used for:

  • Ranges of number values: (2–4 teaspoons, from 1:00–2:30pm, ages 7–10)
  • Relationships and connections: (a JFK–Atlanta flight, Bose–Einstein condensate, the Jackson–Murray fight, the Macy–Jaquith wedding)
  • Attributive compounds: (pre–Vietnam War weapons, the ex–Vice President non–New York style pizza)

Here’s how WordPress replaces hyphens with dashes:

Foo {3 hyphens, spaced} Bar → Foo — Bar (em-dash)
Foo{3 hyphens, no space}Bar → Foo—Bar (em-dash)
Foo {2 hyphens, spaced} Bar → Foo — Bar (em-dash)

Foo{2 hyphens, no space}Bar → Foo–Bar (en-dash)
Foo {1 hyphen, spaced} Bar → Foo – Bar (en-dash)

The last replacement seems misguided, and I wish it weren’t made (or that it were an em-dash instead).

So, three hyphens always gets you an em-dash. Two hyphens gets you an em-dash if spaced apart from surrounding words. Else, you get an en-dash. If you want more control, then I suggest you do as I do, and actually start typing the correct dashes (WordPress won’t mess with them). On OS X, en-dashes are typed with Opt-{hyphen}, and em-dashes are typed with Opt-Shift-{hyphen}. In Windows, en-dashes are typed with Alt + 0150, and em-dashes are typed with Alt + 0151.

You can also type out the HTML entities in WordPress’ HTML mode: — and –.

WordPress Skeleton

At my “Scaling, Servers, and Deploys — Oh My!” talk (slides) at WordCamp San Francisco 2011, I talked a bit about my ideal WordPress repo setup. In the spirit of sharing, I’ve now made that skeleton setup into a GitHub repo.

What you get is a WordPress repo starter kit. WordPress is in a subdirectory (/wp/), content is in a custom directory (/content/), and uploads are mapped to /shared/content/uploads/, which is a Git-ignored location. Re-symlink as appropriate, or alter your deploy script to do the symlinking on the fly.

You get a nice clean wp-config.php with a few of my tips and tricks already implemented (like local-config.php support for local development). .htaccess is ready to go with WordPress rewrite rules for anyone running on Apache.

I don’t really expect people to use it exactly the way I have it set up (though feel free!). What’s more likely is that people will fork it, and then make it their own. For instance, you may want to add mu-plugins drop-ins that you frequently use. Have fun!

How I built “Have Baby. Need Stuff!”

Have Baby. Need Stuff! is a baby gear site that my wife and I just launched. I thought I’d share how I built it.

WordPress Core

WordPress is a Git submodule, with the content directory moved to the /content/ directory. This makes my Git repo smaller, as WordPress isn’t actually in it.


For a theme base, I started with the Underscores starter theme by the theme team at Automattic. Underscores is not a theme itself… it’s a starting point for building your own theme.


Next, I integrated Bootstrap, by Twitter, to handle the CSS base and the grid system. Bootstrap is a really powerful framework, and version 2.0 has great responsive design support, which allowed me to create a single design that scales up to big screens or down to tablet or phone screen sizes. Try resizing it in your browser to see the responsiveness in action!

The CSS for the site is authored using LESS, which plays well with Bootstrap. I’m compiling/minifying/concatenating the CSS and JS using CodeKit, an amazing Mac OS X app that makes development a breeze.


For web fonts, it’s hard to beat Typekit.

Subtle Patterns

I needed some patterns to use on the site, but I was frustrated with the licensing terms on many pattern sites I was finding. And then I found Subtle Patterns. Gorgeous, subtle patterns, liberally licensed. And hey, their site is WordPress powered too!

Posts 2 Posts

The site has the concepts of Departments, Needs, and Products. Each Department has multiple Needs. Each Need has multiple Products. I used Scribu’s phenomenal Posts 2 Posts plugin to handle these relationships.

Here’s the basic Posts 2 Posts connection code:


function hbns_register_p2p_relationships() {
	if ( !function_exists( 'p2p_register_connection_type' ) )

	// Connect Departments to Needs
	p2p_register_connection_type( array(
		'name' => 'departments_to_needs',
		'from' => 'department',
		'to' => 'need',
		'sortable' => 'to',
		'admin_box' => 'to',
		'admin_column' => 'any',
		'cardinality' => 'one-to-many',
	) );

	// Connect Needs to Products
	p2p_register_connection_type( array(
		'name' => 'needs_to_products',
		'from' => 'need',
		'to' => 'product',
		'sortable' => 'from',
		'admin_column' => 'any',
		'admin_box' => array(
			'show' => 'any',
			'context' => 'advanced',
		'cardinality' => 'many-to-many',
		'fields' => array(
			'description' => 'Description',
	) );

add_action( 'wp_loaded', 'hbns_register_p2p_relationships' );

I created a Custom Post Type for each of Departments, Needs, and Products, and connected them all using Posts 2 Posts. The connection between a Need and a Product also contains description metadata, as seen here:

Since Posts 2 Posts was a required plugin for the site to function, I didn’t want there to be any possibility of accidental deactivation. So I wrote a quick mu-plugins drop-in to “lock” certain plugins on.

class HBNS_Always_Active_Plugins {
	static $instance;
	private $always_active_plugins;

	function __construct() {
		$this->always_active_plugins = array(
		foreach ( $this->always_active_plugins as $p ) {
			add_filter( 'plugin_action_links_' . plugin_basename( $p ), array( $this, 'remove_deactivation_link' ) );
		add_filter( 'option_active_plugins', array( $this, 'active_plugins' ) );

	function remove_deactivation_link( $actions ) {
		unset( $actions['deactivate'] );
		return $actions;

	function active_plugins( $plugins ) {
		foreach ( $this->always_active_plugins as $p ) {
			if ( !array_search( $p, $plugins ) )
				$plugins[] = $p;
		return $plugins;

new HBNS_Always_Active_Plugins;

Custom Post Types

I’m using the Products post type in a slightly odd way. You don’t ever go to a product URL. You instead go the URL for the Need that the Product fulfills, and that page lists all of the connected Products. As such, I wanted to make it so that URLs for products pointed to their Need, and I wanted to add an admin bar Edit link for the primary product on its Need page.

Plugin Name: Post Links
Version: 0.1
Author: Mark Jaquith
Author URI:

// Convenience methods
if(!class_exists('CWS_Plugin_v2')){class CWS_Plugin_v2{function hook($h){$p=10;$m=$this->sanitize_method($h);$b=func_get_args();unset($b[0]);foreach((array)$b as $a){if(is_int($a))$p=$a;else $m=$a;}return add_action($h,array($this,$m),$p,999);}private function sanitize_method($m){return str_replace(array('.','-'),array('_DOT_','_DASH_'),$m);}}}

// The plugin
class CWS_HBNS_Post_Links_Plugin extends CWS_Plugin_v2 {
	public static $instance;

	public function __construct() {
		self::$instance = $this;
		$this->hook( 'plugins_loaded' );

	public function plugins_loaded() {
		$this->hook( 'post_type_link' );
		$this->hook( 'add_admin_bar_menus' );

	public function add_admin_bar_menus() {
		$this->hook( 'admin_bar_menu', 81 );

	public function admin_bar_menu( $bar ) {
		if ( is_single() && 'need' == get_queried_object()->post_type ) {
			$primary_product = new WP_Query( array(
				'connected_type' => 'needs_to_products',
				'connected_items' => get_queried_object(),
			) );
			if ( $primary_product->have_posts() ) {
				$bar->add_menu( array(
					'id' => 'edit-primary-product',
					'title' => 'Edit Primary Product',
					'href' => get_edit_post_link( $primary_product->posts[0] ),
				) );

	public function post_type_link( $link, $post ) {
		switch ( $post->post_type ) {
			case 'product' :
				$need = new WP_Query( array(
					'connected_type' => 'needs_to_products',
					'connected_items' => $post,
				) );
				if ( $need->have_posts() )
					return get_permalink( $need->posts[0] );
		return $link;

new CWS_HBNS_Post_Links_Plugin;

For entering data about Products, I made a custom Meta Box that provided a simple interface for entering the link, the approximate price, and then a freeform textarea for key/value pairs and miscellaneous bullet points.


Because I’m using a Git-backed and Capistrano-deployed repo, I don’t want any local file editing. So I dropped this code in:


define( 'DISALLOW_FILE_EDIT', true );

function hbns_disable_plugin_deletion( $actions ) {
	unset( $actions['delete'] );
	return $actions;

add_action( 'plugin_action_links', 'hbns_disable_plugin_deletion' );

I was playing a lot with different Product thumbnail sizes, so Viper007Bond’s Regenerate Thumbnails plugin was invaluable, for going back and reprocessing the images I’d uploaded.

And of course, no WordPress developer should make a site without Debug Bar and Debug Bar Console.

Nginx and PHP-FPM

My server runs Nginx and PHP-FPM, in lieu of Apache and mod_php. My normal setup is to use Batcache with an APC backend to do HTML output caching, but I also have an Nginx “microcache” that caches anonymous page views for a short amount of time (5 seconds). But with this site, I wanted to cache more aggressively. Because there are no comments, the site’s content remains static unless we change it. So I cranked my microcache up to 10 minutes (I guess it’s not a microcache anymore!). But I wanted a way to purge the cache if a Product or Post was updated, without having to wait up to 10 minutes. So I modified the Nginx config to recognize a special header that would force a dynamic page load, effectively updating the cache.

Here’s the relevant part of the Nginx config:

	location ~ \.php$ {
		# Set some proxy cache stuff
		fastcgi_cache microcache_fpm;
		fastcgi_cache_key $scheme$host$request_method$request_uri;
		fastcgi_cache_valid 200 304 10m;
		fastcgi_cache_use_stale updating;
		fastcgi_max_temp_file_size 1M;

		set $no_cache_set  0;
		set $no_cache_get  0;

		if ( $http_cookie ~* "comment_author_|wordpress_(?!test_cookie)|wp-postpass_" ) {
			set $no_cache_set 1;
			set $no_cache_get 1;

		# If a request comes in with a X-Nginx-Cache-Purge: 1 header, do not grab from cache
		# But note that we will still store to cache
		# We use this to proactively update items in the cache!
		if ( $http_x_nginx_cache_purge ) {
			set $no_cache_get 1;

		# For cached requests, tell client to hang on to them for 5 minutes
		if ( $no_cache_set = 0 ) {
		        expires 5m;

		# fastcgi_no_cache means "Do not store this proxy response in the cache"
		fastcgi_no_cache $no_cache_set;
		# fastcgi_cache_bypass means "Do not look in the cache for this request"
		fastcgi_cache_bypass $no_cache_get;

		include        /etc/nginx/fastcgi_params;
		fastcgi_index  index.php;
		try_files      $uri =404;

		fastcgi_pass phpfpm;

Now I just needed to have WordPress ping those URLs with that header to refresh them when something changed. Here’s the “meat” of that code:

	public function transition_post_status( $new, $old, $post ) {
		if ( 'publish' !== $old && 'publish' !== $new )
		$post = get_post( $post );
		$url = get_permalink( $post );

		// Purge this URL
		$this->purge( $url );

		// Purge the front page
		$this->purge( home_url( '/' ) );

		// If a Product changes, flush its Need and that Need's Department
		if ( 'product' === $post->post_type ) {
			// Flush the connected need
			$need = new WP_Query( array(
				'connected_type' => 'needs_to_products',
				'connected_items' => $post,
			) );
			if ( $need->have_posts() ) {
				$this->purge( get_permalink( $need->posts[0] ) );
				// Now this need's connected Department
				$department = new WP_Query( array(
					'connected_type' => 'departments_to_needs',
					'connected_items' => $need->posts[0],
				) );
				if ( $department->have_posts() )
					$this->purge( get_permalink( $department->posts[0] ) );
		// If a Post changes, flush the main Blog page
		} elseif ( 'post' === $post->post_type ) {
			$this->purge( home_url( '/blog/' ) );

	private function purge( $url ) {
		wp_remote_get( $url, array( 'timeout' => 0.01, 'blocking' => false, 'headers' => array( 'X-Nginx-Cache-Purge' => '1' ) ) );

Boom. Now I get the benefit of long cache times, but with the ability to have updates go live quickly when I need to. The upshot here is that while I have Batcache installed, it’s not really going to get a lot of use, as the outer Nginx caching layer should handle everything. This doesn’t just mean that the site scales (Apache Bench has it handling many thousands of requests a second with ease), but that the site is really, really fast to browse. Your experience will vary according to network and geography, of course. But for me, I’m getting 34ms HTML delivery times for pages in the Nginx cache.


So that’s how I did it. Let me know if you have any questions!

Act now to stop Internet censorship legislation in the United States

Right now, the United States Congress is holding hearings on legislation that will have disastrous effects on free speech and the Internet. This video gives a good overview:

One of the reasons that I help make WordPress is because of my deeply held conviction that free speech is the most powerful and beneficial tool humans have ever had at their disposal. The Internet is a grand experiment that not only makes communications fast — it embraces freedom by design. This legislation is a low level attack against the system of trust upon which the Internet is based. It’s an attack on freedom of speech and on economic freedom. It’s corporate cynicism of the worst kind.

Act now, and be sure that your legislators know you oppose this legislation.

Translating WordPress Plugins and Themes: Don’t Get Clever

When you use the WordPress translation functions to make your plugin or theme translatable, you pass in a text domain as a second parameter, like so:

<?php _e( 'Some Text String', 'my-plugin-name' ); ?>

This text domain is just a unique string (usually your plugin’s repository slug). Well, many plugin developers see code like this:

<?php _e( 'Another Text String', 'my-plugin-name' ); ?>
<?php _e( 'Yet Another Text String', 'my-plugin-name' ); ?>
<?php _e( 'Gosh, So Many Text Strings!', 'my-plugin-name' ); ?>

And they think to themselves “hm, I sure am typing the 'my-plugin-name' string a lot. I’ll apply the DRY (Don’t Repeat Yourself) principle and throw that string into a variable or a constant!”

Stop! You’re being too clever! That won’t work!*

See, PHP isn’t the only thing that needs to parse out your translatable strings. GNU gettext also needs to parse out the strings in order to provide your blank translation file to your translators. GNU gettext is not a PHP parser. It can’t read variables or constants. It only reads strings. So your text domain strings needs to stay hardcoded as actual quoted strings. 'my-plugin-name'.

Happy coding!

* Well, it won’t break your plugin, but it could make it harder to be used with automated translation tools. And trust me, you don’t want to be manually managing your translation files… we have a better solution coming.

That’s a lot of shortcodes

I did a scan of the WordPress plugin directory today, and found the following shortcodes in use. The list is non-exhaustive, as some shortcodes are specified with variables. This is just the list of basic quoted string shortcodes that I found. There was some discussion about programmatically extracting these from plugins and publishing a directory, so prospective plugin authors know whether the shortcode they want to use might conflict with another plugin. Or, even better, they can offer compatibility between similar plugins.

Continue reading

Speaking at WordCamp SF 2011

My sessions for WordCamp San Francisco 2011 have been confirmed. This post is a quick teaser. Please let me know if you have any questions on what will be covered, or if you have any suggestions:

Scaling, Servers, and Deploys — Oh My!

Alternate title: First Thing, Second Thing, and Oxford Commas — Em Dash! :-)

If you manage professional WordPress-powered sites, this is going to be an invaluable presentation for you. If you’re a cowboy coder, this presentation is a mandatory WordPress-ordered intervention. (And if you don’t know what cowboy coding is, you probably do it, and don’t even know you need help.)

I’ve worked with a lot of WordPress installations for professional sites in my client work. They inevitably fall short in some way. What is usually the case is that the people in charge of running the installation feel that instead of them having control of the environment, they are merely present in the environment. Deployments that were meant to be scalable are often somewhat less so, in practice. Tell me if this conversation sounds familiar:

“What happens if we outgrow one server?”
“We’ll just add another one!”

That’s easy to say as a hypothetical. But what if you actually had to do it? What if a couple hundred thousand unexpected page views started coming your way and your boss/client tells you “Go ahead and add that second server we’ve been talking about… how many minutes is that going to take?” Did you think “no sweat,” or did your heart start racing just imagining the scenario?

I’m going to disclose the secret sauce. I’ll let you in on everything I’ve learned in the last seven years about how to code and architect WordPress-powered sites, how to scale up a single server, how to manage multiple servers, and how to deploy code in a way that is both responsive and prudent.

Topics will include:

  • Apache
  • nginx
  • Memcached
  • MySQL
  • APC
  • NFS
  • rsync
  • Git
  • Puppet
  • Capistrano

And more. At the end you’ll feel confident that you can run professional, fast, scalable WordPress installations that will make your job easier and your clients or boss happy.

Security Showdown

Instead of my battle-worn and (even to me) rather droll talk on WordPress security, I’m going to be doing an interactive panel with Brad Williams and Jon Cave. We’ll do live security reviews of some plugins that have been submitted ahead of time. This way, you can see WordPress security practices in action! There will be prizes and everything. Should be a bunch of fun.