Nanoc 4 upgrade guide

Nanoc 4 takes a clean break from the past, removing anything that was holding back future development.

The good news is that Nanoc 4.0 is similar to 3.8. Upgrading a Nanoc 3.x site to Nanoc 4.0 only takes minutes.

Why upgrade?

  • Nanoc 4 brings identifiers with extensions, and thereby solves a long-standing usability issue. It also introduces glob patterns, which makes rules easier to write.
  • Nanoc 4 paves the way for new features and performance improvements. Nanoc 3 exposed its internals in a public API, making it hard to make significant changes.
  • Nanoc 3 is in maintenance mode, which means it will only get critical bug fixes.

Installing Nanoc 4

Before installing, ensure you have a supported version of Ruby. Nanoc supports Ruby 2.5 and up:

% ruby --version
ruby 3.3.3 (2024-06-12 revision f1c7b6f435) [x86_64-linux]
%

To upgrade Ruby, follow the installation instructions on the Ruby website.

You can install Nanoc 4 using RubyGems:

% gem install nanoc
If the gem install command fails with a permission error, you likely have to prefix the command with sudo. Do not use sudo until you have tried the command without it; using sudo when not appropriate will damage your RubyGems installation.

We recommend using Bundler to manage dependencies. When using Bundler, ensure there is a line for Nanoc in the Gemfile that looks like this:

gem 'nanoc', '~> 4.0'

Quick upgrade guide

The following steps will get a Nanoc 3 site working on Nanoc 4 with a minimal amount of changes.

  1. Change mentions of Nanoc3 to Nanoc.
  2. Change mentions of @site.config to @config.
  3. Add identifier_type: legacy to the individual data source configurations. For example:
    data_sources:
      -
        type: filesystem
    data_sources:
      -
        type: filesystem
        identifier_type: legacy
  4. Add string_pattern_type: legacy to the configuration file. For example:
    data_sources:
      -
        type: filesystem
        identifier_type: legacy
    string_pattern_type: legacy
    data_sources:
      -
        type: filesystem
        identifier_type: legacy
  5. In Rules, remove the rep. prefix from filter, layout, and snapshot. For example:
    compile '*' do
      rep.filter :erb
      rep.layout 'default'
    end
    compile '*' do
      filter :erb
      layout 'default'
    end
  6. In the preprocess block, use @items.create rather than instantiating Nanoc::Item. For example:
    @items << Nanoc::Item.new('Hello', {}, '/hello/')
    @items.create('Hello', {}, '/hello/')
  7. In data sources, use #new_item or #new_layout rather than instantiating Nanoc::Item or Nanoc::Layout. For example:
    def items
      [Nanoc::Item.new('Hello', {}, '/hello/')]
    end
    def items
      [new_item('Hello', {}, '/hello/')]
    end
  8. In data sources, replace the identifier argument in calls to #new_item and #new_layout with an explicit Nanoc::Identifier instance, constructed with type: legacy:
    def items
      [new_item('Hello', {}, '/hello/')]
    end
    def items
      [new_item('Hello', {}, Nanoc::Identifier.new('/hello/', type: :legacy))]
    end
  9. Replace .reps[0] by .reps[:default]. For example:
    item.reps[0].path
    item.reps[:default].path
  10. Replace calls to #rep_named by reps[something], where something is the argument to #rep_named. For example:
    item.rep_named(:raw).path
    item.reps[:raw].path
  11. If you use the static data source, disable it for now and follow the extended upgrade instructions below.

Extended upgrade guide

This section describes how to upgrade a site to identifiers with extensions and glob patterns. For details, see the Identifiers and patterns page.

This section assumes you have already upgraded the site following the instructions in the Quick upgrade guide section above.

Before you start, add enable_output_diff: true to the configuration file. This will let the compile command write out a diff with the changes to the compiled output. This diff will allow you to verify that no unexpected changes occur.

If you use a filter that minifies HTML content, such as html5small, we recommend turning it off before upgrading the site, so that the output diff becomes easier to read.

Enabling glob patterns

Before enabling them, ensure you are familiar with glob patterns. For details, see the Glob patterns section on the Identifiers and patterns page.

To use glob patterns:

  1. Set string_pattern_type to glob in the configuration file. For example:
    string_pattern_type: legacy
    string_pattern_type: glob
  2. Ensure that all string patterns in the Rules file, as well as in calls to @items[…], @layouts[…], and #render throughout the site, start and end with a slash. This is an intermediate step. For example:
    # Before
    compile 'articles/*' do
      layout 'default'
    end
    # After
    compile '/articles/*/' do
      layout '/default/'
    end
    # Before
    @items['foo']
    @layouts['/bar']
    # After
    @items['/foo/']
    @layouts['/bar/']
    <!-- Before -->
    <%= render 'header' %>
    <!-- After -->
    <%= render '/header/' %>
  3. Replace * and + with **/* in all string patterns in the Rules file, as well as in calls to @items[…], @layouts[…], and #render throughout the site. For example:
    compile '/articles/*/' do
      layout '/default/'
    end
    compile '/articles/**/*/' do
      layout '/default/'
    end
    @items['/articles/*/']
    @items['/articles/**/*/']

This approach should work out of the box: Nanoc should not raise errors and the output diff should be empty.

Enabling identifiers with extensions

This section assumes that glob patterns have been enabled.

Before enabling them, ensure you are familiar with identifiers with extensions. See the Identifiers section on the Identifiers and patterns page for documentation.

To use identifiers with extensions:

  1. Set identifier_type to full in the configuration file. For example:
    identifier_type: legacy
    identifier_type: full
  2. Remove the trailing slash from any argument to #compile, #route, and #layout in the Rules file, as well as in calls to @items[…], @layouts[…], and #render throughout the site. If the pattern does not end with a “*”, add “.*”. For example:
    compile '/articles/**/*/' do
      filter :kramdown
      layout '/default/'
    end
    
    compile '/about/' do
      layout '/default/'
    end
    compile '/articles/**/*' do
      filter :kramdown
      layout '/default.*'
    end
    
    compile '/about.*' do
      layout '/default.*'
    end
    @items['/about/']
    @layouts['/default/']
    @items['/about.*']
    @layouts['/default.*']
    <%= render '/root/' %>
    <%= render '/root.*' %>
  3. Update the routing rules to output the correct path. For example:
    route '/articles/*/' do
      # /articles/foo/ gets written to /articles/foo/index.html
      item.identifier + 'index.html'
    end
    route '/articles/**/*' do
      # /articles/foo.md gets written to /articles/foo/index.html
      item.identifier.without_ext + '/index.html'
    end
  4. Create a routing rule that matches index files in the content directory (such as content/index.md or content/blog/index.md). For example, put the following before any rules matching /**/*:
    route '/**/index.*' do
      # /projects/index.md gets written to /projects/index.html
      item.identifier.without_ext + '.html'
    end
  5. If the site has calls to #children, ensure the lib/helpers.rb file contains use_helper Nanoc::Helpers::ChildParent, and replace method calls to #children with a function call to #children_of, passing in the item as an argument. For example:
    @items['/articles/'].children
    @item.children
    children_of(@items['/articles/'])
    children_of(@item)
  6. If the site has calls to #parent, ensure the lib/helpers.rb file contains use_helper Nanoc::Helpers::ChildParent, and replace method calls to #parent with a function call to #parent_of, passing in the item as an argument. For example:
    @item.parent
    parent_of(@item)
When using identifiers with extensions, the children and parent of an item are no longer unambiguous. For example, the two items /foo.md and /foo.adoc both have /foo/bar.md as a child, and /foo/bar.md has two parents.

Upgrading from the static data source

This section assumes that glob patterns and identifiers with extensions have been enabled.

The static data source no longer exists in Nanoc 4. It existed in Nanoc 3 to work around the problem of identifiers not including the file extension, which is no longer the case in Nanoc 4.

Theoretically, with identifiers with extensions enabled, it is possible to move the contents of the static/ directory into content/. This can be tricky, however, because some rules that did not match any items in static/ might now match.

Because of this, the recommended approach for upgrading is to keep the static/ directory, and set up a new data source that reads from this directory.

In the site configuration, re-enable the static data source, change its type to filesystem, set content_dir to "static" and layouts_dir to null:

data_sources:
  -
    type: filesystem
  -
    type: filesystem
    items_root: /static
    content_dir: 'static'
    layouts_dir: null

The null value for the layouts_dir option prevents this data source from loading layouts—the other data source already does so.

Lastly, update the rules to copy these items as-is, but without the /static prefix:

compile '/static/**/*' do
end

route '/static/**/*' do
  # /static/foo.html → /foo.html
  item.identifier.to_s.sub(/\A\/static/, '')
end

This approach should work out of the box: Nanoc should not raise errors and the output diff should be empty.

A final improvement would be to move the contents of the static/ directory into content/. The main thing to watch out for with this approach is rules that accidentally match the wrong items.

Troubleshooting

  1. If you use Nanoc with a Gemfile, ensure you call Nanoc as bundle exec nanoc. Nanoc no longer attempts to load the Gemfile.

  2. If you get a NoMethodError error on Nanoc::Identifier, call .to_s on the identifier before doing anything with it. In Nanoc 4.x, identifiers have their own class and are no longer strings.

    item.identifier[7..-2]
    item.identifier.to_s[7..-2]
  3. If you get a NoMethodError that you did not expect, you might be using a private API that is no longer present in Nanoc 4.0. In case of doubt, ask for help on GitHub discussions or the Google group.

Removed features

The watch and autocompile commands have been removed. Both were deprecated in Nanoc 3.6. Use guard-nanoc instead.

Because Nanoc’s focus is now more clearly on compiling content rather than managing it, the following features have been removed:

  1. the create-item and create-layout commands
  2. the update and sync commands
  3. VCS integration (along with Nanoc::Extra::VCS)
  4. the DataSource#create_item and DataSource#create_layout methods.