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:
Data is read from any YAML files in
/dataand made available.Your
config.rbis executed and extensions are activated in the order they appear.When an extension is activated, its
registeredmethod is executed immediately. This gives the extension a chance to register code to be run during Middleman’s lifecycle hooks, likeafter_configuration,before, andready.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 afterafter_configurationhandlers have finished executing.Once your
after_configurationhandlers execute, Middleman’sreadycallbacks 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 inconfig.rb.Keep in mind that your
manipulate_resource_listmethods 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).After Middleman’s own
readyhandlers 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:
Load social data during
after_configuration.This didn’t work, because the
Projectssitemap manipulator hadn’t run yet, so project data wasn’t ready.Defer loading social data until
readyevent, when all projects have been loaded.It seems like this ought to work if you tell any
resource_list_manipulators you register to immediately regenerate the sitemap, but adding pages to the sitemap duringready, asMonthPagesdoes, is dangerous.It’s dangerous because sitemap manipulators are executed in the order they’re registered, and if you register one during
ready, it’ll run after extensions likeDirectoryIndexesorasset_hashexecute. Which is not what you want.
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.