yet.org

nanoc

nanoc is a tool that runs on your local computer and compiles documents written in formats such as Markdown, Textile, Haml,… into a static web site consisting of simple HTML files, ready for uploading to any web server.

Installation

Start by installing the following dependencies

# apt-get install ruby-dev zlib1g-dev libglib2.0-dev

Continue on with all the required Gems.

% gem install nanoc

% gem install adsf               # A Dead Simple Fileserver
% gem install fssm               # File System State Monitor
% gem install kramdown           # Markdown parser
% gem install haml               # HTML Abstraction Markup Language
% gem install less               # Invoke the Less CSS compiler from Ruby
% gem install pygments.rb        # Exposes the pygments syntax highlighter to Ruby
% gem install coderay            # Another Syntax Highlightter
% gem install coderay_bash       # Plugin to process Bash
% gem install stringex           # useful extensions to Ruby's String class
% gem install nokogiri           # Parser used by `nanoc validate-links`
% gem install rainpress          # A CSS compressor
% gem install therubyracer       # Call javascript code and manipulate javascript objects from ruby.
% gem install rpeg-multimarkdown # MultiMarkdown processing used in a new Filter. (req. libgtk2.0-dev.)
% gem install nanoc-guard        # now replace nanoc watch command [see [docs](https://github.com/guard/guard-nanoc)]

Create a web site

create yet subdirectory with a blank web site

% nanoc create_site yet 

Layout

edit the surrounding layout displayed on every page layouts/default.html:

<!DOCTYPE HTML>  
<html lang="en">
    <head>
      <meta charset="utf-8">
      <title>My blog - <%= @item[:title] %></title>
      <link rel="stylesheet" type="text/css" 
        href="http://twitter.github.com/bootstrap/assets/css/bootstrap.css" 
        media="screen">
      <link rel="stylesheet" type="text/css" href="/style.css">
    </head>
    <body>
      <div class='navbar'>
        <div class='navbar-inner'>
          <div class='container'>
            <a class='brand' href='/'>My Blog</a>
            <ul class='nav'>
              <li class='active'><a href='/'>Home</a></li>
              <li><a href='/about'>About</a></li>
            </ul>
          </div>
        </div>
      </div>
      <section class='content'>
        <%= yield %>
      </section>
    </body>
</html>

Stylesheet

edit the corresponding stylesheet content/stylesheet.css:

.content {
  width: 800px;
  background: #f5f5f5;
  border: 1px solid #ddd;
  border-top: none;
  margin: 0 auto;
  padding: 60px 20px 0 60px;
}
.post aside {
  color: #888;
  padding-bottom: 8px;
  border-bottom: 1px solid #aaa;
}
.post article {
  margin: 10px 0 60px 0;
}

Compile web site

% nanoc compile

If you don’t want to manually compile after each modification use instead:

% nanoc watch

nanoc looks in your content directory for files and processes them based on rules that you write in Ruby.

  • compile rules = how to compile a file. Parser ?
  • route rules = output directory ? where to put the compiled file ?

View site

to access your site at http://127.0.0.1:3000 first run:

% nanoc view

Helpers

edit lib/default.rb and paste in the following:

include Nanoc::Helpers::Blogging
include Nanoc::Helpers::Tagging
include Nanoc::Helpers::Rendering
include Nanoc::Helpers::LinkTo
  • Blogging add title and created_at fields and provides some helper methods to our layouts to list posts
  • Tagging lets us add tags to content items and query them
  • Rendering allows us to nest layouts
  • LinkTo lets us construct URLs for other items

Creating a Post

% mkdir content/posts
% vi 2012-11-20-first-post.md

---
title: "Just a small test post"
created_at: 2012-11-20 17:30:00 +0000
kind: article
---

This is the first Yet post. There is nothing more right now.

content between --- are metadata that will becomes available within our rules and layouts kind: article required by Blogging Helper to determine which content items are considered posts

Rules

Rules evaluated in sequential order, the first one that match gets applied, lets add one before the route ‘*’ in our Rules file:

route '/posts/*' do
  y,m,d,slug = /([0-9]+)\-([0-9]+)\-([0-9]+)\-([^\/]+)/
    .match(item.identifier).captures

  "/#{y}/#{m}/#{slug}/index.html"
end

slug: is a URL safe version of the post title item.identifier: is the filename (without extension) of the file currently being processed

Formatting

nanoc offers a great deal of flexibility. Here we will change the Markdown instead of default ERB one. Add this to Rules.

compile '/posts/*' do
  filter :kramdown
  layout 'default'
end

Blog posts Layout

create layouts/post.html with the following content:

<% render 'default' do %>
    <div class='post'>
        <h1><%= item[:title] %></h1>
        <aside>Posted at: <%= item[:created_at] %></aside>
        <article>
            <%= yield %>
        </article>
    </div>
<% end %>

And change Rules

compile '/posts/*' do
  filter :kramdown
  layout 'post'
end

This layout use the Rendering helper added above

Listing recent posts on the index page

edit content/index.html to put the following ERB template:

<% sorted_articles.each do |post| %>
    <div class='post'>
        <h1><%= link_to post[:title], post.path %></h1>
        <aside>Posted at: <%= post[:created_at] %></aside>
        <article>
            <%= post.compiled_content %>
        </article>
    </div>
<% end %>

sorted_articles is a variable provided by Blogging helper, contains an ordered list of every Kind: article post.
link_to: generate a link to the full post.

You can now add another post to check it’s working great !!!

Human readable date

Lets create a nanoc helper, add this at the bottom of lib/default.rb

module PostHelper
  def get_pretty_date(post)
    attribute_to_time(post[:created_at]).strftime('%B %-d, %Y')
  end
end

include PostHelper

Use this helper in both layouts/post.html and contents/index.html

<aside>Posted at: <%= get_pretty_date(item) %></aside>

Fold articles on the index page (a fold)

We will use another helper method and a tag like <!-- more -->, first add this tag in one of your article. Now create add the following helper method to lib/default.rd

def get_post_start(post)
  content = post.compiled_content
  if content =~ /\s<!-- more -->\s/
    content = content.partition('<!-- more -->').first +
    "<div class='read-more'><a href='#{post.path}'>Continue reading &rsaquo;</a></div>"
  end
  return content
end

You can now use it within content/index.html

<article>
  <%= get_post_start(post) %>
</article>

Use a Rake task to easily create new blog posts

Create Rakefile in your site root with the following content:

# encoding: utf-8

require 'stringex'
desc "Create a new post"
task :new_post, :title do |t, args|
  mkdir_p './content/posts'
  args.with_defaults(:title => 'New Post')
  title = args.title
  filename = "./content/posts/#{Time.now.strftime('%Y-%m-%d')}-#{title.to_url}.md"

  if File.exist?(filename)
    abort('rake aborted!') if ask("#{filename} already exists. Want to overwrite?", ['y','n']) == 'n'
  end

  puts "Creating new post: #{filename}"
  open(filename, 'w') do |post|
    post.puts '---'
    post.puts "title: \"#{title}\""
    post.puts "created_at: #{Time.now}"
    post.puts 'kind: article'
    post.puts 'published: false'
    post.puts "---\n\n"
  end
end

Now you can use the following command to create a new post

rake new_post["ceph"]

Filters

As of today nanoc provides the following filters : AsciiDoc, BlueCloth, CodeRay, CoffeeScript, ColorizeSyntax, ERB, Erubis, Haml, Handlebars, Kramdown, Less, Markaby, Maruku, Mustache, Pandoc, RDiscount, RDoc, Rainpress, RedCloth, Redcarpet, RelativizePaths, RubyPants, Sass, Slim, Typogruby, UglifyJS, XSL, YUICompressor.

Creating a new filter to process MultiMarkdown is quite simple. You just need a RubyGems able to process your content like rpeg-multimarkdown. As you can see below, you just need to subclass Nanoc::Filter and override the #run method in charge of transforming the content :

require 'rubygems'
require 'multimarkdown'

 class MultiMarkdown < Nanoc::Filter
   identifier :mmd

   def run(content, args)
     MultiMarkdown.new(content).to_html
   end

end

identifier will then be used in compilation rules to process content this filter.

Tags

Install Nanoc::Helpers::Tagging

Make sure you’ve added Tagging helpers to your lib

File lib/helpers.rb
include Nanoc::Helpers::Tagging

which provides :

  • tags_for: return [String] A hyperlinked list of tags for the given item
  • items_with_tag: return [Array] All items with the given tag
  • link_for_tag: return [String] A link for the given tag and the given base URL

For example to display tags in an article, you can do the following :

File layouts/post.haml
%p= tags_for(@item, :base_url => '/tags/')

Get tagging_extra into your lib

Get the source at github, find more details in this thread.

It provides the following methods:

  • tag_set: returns all the tags present in a collection of items or within all the site without collection argument
  • has_tag?: return true if an items has the specified tag
  • items_with_tag: finds all the items having a specified tag
  • count_tags: count the tags in a given collection of items or in overall site if no collection argument
  • rank_tags: return a hash such as: { tag => rank } lower rank is better.

Create Tag Page layout

File layouts/_tag_page.haml
---
---
%section{:id => 'content', :class => 'panel'}
    %h2
        %em
            = "#{tag.capitalize} Articles"
%section{:id => 'content', :class => 'blog'}
    - items_with_tag(tag).each do |item|
        .blog-entry
            -#%a(href="#{item}" title="Full article" class="permalink")= "&laquo; #{item[:title]}"
            %aside
                .date
                    .month= get_post_month(item)
                    .day= get_post_day(item)
            %article
                %h2= link_to item[:title], item.path
                = find_and_preserve do
                    = get_post_start(item)

Add helpers to generate tag pages

File lib/helpers.rb
# Creates in-memory tag pages from partial: layouts/_tag_page.haml
def create_tag_pages
  tag_set(items).each do |tag|
    items << Nanoc::Item.new(
      "= render('_tag_page', :tag => '#{tag}')",           # use locals to pass data
      { :title => "Category: #{tag}", :is_hidden => true}, # do not include in sitemap.xml
      "/tags/#{tag}/",                                     # identifier
      :binary => false
    )
  end
end

Create All Tags page

File content/tags.haml
---
title: All Tags
is_hidden: true
---
%section{:id => 'content', :class => 'panel'}
    %h2
        %em
            All Tags
    %p Listed are the set of tag links related to articles in this site. The number of articles related to a tag    succeeds the tag.

    .tags-page
        - tags = count_tags()
        %ul
            - tags.sort_by{|k,v| k}.each do |tag_count|
                - tag = tag_count[0]
                - count = tag_count[1]
                %li
                    %a(href="/tags/#{tag}/" class='tag')= tag
                    = "[#{count}]"

Create a Rules preprocess to generate each tags page item in memory

File Rules
preprocess do
  # authors may unpublish items by setting meta attribute publish: false
  items.delete_if { |item| item[:published] == false }

  create_tag_pages
end

Manage static content

Some content like plain CSS don’t need any nanoc processing, you just need to position them in a static directory below content. The simplest method to copy them to output is to add a copy_static call to your preprocess Rules.

File Rules
preprocess do
  # authors may unpublish items by setting meta attribute publish: false
  items.delete_if { |item| item[:published] == false }

  copy_static
  create_tag_pages
end

Now, you need to add copy_static to your helpers

File lib/helpers
def copy_static
    FileUtils.cp_r 'static/.', 'output/' 
end

highlighters helpers

Configure colorize_syntax.rb

By using the provided colorize_syntax nanoc helper, you can easily colorize your <pre><code> blocks, like we do in this page. You just have to indent your code block four spaces and put a comment to describe the language in the first line like this:

#!language

example

#!ruby

You’ll find the list of supported language on the Coderay site

CodeRay CSS

Let’s use the trick above and put our coderay.css into our static directory and add it to your default.haml layout.

%link{:rel => 'stylesheet', :type => 'text/css', :href => '/coderay.css'}

This file will be copied at compilation time, see below for the way to do just that.

Call the colorizer in your Rules

By default nanoc use Coderay so you don’t need a line saying :default_colorizer => :coderay

File Rules
compile '/posts/*' do
  filter :mmd
  filter :colorize_syntax,
                :colorizers => { :ruby => :coderay },
                :coderay    => {}
  layout 'post'
end

Use :coderay => { :line_numbers => :inline } to add line numbers.

Creating a Portfolio

First create an entry in a new portfolio directory with a name like 2012-11-20-openstack.md containing

---  
title: OpenStack  
url: http://www.openstack.org  
created_at: 2012-20-11  
kind: portfolio  
image_id: openstack  
---

OpenStack Foundation is now officialy created. Let's Join it.

Create Portfolio Helper lib/portfolio.rb

module PortfolioHelper

  def portfolios
    @items.select { |item| item[:kind] == 'portfolio' }
  end

  def sorted_portfolios
    portfolios.sort_by { |p| attribute_to_time(p[:created_at]) }.reverse
  end

  def portfolio_image_url(item, type)
    '/images/portfolio/' + item[:image_id] + '_' + type + '.jpg'
  end
end

include PortfolioHelper

Create associated Images

openstack_full.jpg
openstack_small.jpg
openstack_large.jpg

Create content/portfolio.haml for index rendering

%h2 My portfolio
- sorted_portfolios.each do |entry|
  .portfolio-entry
    %h3= link_to entry[:title], entry
    .picture{:style => 'background-image:url(' + portfolio_image_url(entry, 'small') + ')'}

Create layouts/portfolio.haml for portfolio rendering

%h2= item[:title]
.portfolio-full
    - if item[:url]
.url= '<strong>URL:</strong> ' + item[:url]
.picture{:style => 'background-image:url(' + portfolio_image_url(entry, 'full') + ')'}
.details
    = yield

Add a new Rule for portfolio rendering/routing in Rules

compile '/portfolio/*' do
  filter :kramdown 
  layout 'portfolio'
end

route '/portfolio/*' do
  y,m,d,slug = /([0-9]+)\-([0-9]+)\-([0-9]+)\-([^\/]+)/.match(item.identifier).captures
  "/portfolio/#{y}/#{slug}/index.html"
end

Using Compass with nanoc

See details here

References

  • This article is built from some Clark Dave blog posts