Middleman 3.0 extensions and execution order

Adam Luikart
Adam Luikart Developer September 11, 2012

If you’re building a Middleman site using custom extensions, you’ll eventually need a firm understanding of the Middleman lifecycle. But because many of Middleman’s core features are written as extensions themselves, it can be difficult to tell at a glance what happens when.

So, in general, here’s the order in which things happen when Middleman starts up:

  1. Data is read from any YAML files in /data and made available.

  2. Your config.rb is executed and extensions are activated in the order they appear.

  3. When an extension is activated, its registered method is executed immediately. This gives the extension a chance to register code to be run during Middleman’s lifecycle hooks, like after_configuration, before, and ready.

  4. If you want to manipulate or extract data from the sitemap, register a resource list manipulator in after_configuration. If you try to access the sitemap at that point, though, you’ll notice it’s empty! The sitemap doesn’t get populated – at all – until after after_configuration handlers have finished executing.

  5. Once your after_configuration handlers execute, Middleman’s ready callbacks are executed. The first of these populate the sitemap by executing all the registered resource list manipulators. Some of these are supplied and run automatically by Middleman, like the one that reads files from disk, building the initial sitemap. The manipulators you registered will be run in the order you activated your extensions in config.rb.

    Keep in mind that your manipulate_resource_list methods may be invoked more than once while the app is starting up, so make sure they don’t have any side effects you wouldn’t want to run multiple times (like warning on validation errors).

  6. After Middleman’s own ready handlers execute, the ones you’ve registered are invoked.

An example

Our own web site is built using Middleman and a few custom extensions.

Projects

The first extension, Projects, finds our project pages from the sitemap during a manipulate_resource_list method, and mixes in behaviors so we can easily do things like access a project’s “directory” image, or determine if that project has been archived and should be skipped. This extension also adds some helpers to make it easy to grab particular subsets of projects. We use this to determine which “hero” projects should appear on the home page, or which projects should show up in the main project directory.

SocialFeed

The second extension, SocialFeed, pulls data from an external archive of all our tweets & tumblr posts so we can display them in an aggregate Pinterest-style feed. The data is initialized in the extension’s after_configuration helper.

There’s a second part to this extension, MonthPages, that needs to insert one page into the sitemap for each month of social content, so that we can page backwards in time through the archive.

We also wanted to insert our projects into the feed, but at first it wasn’t clear where to do this. I thought I had two options:

The solution?

Load data early, and add another sitemap manipulator to grab Projects.

If the projects aren’t initialized until their resource_list_manipulator runs, but we need them to be ready in the social feed when the MonthPages manipulator runs, then the only option is to register a manipulator whose only job is to populate the SocialFeed data store with projects.

Here’s SocialFeed#registered:

def registered(app, options={}, &block)
  require 'social'
  require 'social-feed/item'

  options = Options.new(options)
  yield options if block_given?

  options.month_template ||= "social-month.html"

  app.helpers Helpers

  app.after_configuration do
    puts "== Loading social feed..."
    social_feed(options) # Helper that inits and memoizes an instance of
                         # SocialFeed::Store

    sitemap.register_resource_list_manipulator(
      :social_feed,
      social_feed, # Returns our cached instance of SocialFeed::Store
      false
    )

    ignore options.month_template
    sitemap.register_resource_list_manipulator(
      :social_month_pages,
      MonthPages.new(self),
      false
    )
  end
end

And here’s SocialFeed::Store#manipulate_resource_list:

def manipulate_resource_list(resources)
  @feeds[:projects] = @app.projects.all
  resources
end

Because manipulators are run in order, we’re now guaranteed that everything happens exactly when it should.