WP Help 1.0

My WP Help plugin just got a huge update. Version 1.0 is really worth checking out. I’m quite proud of it.

WP Help

WP Help is a plugin for creating documentation for display within the WordPress admin. Many WordPress installs are customized, and it’s really helpful to have a centralized resource for documenting those features. You can create documents about creating content, editing content, moderating comments, or whatever you want! If you have clients who can’t seem to remember how to do X, you should install WP Help and document it for them. WP Help is powered by WordPress Custom Post Types, so you create content using the full WordPress editor.

Document Syncing

Oh yeah. This is the feature you’ve all been waiting for. If you have a standard set of help documents you want to use on multiple sites, this lets you do that. Create the documents, grab the (secret) sync URL for that site, and then plug that URL in to other sites. Those other sites will automatically pull down those documents, and keep them up-to-date (even handling new documents, deleted documents, renamed documents, and re-parented documents). Any internal links in the original document will be rewritten to be local to the destination WP Help install. So go ahead and use the WP internal linking functionality on your source site and know that those links will work on all the destination sites!

Menu Placement

The menu item for the help documents can now be placed in one of four locations:

  1. As a Dashboard submenu
  2. Top level, above the Dashboard
  3. Top level, below the Dashboard
  4. Top level, at the bottom

You get a live preview of this (yeah, I know, super fancy).

Menu Name

The menu name (and page title) can be changed. Just doubleclick the page title, edit it, and hit return. Boom. Again, this has a live preview.

Topics List Name

Likewise the topics list header can be renamed. Doubleclick, edit, return. Live preview.

Edit Links & Management Links

If you can edit help documents, you’ll get an edit link. When editing, there is a handy “Manage” link to jump you back to the documents management interface. Navigating in general has been improved quite a bit.

Dashboard Widget

There is now a simple dashboard widget, listing all of your help documents.

Better Default Access

A lot of you said that you have Contributor-level users who need documentation just like authors do. So now they can view documentation by default (there’s a hook if you want to change the capability required to view help documents).

Lots of Little Tweaks

There are numerous little tweaks to improve your experience. Check out out!

Roadmap

Things I’m considering:

  • Restricting individual documents to users with a certain level of access
  • Multiple sync sources

Any other ideas?

“The writer should be the one to click it”

The beauty of Web publishing, and the specific beauty of TechCrunch, is that the only barrier between bloggers’ overflowing brains and readers’ racing hearts is a publish button. And the writer should be the one to click it.

[…]

Over the course of our conversation, Tsotsis told me she viewed the act of writing the post as a sort of meta-comment on The New York Times’ antiquated editorial structure: “Look what I can do with my dumb little WordPress blog. You guys can’t do this. Watch,” she remembered thinking.

Andrew Beaujon @ Poynter

Whether you love or hate Alexia Tsotsis’ writing, one thing is certainly true: she’s no one’s monkey. And while a debate about the wisdom of making “Fuckers” the first word of a blog post would undoubtedly be entertaining, that’s not what I want to talk about. Tsotsis and Beaujon make two very interesting points that directly relate to WordPress and its effect on publishing.

WordPress is absolutely a tool that can (and given the state of the tools they were previously using, should) be used by Ye Olde Media Guarde. We’ve even made some steps toward supporting a basic editorial workflow with the Contributor role (which can submit posts for review, but not publish them). But using WordPress in that way is like using a computer to print a letter and then fax it to your recipient. It feels like an artificial restriction. We’ve empowered individuals to publish what they want to publish, when they want to publish. Reinstating the print media workflows of the last century very much feels like a step backwards. Writers got a taste of true publishing freedom. Readers got a taste of what it’s like to actually have a connection and conversation with a writer instead of just being delivered their words. We can’t pretend that didn’t happen.

One of WordPress’ user experience breaks from its competitors was its “Publish” button. Other software had a “Save” button and then a drop-down status selector. So you’d select “Published” from the status drop-down, and then hit “Save”. With WordPress, your post is always one quick click away from being shared with the entire world. It’s more raw, more immediate, and more emotionally satisfying. WordPress also instituted “Edit” links on the front end, which gave the same sense of immediacy to the updating process. As much as the old media has embraced blogging as part of their publishing strategy, they’ve not fully integrated its responsive and update-able nature. On the whole, old media “blog posts” feel like a place where statements of questionable truth go to die and never be corrected, clarified, or amended.

Web publishing is much, much more than print publishing with the web as a distribution network. As long as the old media treats the web as a pipe instead of an opportunity to modernize the way they publish, they’ll continue to be called “the old media”.

BRB. Leaving a voicemail with my editor so she can get this post published in tomorrow’s issue.

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.

Underscores

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.

Bootstrap

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.

Typekit

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:

<?php

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

	// 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.

<?php
class HBNS_Always_Active_Plugins {
	static $instance;
	private $always_active_plugins;

	function __construct() {
		$this->always_active_plugins = array(
			'batcache/batcache.php',
			'posts-to-posts/posts-to-posts.php',
			'login-logo/login-logo.php',
			'manual-control/manual-control.php',
		);
		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.


<?php
/*
Plugin Name: Post Links
Version: 0.1
Author: Mark Jaquith
Author URI: http://coveredwebservices.com/
*/

// 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] );
				break;
		}
		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 Amazon.com link, the approximate price, and then a freeform textarea for key/value pairs and miscellaneous bullet points.

Misc

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:


<?php

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:

<?php
	public function transition_post_status( $new, $old, $post ) {
		if ( 'publish' !== $old && 'publish' !== $new )
			return;
		$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.

Questions?

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 WordPress.org 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.