Fork me on GitHub

Meet Ace a New Static Site Generator

This post has been updated at 10/07/2011.

Ace is a powerful tool designed to easily generate even complex web sites. I used to use Nanoc, but it doesn't work well for the complex stuff. Neither does Webby.

So why is Ace better?

Most of the static generators in Ruby are fairly opinionated: blog posts in Nanoc are supposed to have kind: article, Jekyll uses _posts directory for posts etc. It can be fine for most of the use cases, but sometimes it complicates things. Ace doesn't make any assumptions really, you can do anything you want any way you want.

I'm also trying to keep the core as small as possible and put additional functionality to filters and modules. Remember why people have been so amazed by Merb and Rails 3? That's because these tools are easy to hack, and so is Ace!

#1 Plain old Ruby instead of DSL

Take a look at these two examples. First is in Nanoc, the second one is in Ace:

compile "*" do
  filter :haml
end

route "*" do
  item.identifier + "index.html"
end

layout "*", :haml, format: :html5
class Post < Ace::Item
  before Ace::TemplateFilter, layout: "post.html"

  def output_path
    "/posts/#{self.slug}"
  end
end

I have chosen plain old Ruby over DSL, so you can use any Ruby features you want like inheritance without even thinking how to do so! In Ace you actually work with instances more than with some pages, because Ace is an OOP framework.

#2 Logic goes out of the templates

- tag_names = articles.map { |article| article.attributes[:tags] }
- tag_names.flatten.uniq.each do |tag_name|
  %h2= tag_name
  - items_with_tag(tag_name).uniq.each do |post|
    %h3= link_to post.attributes[:title], post
    %p= post.attributes[:excerpt]
class Tag < Ace::Item
  before Ace::TemplateFilter, layout: "tag.html"

  def self.posts_for(tag)
    Post.instances.select do |post|
      post.metadata[:tags].include?(tag)
    end
  end

  def self.tag_list
    Post.instances.reduce(Array.new) do |buffer, post|
      post.metadata[:tags].each do |tag|
        buffer << tag unless buffer.include?(tag)
      end
      buffer
    end
  end

  def self.tags
    self.tag_list.inject(Array.new) do |buffer, tag_name|
      buffer << Tag.new(tag_name, self.posts_for(tag_name))
    end
  end

  attr_reader :name, :posts
  def initialize(name, posts)
    @name, @posts = name, posts
  end
end

Again, the first example is in Nanoc, the second one in Ace. The Nanoc example is shorter, it's because Nanoc have some helpers for tags, but it also means that it's not very flexible. Both do the same, but only the second one provides some reasonable API which you can reuse in other parts of the application or you can test it. Without any rubbish in template.

You can argument that you can create a class and then create helper using this class ... and you're right. But in fact you'll probably end up with something like in the example above (it's from a previous version of this blog).

#3 On the fly generation

In Ace, this is dead simple, just create an instance of Ace::Item or its subclass and use its #register method. Class which registers items is called generator and it should have .generate method. Generator is usually subclass of Ace::Item and the class work as a generator, whereas the instance represents a single page.

class Tag < Ace::Item
  # Usage: generator Tag
  def self.generate
    self.tags.each(&:register)
  end
end

I didn't make any example for Nanoc, basically you'd use Nanoc3::Item.new(content, metadata) and you'd add such item into @items collection. But wait, what's the @items? Of course, some variable which can be accessed only from inside, so you either have to write the code directly to the templates or define a helper.

#4 Powerful filters

Thanks to hotovson, Ace has great Sass filter for generating CSS through Sass and Pygments filters for syntax highlighting. And writing custom filters is easy! Take a look for example at the Pygments filter:

# encoding: utf-8

require "ace/filters"
require "nokogiri"
require "albino"

module Ace
  class PygmentsFilter < Filter
    def call(item, content)
      doc = Nokogiri::HTML(content)
      doc.css("pre[lang]").each do |pre|
        unless pre["lang"].nil? || pre["lang"].empty?
          pre.replace Albino.colorize(pre.content, pre["lang"])
        end
      end

      doc.to_s
    end
  end
end

#5 It has template inheritance

I love template inheritance, you know it. I made Rango, the first Ruby framework with template inheritance, now I created Ace, the first static sites generator with template inheritance. It can really simplify your templates, although you'll truly appreciate this feature only with more complex sites (not like this blog).

How can I start with Ace?

It's simple, just take a look at blog.101ideas.cz sources on GitHub. Download it and run bundle install as usual. Any contributions are welcomed!

blog comments powered by Disqus
About

RSS

All Posts IT Ace web static generator template inheritance Haml

Tags

GitHub projects

Twitter @botanicus

Recent Comments