I use Nanoc, a Ruby static website generator. It is pretty nice and has all the features I need for generating this site. One feature that was missing, however, was the ability to capture content on one page and use it on others. I manage the archive links as a page and the page content is inserted in the left nav bar using the site's layout template. I also reuse my resume content on the printable version of the page.

After inquiring on the Nanoc mail list, I came up with the following hack to solve my problem. Just add the code at the bottom of the page to a .rb file in your lib directory and then you can use it as follows.

<% global_content_for :foo do %>
This is the FOO stuff.
<% end %>

In any page you want to see the :foo content, just use markup like the following.

<%= global_content_for :foo %>

I wrote in a rudimentary ability to detect that one or more pages has unsatisfied global references. This situation arises pretty much whenever you use nanoc to do an update compile. In this case, nanoc will raise an exception with a message that there are unsatisfied global references. The "workaround" is to ensure that you are recompiling not only the pages that print out the global capture, but also the pages where the global capture is made. Because I have so few pages, it is not a problem for me to just recompile the whole site.

The real solution to this problem is smarter dependency resolution. It should be possible in a page's metadata to mark a page as dependent on some other page. If a page were being recompiled, all it's dependencies would be recompiled as well. If Nanoc can't handle it, maybe one of the other static website generators such as Webby will be able to handle it.

The Code

Add this to a file in your site's lib directory, say lib/global_capturing.rb:

# override some stuff

module Nanoc
  #Raised in the event of an unsatisified global capture dependency
  class UnsatisfiedGlobalCaptureReference < StandardError
  end
  class Compiler
    attr_accessor :recompiles
    alias :old_run :run
    def run(objects = nil, params = {})
      # Track recursion depth.  We allow a depth of no more
      # than 2. If greater, we have unsatisfied global capture
      # references
      (@depth ||= 0)
      @depth += 1
      if @depth > 2
        raise UnsatisfiedGlobalCaptureReference.new("The following pages have unsatisfied global capture references: #{@recompiles.join(', ')}") 
      end
     
      @recompiles = []
      old_run(objects, params)

      # Recompile stuff that had a global capture miss
      recompile_pages = @recompiles.map do |path|
        @site.pages.find do |site_page|
          check_path = path.gsub('.html', '/')
          site_page.path == check_path
        end
      end
      if recompile_pages.size > 0
        run(recompile_pages, :also_layout => params[:also_layout], :even_when_not_outdated => true, :from_scratch => true)
      end
    end  
  end
end

module GlobalCapturing
  CAPTURES = {}
    def global_content_for(name, &block)
      if !block.nil?
        global_captures[name] = global_capture(&block)
      end
      if !global_captures.has_key?(name)
        # TODO: Is this possible
        # Need to somehow signal the compiler that this page should be skipped for now
        # HACK: For some reason '==' does not work on @page objects, so use the path as the
        # equality check
        @site.compiler.recompiles << @page.path
      end
      global_captures[name]
    end

    def global_captures
      CAPTURES
    end

  private

    def global_capture(*args, &block)
      buffer = eval('_erbout', block.binding)

      pos = buffer.length
      block.call(*args)

      data = buffer[pos..-1]

      buffer[pos..-1] = ''

      data
    end
end

include GlobalCapturing