Why Jetpack isn’t a Collection of Plugins, Part the First

In keeping with a previous post I’d made a couple months ago explaining the oft-discussed rationale of why we do things the way we do with Jetpack, I’ll be doing it again today, on a different — but related — topic.

I may as well make a series of it.

This is the first of two posts (in theory, I’ll remember to write the second) explaining why Jetpack is a big plugin with many features, rather than many individual plugins.  This post will be looking at the primary technical reason.  The abundance of other reasons will be in the subsequent post.  (So please don’t read this post and think it’s the only reason — it’s not)

tl;dr: Dependency management sucks.

Jetpack, as you may be aware, is structured as a bunch of modules.  Many — but not all — require a connection to WordPress.com to function.  This isn’t for vanity purposes, it’s because they actually leverage the WordPress.com server infrastructure to do things harder, better, faster, stronger than a $5/month shared host is capable of.  To do that, they need to be able to communicate securely with WordPress.com, and WordPress.com must be able to communicate securely back to your site.

Some of the modules that require a connection are things such as Publicize (which uses the WordPress.com API keys to publicize to assorted third-party systems, rather than making users register various developer accounts and get their own API keys), Related Posts (which syncs some content up to the WordPress.com servers and indexes it on a large ElasticSearch index more efficiently and accurately than could be done in a MySQL database), Monitor (which pings your site every five minutes and emails you if it’s down), Comments (which passes data back and forth behind the scenes to enable secure third-party comment authentication) — you get the idea.

We could bundle the connection library with each individual plugin.  However, we’d need to make sure it was namespaced correctly so each different plugin can use its own correctly versioned instance of the connection classes.  Which would then mean a user could have well over a dozen copies and different versions of the same connection class active at a given time.  Which will make things more difficult with respect to developing the plugins, as you can’t assume methods in one are necessarily in another.  And when you make a change in the master class, you need to scan each repository to make sure you’re not breaking anything there, and keep changes synced to well over a dozen repositories.  But I digress.

To avoid duplicate code, the modules that depend on talking back and forth with WordPress.com all use a common library that handles signing and verifying requests, API calls, and the like.

Because it’s all packaged in a single plugin, we can be sure that it’s all running the required version.  If Publicize needs a change in the core connection library, we can be sure that the version of the connection library in Jetpack has those changes.  If the core connection library needs to change structure, we can make sure that any modules that used the old methods are updated to run the new ones instead.  Everything is maintained so that it’s running smoothly and works properly with each other.

Now, if Likes, Single Sign On, After the Deadline, Post by Email and others were their own plugins, and connected to a separate Jetpack Core plugin, versioning gets tricky.  It could work, in theory, if every plugin is kept up to date, always and forever.  But the instant that the user is using, say, an outdated version of Subscriptions with an outdated Jetpack Core (which work perfectly together), and then installs the up-to-date WP.me Shortlinks plugin, things could break because WP.me Shortlinks expects a more up-to-date Jetpack Core.  So you go ahead and update Jetpack Core to current, but now Subscriptions — which used to work perfectly — now breaks because there was a method change in Jetpack Core, that is fixed in the up-to-date version of Subscriptions, but the user isn’t running the up-to-date version.  Horrible UX.

Plus, if the user doesn’t have any Jetpack stuff, the installation flow for their first Jetpack Plugin that needs the core would be something like this:

  1. Install Stats.
  2. Activate Stats.
  3. Get error saying you need Jetpack Core for Stats to function.
  4. WTF is Jetpack Core? I just want Stats!
  5. Okay, install Jetpack Core.
  6. Activate Jetpack Core.
  7. Wait, what was I doing?
  8. Stats!  Okay, right.
  9. Connect the Jetpack Core to WordPress.com.
  10. Drink Scotch in celebration.

Compare this to the status quo of:

  1. Install Jetpack.
  2. Activate Jetpack.
  3. Connect Jetpack to WordPress.com.
  4. Stats is already active (unless you’re Mark Jaquith, in which case you activate it in this step)
  5. Drink Scotch in celebration.

As I said, dependency management is hard, and there’s not really a good way to manage it in WordPress.  There have been some very worthwhile attempts made, but none that can have a sufficiently solid user experience for an average user to compare with our current system and flow.

Any questions or suggestions about dependency management and Jetpack? Ask away!

Better handling of RTL stylesheets in plugins and themes

Or, How I Learned To Stop Worrying and Let Core Do The Work For Me.


If you’ve ever done something like this, we should have a little chat:

if ( is_rtl() ) {
	wp_register_style( 'example', plugins_url( "css/example-rtl.css", __FILE__ ) );
} else {
	wp_register_style( 'example', plugins_url( "css/example.css", __FILE__ ) );
}
wp_enqueue_style( 'example' );

Now, don’t worry. I’ve done it too! It’s not that big of a deal. But there is a better, tidier way. Just compare the above code blurb to this:

wp_register_style( 'example', plugins_url( "css/example.css", __FILE__ ), array(), '1.0' );
wp_style_add_data( 'example', 'rtl', 'replace' );
wp_enqueue_style( 'example' );

which will output something like this in rtl locales:

<link rel='stylesheet' id='example-rtl-css' href='http://domain.com/path/to/css/example-rtl.css' type='text/css' media='all' />

Simpler, right? It reads more easily, and as an added bonus, if something is to toggle RTL after you’ve registered the path to the asset, it handles it gracefully! As it doesn’t determine which asset path to serve up until it’s actually outputting the tag.

Now, this is assuming that your rtl stylesheet is just a replacement for your normal stylesheet. Which most are — it could be automatically generated with some tool like CSSJanus or CSS-Flip. But if you’ve got an add-on css file, that you want to load in addition that just contains overrides for RTL languages, you can handle that just as easily!

wp_register_style( 'example', plugins_url( "css/example.css", __FILE__ ), array(), '1.0' );
wp_style_add_data( 'example', 'rtl', 'addon' );
wp_enqueue_style( 'example' );

which will output something like this in rtl locales:

<link rel='stylesheet' id='example-css' href='http://domain.com/path/to/css/example.css' type='text/css' media='all' />
<link rel='stylesheet' id='example-rtl-css' href='http://domain.com/path/to/css/example-rtl.css' type='text/css' media='all' />

For the curious as to how Core actually does it, read here:

https://github.com/WordPress/WordPress/blob/809baf442b/wp-includes/class.wp-styles.php#L88-L104

Detailed explanation (with bonus examples for handling minified versions of both regular and rtl css as well):

/**
 * If you're supplying a pre-minified version of the stylesheet, you'll
 * need this, and to add the `suffix` data, so that core knows to
 * replace `example-min.css` with `example-rtl-min.css` -- handling
 * the suffix properly.
 */
$min = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';

/**
 * The normal registration. You're familiar with this already.
 */
wp_register_style( 'example', plugins_url( "css/example{$min}.css", __FILE__ ), array(), '1.0' );

/**
 * I set the value to 'replace', so it will replace the normal css file if rtl,
 * but it could also be 'addon' for a css file that just gets enqueued as
 * well, rather than replacing the normal one.
 */
wp_style_add_data( 'example', 'rtl', 'replace' );

/**
 * Finally, if we are replacing the existing file, and there's some sort of
 * suffix like `-min` as mentioned earlier, we need to let core know about
 * it, so that it can keep that suffix after the added `-rtl` that it's adding to
 * the path.
 */
wp_style_add_data( 'example', 'suffix', $min );

/**
 * Then we just enqueue it as we would normally!  If it's going to always
 * be enqueued regardless, we could just call `wp_enqueue_style()` rather
 * than `wp_register_style()` above.
 */
wp_enqueue_style( 'example' );

On Jetpack and Auto-Activating Modules

Hopefully, this is the last time that I’ll have to answer this question.

Frankly, it’s been answered dozens of times before. Now, I’m hoping to use this as a canonical ‘Answer Link’ that I can refer people to.  I’ll keep up with comments, so if anyone would like to ask

So, why does Jetpack auto-activate features?

Well, to start off, I should probably clarify what we currently do on this. We don’t auto-activate every new module that comes in.

We never auto-activate features that affect the display or front-end of your site — or at least not unless a site administrator explicitly configures them to.

So, for example, something like Photon, which would swap all your content images to CDN-hosted versions, doesn’t auto-activate. Our comments system doesn’t auto-activate either, as that would swap out your native comment form. Our sharing buttons do, but they don’t display unless you take the time to drag down some sharing buttons to the output box under Settings > Sharing.

However, modules like Publicize, Widget Visibility, and the like — they just give you new tools that you can use, with no risk to affecting your everyday visitors. When users upgrade, we give them a notification of what just happened, and point out some new features we’ve built in that they may want to activate themselves.

One thing we’ve recently expanded on, perhaps six months ago, is a ‘plugin duplication list’, for lack of a better phrase. These aren’t plugins that have an actual code-based conflict with a module, they’re ones that may be … duplicating effort. Previously, we were just scanning for plugins that would output OG Meta Tags, and short-circuit our own provider. However, since Jetpack 2.6, which shipped in November 2013, we’re actually doing it via a filter for all modules. For example, if you’ve got Gravity Forms or Contact Form 7 installed and active, our internal Jetpack Contact Form won’t auto-activate. If you’ve got AddThis or ShareThis active, our sharing buttons module won’t even kick in.

Now, obviously, we can’t catch every single plugin that may be similar enough to one of our modules to give cause to negate auto-activation. So there’s a filter, `jetpack_get_default_modules`, that can be used in any plugin to cancel auto-activation on any module.


add_filter( 'jetpack_get_default_modules', 'my_jetpack_get_default_modules' );
function my_jetpack_get_default_modules( $modules ) {
    return array_diff( $modules, array( 'module-slug' ) );
}

But I don’t like auto-activation of new features!

Okay.

You’re totally allowed not to.

We’re going to continue using our discretion to auto-activate select modules by default, but if you’d like to turn it off permanently for yours or a client’s site, we’ve made it ridiculously easy to do.


add_filter( 'jetpack_get_default_modules', '__return_empty_array' );

That’s it.

We believe that judiciously enabling new features is a win for users, especially considering 1) how low-impact most features are when ‘active’ but not actually implemented by a site owner, 2) how awkward it is for a site owner to have to enable something twice — for example, enabling the Custom Post Formats bit, and then having to visit Settings > Writing in order to actually enable the Portfolio custom post type.

We’ve spoken to many, many users who find a default feature set convenient, and resent having to make a bunch of ‘decision points’ if they had to manually activate each and every module. Good software should run well out of the box. So we’ve set up the defaults as we have. Yes, some people disagree and are vocal about not wanting anything to auto-activate. That’s okay. We try to design for the majority, with the best user experience we can provide.

If you have clients, that you’d like to be active in the relationship with, and customize the Jetpack experience for — that’s terrific. You’re the type of people that we add bunches of filters for. We’re all about empowering you to override our decisions, we just prefer to keep the default user interface free of a thousand toggles.

Decisions, not options — no?

Decisions

Decisions are never easy.

Even if it’s an entirely trivial matter, it’s still forcing you to do something.

And goshdarnit, I’m lazy.

I also prefer front-loading effort when possible. Ounce of prevention, pound of cure, stitch in time saving nine, and all that.

And I respect other people’s time as much as I value my own. So when I build something, I try to avoid decision points whenever possible. This results in the loss of options occasionally, but I believe a smoother user flow.

Now, occasionally power-users will want to modify functionality. Adding a decision point for all users for the sake of the minority is silly, especially when power-users can leverage other methods — filters, actions, functionality plugins that extend the first plugin — to accomplish their goals.

To each according to their needs. Typical users need a simple, smooth, classy interface. Power users need to get under the hood. Why try to make something that doesn’t work well for either by trying to serve both?

The best middle ground I’ve been able to come up with is offering a secondary ‘under the hood’ plugin that exposes a lot of filters as options. Keep it canonical and clean, but present all the options.

Ideal? Not really. Workable? Probably.

Events Custom Post Type Proposal for Multisite

This is intended for the Make.WordPress.org series of blogs.  There are a number of needs, from weekly chat schedules for some Make blogs, to WordCamps for others.  Each site needs to be able to display their own events, and the main Make site would need to be able to display an aggregate of all (or some) of the sub-sites.

I see the implementation of the output (data structures will be addressed separately) being done via a shortcode, as follows:

[super-spiffy-event-calendar]

which would do stuff roughly like:

$events = Super_Spiffy_Event_Calendar::get_events();
Super_Spiffy_Event_Calendar::render_plugins( $events );

Not really tricky.  That will display any events from the multisite blog that you happen to be on.  However, for the aggregate, I see something more akin to this:

[super-spiffy-event-calendar blog_ids="2,3,4,5,6,13,14,19,22"]

which would be more akin to:

$original_blog_id = get_current_blog_id();
$events = array();
foreach ( $blog_ids as $blog_id ) {
	switch_to_blog( $blog_id );
	$events = array_merge( $events, Super_Spiffy_Event_Calendar::get_events() );
}
switch_to_blog( $original_blog_id );
Super_Spiffy_Event_Calendar::render_plugins( $events );

A couple things we’d need to add in that aren’t noted here:

  • Caching. Shove it in a transient, so we’re not doing an expensive operation with blog switching on every page load.
  • Sorting. After building the aggregate, it’s probably worth sorting the events chronologically.
  • Display. I’d like to use http://arshaw.com/fullcalendar/ or something similar to handle the output.

Translations Abstraction Proposal for WordPress

plugins.svn.wordpress.org:

  • There is a new special folder in Plugin Repositories, at /assets/i18n/
  • A /assets/trunk.pot file should probably be maintained for support of repositories where trunk is their public release version.
  • Whenever a new tag is created, for example /tag/3.2-beta1 (a script automatically|the plugin author) generates the po/mo source files, and stores it as /assets/i18n/3.2-beta1.pot or the like.
  • I’m torn as to who generates this, and I have no desire for this to generate additional changesets on our already burdened plugins.svn.wordpress.org — perhaps they could live elsewhere, and wouldn’t necessarily need to be under version control.
  • A /assets/i18n/master.pot and the like file may be maintained which is a merger of all the strings in all the versions in all the tags.
    • This is to simplify things so that if a string is dropped from one version to the next, it is still included in a master index, potentially simplifying things from a storage perspective, so that if requesting translations for a plugin, a version number does not need to be specified, and GlotPress wouldn’t need to store fifty copies of the same string for the same plugin, if there are fifty different tagged versions.

plugins.glotpress.wordpress.org

  • I know very little about the inner workings of GlotPress currently, so I’ll leave this part of the proposal as a ‘black box’ that magically works.
  • Sharing identical strings between plugins would be amazing, if possible, but with the ability to break the link if one plugin author needs it to be translated differently.  But I suppose that can be done with _x() and notes for translators.
  • API requests for translations shouldn’t by default be given up-to-the-second results.  If there’s a cached version from the last 24 hours, or it hasn’t been invalidated with any new translations yet, just serve that version up.
  • Gzip it all in transit & storage.

core

  • This would be implemented as a plugin tentatively by using the 'override_load_textdomain' filter — which would then query the API and either store the translations in a transient/option, or in a folder within /wp-content/.
  • If not using a transient, set a wp_cron task to check for updates every X days, weeks, or on upgrades / installs / manually pushing the update translations button.
  • Store a version number for the most recently received translations, and pass that back with subsequent queries, so it only receives the strings that have been updated since the last pass (huge potential savings on bandwidth and server processing time).
  • On the (client|server) side, round the version number down to a given interval (thousand, ten thousand?) so that it can be cached more easily on the server side.  A couple duplicate translations could get delivered, but that’s a small price to pay for the savings in processing time.