Saying stuff about stuff.

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 a Pathname – which may or may not be an absolute path depending on the configured dir.
  • #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.