1. Introduction to nanoc
  2. Installation
  3. Getting Started
  4. Basic Concepts
  5. Advanced Concepts
  6. Guides
       
    1. Deploying sites
    2. Paginating articles
    3. Using filters based on file extensions
    4. Using binary items effectively
    5. Creating multilingual sites
  7. Glossary
  8. Frequently Asked Questions
  9. API documentation

Guides

Deploying sites

There are quite a few ways to deploy a site to a web host. The most traditional way of uploading a site involves using FTP to transfer the files (perhaps using an “update” or “synchronise” option). If your web host provides access via SSH, you can use SCP or SFTP to deploy a site.

With rsync

If your web host supports rsync, then deploying a site can be fully automated, and the transfer itself can be quite fast, too. rsync is unfortunately a bit cumbersome, providing a great deal of options (check man rsync in case of doubt), but fortunately nanoc provides a “deploy” command that can make this quite a bit easier: a simple nanoc deploy will deploy your site.

To use the deploy command, open the config.yaml file and add a deploy hash. Inside, add a hash with a key that describes the destination (for example, public or staging). Inside this hash, set dst to the destination, in the format used by rsync and scp, to where the files should be uploaded, and set kind to rsync. Here’s what it will look like:

deploy:
  public:
    kind: rsync
    dst:  "stoneship.org:/var/www/sites/example.com/public"
  staging:
    kind: rsync
    dst:  "stoneship.org:/var/www/sites/example.com/public/staging"

By default, the rsync deployer will upload all files in the output directory to the given location. None of the existing files in the target location will be deleted; however, be aware that files with the same name will be overwritten. To run the deploy command, pass it a --target option, like this:

% nanoc deploy --target staging

This will deploy using the “staging” configuration. Replace “staging” with “public” if you want to deploy to the location marked with “public”.

If you want to check whether the executed rsync command is really correct, you can perform a dry run by passing --dry-run. The rsync command will be printed, but not executed. For example:

% nanoc deploy --target public --dry-run

You can override the options that nanoc uses for invoking rsync. The following example will make sure that all existing files on the remote server are deleted after uploading (use with caution!).

options: [ '-gpPrtvz', '--delete-after' ]

Paginating articles

First, a word of caution: I am not a fan of paginating items. Even though pagination is fairly easy to do in nanoc, I recommend not doing it, for one specific reason. Every time an object is added to a paginated collection, one object shifts from one page to the next. When a paginated page is bookmarked, it may show entirely different content a month later, and when a paginated page turns up as a result on a search engine, it may no longer contain the content that the person was looking for anymore. To avoid these issues, I recommend creating separate pages for each category, tag or year. Having said all this, I’ll nonetheless show you how to do pagination in nanoc, so you can get an idea of how it can be done.

To paginate articles, we’ll use the preprocessor block in the Rules file. The preprocessor contains code that will be executed before the site is compiled, but after the site data (items, layouts, etc) have been loaded. This is the ideal moment to create new items (which is what we will be doing) or modify or delete existing ones. The preprocessor block will look like this:

preprocess do
  def paginate_articles
    # code will go here
  end

  paginate_articles
end

Like the snippet says, the actual code for paginating the articles will go in the #paginate_articles method that is defined inside the preprocess block. The code could go directly in the preprocess block, with no method definitions or calls, like this:

preprocess do
  # code will go here
end

…but if your preprocessor block grows, it’s quite useful to separate different tasks in different methods. It’s also possible to store the method definitions in the lib/ directory somewhere, allowing the preprocessor block to be quite small and easy to understand.

The #paginate_articles method will have to do three things: fetch a list of all articles to paginate, split the entire list of articles in a list of sub-lists (read: “pages”), and finally generate a Nanoc::Item for each of these sub-lists.

The first step, getting all articles to paginate, is quite easy. It will probably look a bit like this:

articles_to_paginate = items.select { |i| i[:kind] == 'article' }.
  sort_by { |a| Time.parse(a[:created_at]) }

However, you can use the blogging helper to make this a bit cleaner. The blogging helper has a #sorted_articles method, which does exactly that. Here’s the cleaned-up version:

include Nanoc::Helpers::Blogging
articles_to_paginate = sorted_articles

The next step involves splitting the list of articles into sub-arrays. Here’s an easy way to do it:

article_groups = []
until articles_to_paginate.empty?
  article_groups << articles_to_paginate.slice!(0..4)
end

This will generate sub-lists of length 5, but you can make the length of these pages configurable by adding page_size: 5 in the site configuration and changing the article-slicing code to the following:

article_groups = []
until articles_to_paginate.empty?
  article_groups << articles_to_paginate.slice!(0..@config[:page_size]-1)
end

The final step involves generating pages for each individual sub-list. This is done by constructing Nanoc::Item objects and adding them to the @items array, like this:

article_groups.each_with_index do |subarticles, i|
  first = i*config[:page_size] + 1
  last  = (i+1)*config[:page_size]

  @items << Nanoc::Item.new(
    "… page content here …",
    { :title => "Archive (articles #{first} to #{last})" },
    "/blog/archive/#{i+1}/"
  )
end

In this piece of code, I’m running over each sub-list (named articles) with the index of the sub-list (named i). I’m also calculating the index of the first and the last article, so I can give the pagination pages titles such as “Archive (articles 15 to 20).” The last statement generates the actual item: the first argument is the page content (see next paragraph), the second argument is a hash with attributes, and the last argument is the item identifier.

Obviously, one thing that is still lacking from the above piece of code is the content for the pagination page. This content should be code that runs over each article and prints each one. The content for this pagination page is rather large, so I’ve opted to create a partial named pagination_page and put the content in there. This partial is stored in layouts/pagination_page.erb and looks like this:

<% pages = sorted_articles.slice(@item_id*@config[:page_size], @config[:page_size]) %>
<% pages.each do |article| %>
  <h1><%= article[:title] %></h1>
  <%= article.compiled_content %>
<% end %>

Your version should probably be a little more fancy, including a navigation bar and previous/next links, but you get the idea: this piece of code takes a sub-list of the list of all articles and iterates over this sub-list. To make the newly generated pagination pages render this partial, replace the “page content here” string with the following:

"<%= render 'pagination_page', :item_id => #{i} %>"

The final thing that you should take care of, is to ensure that the pagination_page partial is rendered using the correct filter. In the Rules file, add this line above all other layout rules:

layout '/pagination_page/', :erb

You should also ensure that the generated pagination pages are filtered using erb. Add this line above all other compilation rules:

compile '/blog/archive/*/' do
  filter :erb
  layout '/default/'
end

When you compile the site now, you should see that nanoc generates new files corresponding to the pagination pages, such as output/blog/archive/1/index.html. Mission complete!

Using filters based on file extensions

Each item has an :extension attribute, containing the extension of the file that corresponds with the item. For example, an item with identifier /foo/ that is read from content/foo.md will have an :extension attribute with value 'md'.

You can use this attribute in the Rules file. If you want to run filters based on file extensions, use a case/when statement on the file extension, like this:

compile '*' do
  case item[:extension]
    when 'md'
      filter :rdiscount
    when 'haml'
      filter :haml
  end
end

Using binary items effectively

nanoc 3.1 adds support for binary items. Such items will not be loaded into memory, allowing nanoc to handle large files if necessary. Images, audio files and videos are good examples of binary items. Support for binary items makes nanoc quite powerful. This section gives a couple of examples of how binary items can be used.

Preparing videos for HTML5

Let’s see how a video file can be handled by nanoc. The video file that I’ll use in this example is a H.264 file, which I would like to use in a HTML5 <video> element. Because Firefox does not support the H.264 format, we’ll let nanoc convert this item into a Theora-encoded movie in an Ogg container.

Note: Because videos can take a long time to convert (several minutes or more), and because nanoc’s dependency resolution is not yet perfect, it is for the time being not recommended to let nanoc convert these items. It may be better to convert movies up front and copy them to the output directory at compile time (perhaps using a Rake task).

Let’s assume the source video file is stored in content/movies/rick.mp4. For the time being, let’s just copy this file to the output and not transform it yet. For this, a compilation and a routing rule will be necessary:

compile '/movies/*/' do
  # Do nothing
end

route '/movies/*/' do
  # Make sure that "/movies/rick/" becomes "/movies/rick.mp4"
  item.identifier.chop + '.' + item[:extension]
end

We’ll use the ffmpeg2theora commandline tool to convert videos from various source formats into Theora. Usually, ffmpeg2theora will be invoked like this:

ffmpeg2theora input.mp4 --output output.ogg

The conversion will be handled by a filter. The code for this filter is listed below. This filter should be stored somewhere in the lib/ directory—I recommend lib/filters/ffmpeg2theora.rb. Note how the filter writes the generated file to the filename returned by the #output_filename method; this is necessary for nanoc to find the generated file.

class Ffmpeg2TheoraFilter < Nanoc::Filter

  identifier :ffmpeg2theora
  type       :binary

  def run(filename, params={})
    system('ffmpeg2theora', filename, '--output', output_filename)
  end

end

Note that it is a good idea to add error checking to the #run method here: if the executed command exits with a non-zero exit status, indicating failure, or if no output file is written, the filter should raise an exception. For clarity, this error-handling code has been left out.

Creating the Theora representations of the original video is now done by adding a compilation rule and a routing rule for a new :theora representation, like this:

compile '/movies/*/', :rep => :theora do
  filter :ffmpeg2theora
end

route '/movies/*/', :rep => :theora do
  # Make sure that "/movies/rick/" becomes "/movies/rick.ogg"
  item.identifier.chop + '.ogg'
end

When the site is compiled, you should find two new files the output directory: output/movies/rick.mp4 and output/movies/rick.ogg, which you can now use in HTML5 video!

Creating multilingual sites

Creating a site in multiple languages can be tedious, but nanoc can nonetheless be useful in making the management of multilingual sites a bit easier. The approach that I will be describing in this guide is fairly opinionated. It is not necessarily the best way, but it is an approach that worked quite well for me. This guide is inspired by the techniques I used for the Myst Online web site. Feel free to check out the source for the Myst Online web site to see the details about how it is done.

A multilingual site is a site where each page is available in multiple languages. Each language forms some sort of sub-site. For example, the English translation could have pages “About” and “Play,” while the Dutch translation could have matching “Over” and “Speel” pages.

For the Myst Online site, I decided to organise the pages in different languages by creating a top-level directory containing the abbreviated language name (en for English, nl for Dutch, fr for French, etc). Inside the language-specific directory, each page has a path that is also translated (so no /nl/play, but rather /nl/speel). Here’s an example:

/en
  /about
  /play
/nl
  /over
  /speel
/fr
  /informations
  /jouez

The way these pages are maintained is fairly standard: each page is an individual item in the content/ directory that is compiled to the output/ directory. You’d create these items just like you would in an ordinary, monolingual site. Here’s what the content/ directory looks like:

content/
  en/
    about.html
    play.html
  nl/
    over.html
    speel.html
  fr/
    informations.html
    jouez.html

One useful function that will be necessary later on is #language_code_of, which returns the language code (e.g. en or nl) for a given item. This function is implemented like this:

def language_code_of(item)
  # "/en/foo/" becomes "en"
  (item.identifier.match(/^\/([a-z]{2})\//) || [])[1]
 end

Once you have the basic content, you can improve the site to make it easier for switch languages. For this to work, each item needs a “canonical identifier” so that it is easy to find translations of a given page. The same items in different languages will all have the same canonical identifier. For the Myst Online web site, I chose the English identifier as the canonical identifier, but the choice you make here is fairly arbitrary. For example, the “/en/about/” page has “/about/” as its canonical identifier, and “/nl/speel/” has “/play/” as its canonical identifier. The canonical identifier is probably best stored in a canonical_identifier attribute.

Now, it is possible to find all translations of a given item simply by finding all items with the same canonical identifier. This is fairly simple:

def translations_of(item)
  @items.select do |i| 
    i[:canonical_identifier] == item[:canonical_identifier]
  end
end

One more function is necessary: one that converts a language code into the language name (in the language itself, so it should not return “Dutch” for “nl” but it should return “Nederlands”). Here’s now this function works:

LANGUAGE_CODE_TO_NAME_MAPPING = {
  'en' => 'English',
  'nl' => 'Nederlands'
}

def language_name_for_code(code)
  LANGUAGE_CODE_TO_NAME_MAPPING[code]
end

For completeness, let’s write a function that returns the language name for a given item as well:

def language_name_of(item)
  language_name_for_code(
    language_code_of(item))
end

Now, it is possible to link to all translations from a given item. Here’s how it is done (in ERB):

<ul>
  <% translations_of(@item).each do |t| %>
    <li>
      <a href="<%= t.path %>">
        <%= language_name_of(t) %>
      </a>
    </li>
  <% end %>
</ul>

It is best to prevent linking to the active page, so you should check whether the translation t is the same as @item and handle this situation differently. For example:

<ul>
  <% translations_of(@item).each do |t| %>
    <li>
      <% if @item == t %>
        <span class="active">
          <%= language_name_of(t) %>
        </span>
      <% else %>
        <a href="<%= t.path %>">
          <%= language_name_of(t) %>
        </a>
      <% end %>
    </li>
  <% end %>
</ul>

One extra enhancement would be to indicate the language of the link destinations as well as the language of the link text itself. For this, the hreflang resp. the lang attributes are used. Here’s what the code could look like:

<ul>
  <% translations_of(@item).each do |t| %>
    <li>
      <% if @item == t %>
        <span class="active" lang="<%= language_code_of(t) %>">
          <%= language_name_of(t) %>
        </span>
      <% else %>
        <a href="<%= t.path %>"
           lang="<%= language_code_of(t) %>"
           hreflang="<%= language_code_of(t) %>">
          <%= language_name_of(t) %>
        </a>
      <% end %>
    </li>
  <% end %>
</ul>

The language of the links and the link destinations are now indicated, but the language of the document itself isn’t yet. The html element should get a lang attribute that contains the language code. Here’s what it could look like in the layout:

<html lang="<%= language_code_of(@item) %>">

At this point, the site is already a lot friendlier for people from different languages. One thing is still missing , though: a landing page that redirects people to the language of their choice. This means that the landing page will require server-side scripting. For the Myst Online site, I used PHP as this is a widely available scripting language for creating web sites, but other languages such as Ruby would have worked as well. A good way of redirecting visitors is to check the contents of the Accept-Language HTTP header, find the preferred language, and then redirect them to the appropriate page.

Here’s the PHP code for parsing the header and returning a list of language codes requested by the user agent, sorted by decreasing preference (“qval”):

// Parse the Accept-Language header
$langs = array();
if(isset($_SERVER['HTTP_ACCEPT_LANGUAGE']))
{
  // Parse language
  // e.g. en-ca,en;q=0.8,en-us;q=0.6,de-de;q=0.4,de;q=0.2
  preg_match_all(
    '/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i',
    $_SERVER['HTTP_ACCEPT_LANGUAGE'],
    $lang);

  if(count($lang[1]) > 0)
  {
    // Create key-value pair
    $langs = array_combine($lang[1], $lang[4]);

    // Use default q value of 1
    foreach ($langs as $lang => $val)
    {
      if ($val === '')
        $langs[$lang] = 1;
    }

    // Sort based on q value
    arsort($langs, SORT_NUMERIC);
  }
}

Once the list of requested language codes is constructed, we can iterate over this list and try to redirect. For each of the requested languages, check whether the site has a translation in this language, and if it does, redirect.

First, though, we need to build the list of codes of all languages the site is translated in. This involves generating PHP code using Ruby code, which is icky, but it does the trick. Here’s the code:

<%# Find all language codes %>
<%
home         = @items.find { |i| i.identifier == '/en/' }
translations = translations_of(home)
codes        = translations.map { |t| language_code_of(t) }
%>

<%# Build PHP array of language codes %>
$codes = array(<%= codes.join(', ') %>);

The redirection code itself is given below. Note the redirection to the English version of the site s a fallback if no other languages could be satisfied.

// Show correct site
foreach($langs as $request_lang => $qval)
{
  foreach($codes as $code)
  {
    if(strpos($request_lang, $code) === 0)
      redirect($code);
  }
}
redirect('en');

The PHP redirect() function still needs to be implemented. This function creates a HTTP redirect to the home page in the given language. The HTTP header is different based on whether HTTP 1.0 or 1.1 is used. Here is its implementation:

function redirect($lang)
{
  global $base_url;
  
  // Set HTTP status code
  if ($_SERVER['SERVER_PROTOCOL'] == 'HTTP/1.1')
    header('HTTP/1.1 303 See Other');
  else
    header('HTTP/1.0 302 Moved Temporarily');
  
  // Set location
  header('Location: ' . $base_url . '/' . $lang . '/');
  
  // Stop!
  exit();
}

The global $base_url variable contains the base URL for the web site. For the Myst Online web site, this is “http://mystonline.com”. It is used to build the full redirection URL. You can either hardcode this in PHP, like this:

$base_url = 'http://mystonline.com';

… or you can set the base_url confguration attribute in “config.yaml” and generate the PHP code for setting it (a bit icky, but DRYer):

$base_url = '<%= @site.config[:base_url] %>';

That’s the end of this guide. Now, you have a web site in multiple languages where every language is given equal attention. For example, a German speaking person can arrive on the site and be redirected to the German version of the site, and even the URLs will be in German.