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 ›</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 itemitems_with_tag
: return [Array] All items with the given taglink_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")= "« #{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:
#!ruby
#!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
References
- This article is built from some Clark Dave blog posts
Links
- nanoc google groups