Rules
The Rules file contains the processing instructions for all items in a Nanoc site. Three different kinds of rules exist:
- compilation rules
- These rules describe the actions that should be executed during compilation (filtering, layouting, and snapshotting).
- routing rules
- These rules describe the path where the compiled items should be written to.
- layouting rules
- These rules describe the filter that should be used for a given layout.
For every item, Nanoc finds the first matching compilation rule. Similarly, for every layout, Nanoc finds the first matching layouting rule. The first matching rule that is found in the rules file is used. If an item or a layout is not using the correct rule, double-check to make sure that the rules are in the correct order.
Compilation rules
A compilation rule describes how an item should be transformed. It has the following shape:
compile "/some/pattern.*" do
# (compilation code here)
end
The argument for the #compile
method call is a pattern.
The code block should execute the necessary actions for compiling the item. The return value of the block is ignored. There are three kinds actions that can be performed:
- filter items, transforming their content
- layout items, placing their content inside a layout
- snapshot items, remembering their content at that point in time for reuse
Lastly, a compilation block can end with a write action, which will write the compiled content to a file with the given name. (Alternatively, you can use routing rules to describe where compiled content should be written to.)
The code block does not need to execute anything. An empty #compile
block will not execute anything. The following rule will not perform any actions, i.e. the item will not be filtered nor laid out:
compile '/images/**/*' do
end
Writing
A compilation rule can contain a #write
call, which will write out the item representation in its current state. It can be called in three ways:
- with a string
- writes the item rep to the given path, relative from the output directory
- with
nil
- prevents Nanoc from applying a matching routing rule (see below for details)
- with
:ext
- writes the item rep to a path that is the same as the identifier, but with a different extension
For example, this compilation rule will copy /images/denis.jpg without further processing:
compile '/images/**/*' do
write item.identifier.to_s
end
This compilation rule will copy all .jpg
and .jpeg
files, and give them the extension .jpg
:
compile '/**/*.{jpg,jpeg}' do
write item.identifier.without_exts + '.jpg'
end
To keep the basename of an item and only change the extension when writing, you can pass the ext
param, specifying an extension. The following compilation rule is the same as the above one, but more concise:
compile '/**/*.{jpg,jpeg}' do
write ext: 'jpg'
end
To prevent Nanoc from applying a matching routing rule, call write nil
at the end of a compilation rule. In the following example, no items in snippets/ will be written out, because the routing rule is skipped:
compile '/snippets/*.md' do
filter :kramdown
write nil
end
route '/**/*.md' do
item.identifier.without_ext + '/index.html'
end
Filtering
To filter an item representation, use the #filter
method. It takes the following arguments:
- (required) the name of the filter (a
Symbol
) - (optional) additional arguments which will be passed on to the filter
For example, the following rule will filter items with identifiers ending in .md
using the :kramdown
filter, but not perform any layouting, and then write it with a .html
extension, so that /about.md is written to /about.html:
compile '/**/*.md' do
filter :kramdown
write @item.identifier.without_ext + '.html'
end
For example, the following rule calls both the :sass
filter as well as the :relativize_paths
filter with the extra arguments:
compile '/**/*.sass' do
filter :sass, style: :compact
filter :relativize_paths, type: :css
write ext: '.css'
end
Laying out
To lay out an item representation, use the #layout
method. It takes the following arguments:
- (required) the identifier of a layout, or a pattern that matches the identifier of a layout
- (optional) additional arguments which will be passed on to the filter
For example, the following rule will (among other things) lay out the item representation using the layout that matches the /shiny.*
pattern:
compile '/about.*' do
filter :erb
layout '/shiny.*'
filter :rubypants
write @item.identifier.without_ext + '/index.html'
end
layout '/*.erb', :erb
The following example is similar, but passes extra arguments (:locals
) to the filter associated with the layout:
compile '/about.*' do
filter :erb
layout '/shiny.*', locals: { daleks: 'exterminate' }
filter :rubypants
write @item.identifier.without_ext + '/index.html'
end
layout '/*.erb', :erb
If layout
is called multiple times, the content is wrapped in each of the specified layouts. In the example below, markdown articles are filtered through the kramdown
filter, then wrapped in the article.erb
layout, and finally wrapped in the default.erb
layout.
compile '/articles/*.md' do
filter :kramdown
layout '/article.*'
layout '/default.*'
end
layout '/*.erb', :erb
Dynamic rules
In the code block, Nanoc exposes @item
and @rep
, among others. See the Variables page for details.
The following rule will only invoke the :erb
filter if the item’s :is_dynamic
attribute is set:
compile '/about.*' do
filter :erb if @item[:is_dynamic]
write @item.identifier.without_ext + '/index.html'
end
Creating snapshots
To take a snapshot of an item representation, call #snapshot
and pass the snapshot name as argument. For example, the following rule will create a snapshot named :without_toc
so that the content at that snapshot can then later be reused elsewhere:
compile '/foo/*' do
filter :markdown
snapshot :without_toc
filter :add_toc
write @item.identifier.without_ext + '/index.html'
end
Handling non-default representations
A :rep
argument can be passed to the #compile
call. This argument contains the name of the representation that is generated by this rule. This is :default
by default.
The following rule will generate a :text
representation for all items below /people:
compile '/people/**/*', rep: :text do
write @item.identifier.without_ext + '.txt'
end
Matching with regular expressions
When using a regular expression to match items, the block arguments will contain all matched groups. This is more useful for routing rules than it is for compilation rules. For example, the following rule will be matched using a regular expression instead of with a wildcard string:
compile %r<\A/blog/\d{4}/.*> do
filter :kramdown
write @item.identifier.without_ext + '/index.html'
end
Routing rules
Routing rules are an alternative way to specify where a compiled item should be written to. It has the following shape:
route '/some/pattern.*' do
# (routing code here)
end
The argument for the #route
method call is a pattern.
The code block should return the routed path for the relevant item. The code block can return nil, in which case the item will not be written.
A compilation rule that ends with a #write
call can be written as a combination of a compilation rule and a routing rule. Typically, using #write
in the compile block leads to a more compact and easier-to-understand Rules file, but separate #route
calls can nonetheless be useful.
The following compile/route rules are equivalent:
compile "/*.md" do
filter :kramdown
end
route "/*.md" do
item.identifier.without_ext + '/index.html'
end
compile "/*.md" do
filter :kramdown
write item.identifier.without_ext + '/index.html'
end
The following rule will give the item with identifier /404.erb the path /errors/404.php:
route "/404.erb" do
"/errors/404.php"
end
The following rule will prevent all items below /links from being written:
route "/links/**/*" do
nil
end
In the code block, Nanoc exposes @item
and @rep
, among others. See the Variables page for details.
The following rule will give all identifiers for which no prior matching rule exists a path based directly on its identifier (for example, the item /foo/bar.html would get the path /foo/bar/index.html):
route "/**/*" do
@item.identifier.without_ext + "/index.html"
end
When using a regular expression to match items, the block arguments will contain all matched groups.
The following rule will capture regex matches and provide them as block arguments (for example, the item with identifier /blog/2015-05-19-something.md will be routed to /blog/2015/05/something/index.html):
route %r[/blog/([0-9]+)\-([0-9]+)\-([0-9]+)\-([^\/]+)\..*] do |_, y, m, d, slug|
"/blog/#{y}/#{m}/#{slug}/index.html"
end
Just like with #compile
calls, a :rep
argument can be passed to the #route
call. This argument contains the name of the representation that this rule applies to.
The following rule will apply to all textual representations of all items below /people (for example, the item /people/denis.md would get the path /people/denis.txt):
route "/people/**/*", rep: :text do
item.identifier.without_ext + '.txt'
end
When a :snapshot
argument is passed to a routing rule definition, then that routing rule applies to the given snapshot only. The default value for the :snapshot
argument is :last
, meaning that compiled items will only be written once they have been fully compiled.
The following rules will apply to the raw
snapshot of all items below /people (for example, the raw snapshot of the item /people/denis.md would get the path /people/denis.txt):
route "/people/**/*", snapshot: :raw do
item.identifier.without_ext + '.txt'
end
Writing/routing pitfalls
A Rules file that combines #write
calls with routing can be confusing. For this reason, we recommend either using compilation rules that use #write
, or routing rules, but not both. This section intends to give more clarity on how #write
calls and routing rules interact.
The main point: If a compilation rule does not end with a #write
call, Nanoc will find the routing rule that matches this item representation, and use it to write.
For example, here the routing rule is used for the /about.md item:
compile '/about.md' do
filter :kramdown
end
route '/about.md' do
'/about.html'
end
# output files created:
# /about.html
In the following example, on the other hand, the routing rule is not used, because the compilation rule ends with a #write
call:
compile '/about.md' do
filter :kramdown
write '/about.html'
end
route '/about.md' do
'/about-irrelevant.html'
end
# output files created:
# /about.html
In the next example, the routing rule is used, because even though there is a #write
call, the compilation rule does not end with one:
compile '/about.md' do
filter :kramdown
write '/about-inbetween.html'
layout '/default.*'
end
route '/about.md' do
'/about.html'
end
# output files created:
# /about-inbetween.html
# /about.html
To prevent Nanoc from applying a matching routing rule, call write nil
at the end of a compilation rule. Refer to the paragraph describing write nil
for details.
To reduce potential confusion, avoid mixing routing rules and #write
calls. Both can be used to specify the path for an item representation, but mixing them will reduce the clarity of the Rules file.
Layouting rules
To specify the filter used for a layout, use the #layout
method. It takes the following arguments:
- (required) the identifier of a layout, or a pattern that matches the identifier of a layout
- (required) the identifier of the filter to use (a
Symbol
) - (optional) additional arguments which will be passed on to the filter
If addition arguments are given to both a layouting rule, as well as a #layout
call for the same layout inside a compilation block, the arguments will be merged.
The following rule will make all layouts use the :erb
filter:
layout '/**/*', :erb
The following rule will make all layouts with the haml extension use the haml
filter, with an additional format
argument that will be passed to the haml
filter:
layout '/*.haml', :haml, format: :html5
The following rule will be applied to all layouts with identifiers starting with a slash followed by an underscore. For example, /foo.erb and /foo/_bar.haml would not match, but /_foo.erb, /_foo/bar.html, and even /_foo/_bar.erb would:
layout %r{\A/_}, :erb
In the following example, the layout is filtered using the haml
filter, with arguments ugly: true
and format: :html5
:
compile '/*.md' do
filter :kramdown
layout '/shiny.*', ugly: true
write ext: 'html'
end
layout '/*.haml', :haml, format: :html5
Convenience methods
The #passthrough
method does no filtering or laying out, and copies the matched item as-is. For example:
passthrough '/assets/images/**/*'
This is a shorthand for the following:
route '/assets/images/**/*' do
item.identifier.to_s
end
compile '/assets/images/**/*' do
end
The #ignore
method does no filtering or laying out, and does not write out the matched item. This is useful for items that are only intended to be included in other items. For example:
ignore '/assets/style/_*'
This is a shorthand for the following:
route '/assets/style/_*' do
nil
end
compile '/assets/style/_*' do
end
Preprocessing
The Rules file can contain a #preprocess
block. This preprocess block is executed before the site is compiled, and has access to all site data (@config
, @items
, and @layouts
). Preprocessors can modify data coming from data sources before it is compiled. It can change item attributes, content, and the path, but also add and remove items.
Here is an example preprocess block that sets the author
attribute to denis
on every HTML document:
preprocess do
@items.each do |item|
item[:author] = 'denis' if item.identifier.ext == 'html'
end
end
Here is an example preprocess block that finds all unique tags, and creates collection pages for them:
preprocess do
tags = @items.flat_map { |i| i[:tags] || [] }.uniq
tags.each do |tag|
content = ''
attributes = { tag: tag }
identifier = "/tags/#{tag}.erb"
@items.create(content, attributes, identifier)
end
end
To make the tag pages work, you’d handle tag items specifically (using a glob /tags/*
to match them) by applying a layout that finds and shows all items for the tag page’s tag. Doing this is left as an exercise to the reader.
Preprocessors can be used for various purposes. Here are two sample uses:
- A preprocessor could set a
language_code
attribute based on the item path. An item such as/en/about/
would get an attributelanguage_code
equal to'en'
. This would eliminate the need for helpers such aslanguage_code_of
. - A preprocessor could create new (in-memory) items for a given set of items. This can be useful for creating pages that contain paginated items.
Post-processing
The Rules file can contain a #postprocess
block. This post-process block is executed after the site is compiled. At this point, all output files have been updated.
Post-processing is useful to model tasks that are tricky to model in Nanoc’s workflow, such as incrementally updating a search index. For example:
postprocess do
items.flat_map(&:modified_reps).each do |rep|
update_search_index(rep.path, rep.compiled_content(snapshot: :last))
end
end