Introducing Decant
Decant is a dependency-free frontmatter-aware framework-agnostic wrapper around a directory of static content (probably Markdown). It pairs perfectly with Parklife plus your favourite Ruby web framework to help you make a content-driven static website.
Usage
Start by defining a Decant model-like content class that points to a directory of files and their extension. You can declare convenience readers for common frontmatter data, and add your own methods – it’s a standard Ruby class.
Page = Decant.define(dir: 'content', ext: 'md') do
# Declare frontmatter convenience readers.
frontmatter :title
# Add custom methods - it's a standard Ruby class.
def shouty
"#{title.upcase}!!!"
end
end
Then, given the file content/about.md
with the following contents:
---
title: About
stuff: nonsense
---
# About
More words.
You can find
the item by its extension-less path within the directory, read its content and frontmatter, and call frontmatter
convenience methods or your own manually-defined methods:
about = Page.find('about')
about.content # => "# About\n\nMore words.\n"
about.frontmatter # => {:title=>"About", :stuff=>"nonsense"}
about.title # => "About"
about.shouty # => "ABOUT!!!"
Nesting
Files can be nested so with the following layout:
└ content/
├ about.md
├ features/
│ ├ frontmatter.md
│ ├ globs.md
│ ├ nesting
│ │ ├ lots.md
│ │ └ more.md
│ └ slugs.md
└ usage.md
A nested file can be targeted by its full path:
page = Page.find('features/nesting/more')
page.title # => "More Nesting"
Which makes it easy to support catch-all-style routes, like this one in Rails:
# Route.
get '/*slug', to: 'pages#show'
# Controller action.
def show
@page = Page.find(params[:slug])
end
Or Sinatra:
get '/*slug' do
@page = Page.find(params[:slug])
erb :page
end
Collections
Each class provides a few methods for accessing its collection of files. You’ve already seen find
which returns a single matching item, there’s also all
which returns all of the collection’s matching items, and glob
. With glob
you pass a shell-like pattern that can include some special characters, it uses Pathname#glob
so some knowledge of Ruby’s glob behaviour can be useful – though I imagine *
and **/*
will be most commonly used. The other thing to note is that including the file extension isn’t necessary – in fact it mustn’t be included because it will be added for you from the configured ext
.
Here are a couple of examples using the same nested file layout from earlier. You can target a subdirectory’s immediate files:
Page.glob('features/*')
# Returns Page objects for:
# - features/frontmatter.md
# - features/globs.md
# - features/slugs.md
Or include everything from a directory down:
Page.glob('features/**/*')
# Returns Page objects for:
# - features/frontmatter.md
# - features/globs.md
# - features/nesting/lots.md
# - features/nesting/more.md
# - features/slugs.md
The Content object
As well as the file’s content, its frontmatter, frontmatter
-added convenience methods, and your own manually-defined methods, a Decant::Content
object also knows a little about itself:
-
#path
returns aPathname
– which may or may not be an absolute path depending on the configureddir
. -
#relative_path
returns the item’s relative path within its collection. -
#slug
returns the item’s “slug” (its extension-less relative path) within the collection.
page = Page.find('features/slugs')
page.path # => #<Pathname:content/features/slugs.md>
page.path.expand_path # => "/Users/dave/my-website/content/features/slugs.md"
page.relative_path # => "features/slugs.md"
page.slug # => "features/slugs"
Frontmatter
Frontmatter must start with a line consisting of three dashes ---
, then the YAML, then another line of three dashes (just like Jekyll). The YAML should be key/value pairs and the returned Hash
will have Symbol
keys.
---
title: Frontmatter
tags:
- lists
- are
- fine
as:
is:
nesting: etc (it's YAML)
---
Content begins here
The above YAML frontmatter will be turned into the following Ruby data:
{
title: "Frontmatter",
tags: ["lists", "are", "fine"],
as: {
is: {
nesting: "etc (it's YAML)"
}
}
}
What, no Markdown?
You’re probably using Decant with collections of Markdown files – ala Jekyll and friends – so it might be a surprise to find that Decant knows nothing about Markdown and how to turn it into HTML but it’s just not a Decant concern; there are loads of different Markdown libraries out there and it’s up to you which one you choose. Besides, there’s more than just Markdown – remember Textile?
Anyway it’s generally simple to add Markdown support using the library of your choice configured exactly how you like it. Here I’m using Kramdown with its GitHub-Flavoured Markdown support:
Page = Decant.define(dir: 'content', ext: 'md') do
def html
Kramdown::Document.new(content, input: 'GFM').to_html
end
end
about = Page.find('about')
about.html # => "<h1 id=\"about\">About</h1>\n\n<p>More words.</p>\n"
Sinatra also has a builtin markdown
helper that can be used in a view:
<div class="page">
<%= markdown @page.content %>
</div>
Or directly from a route (and wrapped by a layout):
get '/*slug' do
@page = Page.find(slug)
markdown @page.content, layout_engine: :erb
end
But maybe you can even get away without Markdown and use something like Rails’s simple_format
helper.
Maybe just make a website
When paired with Parklife plus the Ruby web framework of your choice Decant helps make it incredibly easy to make a content-driven static website with very little code and none of the constraints of a traditional static site framework. I’m using Decant in a few places (here for instance), the most visible is the Parklife website that, thanks to Sinatra, has just a handful of lines of code leaving me to get on with making a website.