@hackage slick0.1.0.1

Slick

Slick is a static site generator written and configured using Haskell. It's the spiritual successor to my previous static-site generator project SitePipe; but is faster, simpler, and more easily used in combination with other tools.

Slick provides a small set of tools and combinators for building static websites on top of the Shake build system. Shake is adaptable, fast, reliable, and caches aggressively so it's a sensible tool for static-site builds, but figuring out how to get started can be a bit abstract. Slick aims to answer the question of 'how do I get a site building?' while giving you the necessary tools and examples to figure out how to accomplish your goals.

See the hackage docs for in depth help on available combinators.

Also check out the example site!

Overview

Here's a quick overview of what Slick can do:

  • Slick provides helpers for loading in blog-post-like things using Pandoc under the hood;
    • This means that if Pandoc can read it, you can use it with Slick!
    • Write your blog posts in Markdown or LaTeX and render it to syntax-highlighted HTML!
    • Slick processes Pandoc (and LaTeX) metadata into a usable form (as an Aeson Value object) which you can manipulate as you please.
  • Slick provides combinators for rendering Mustache templates
    • Slick wraps Justus Adam's Mustache library and provides cached template rendering with awareness of changes to templates, partials, and Mustache objects.
    • It's a thin wrapper so you can still use things like Mustache functions, etc. if you like!
  • Provides only the individual tools without opinions about how to wire them up; if you want to load blog posts from a database and render them out using Blaze html; well go ahead, we can help with that!
  • Provides caching of arbitrary (JSON serializable) objects using Shake resulting in super-fast rebuild times!

Another static site generator? What about Hakyll/Jekyll?

Yup, yet another static site generator. I've tried using Hakyll and Jekyll on different occasions and found there was too much magic going on with all of the monadic contexts for me to understand how to customize things for my use-cases. Even adding simple tags/categories to my blog seemed far more complex then it needed to be; Hakyll specifically got me really bogged down; what was the Compiler monad? How does an Item work? How do I add a custom field? Why couldn't I just edit data directly like I'm used to doing in Haskell? They seemed a bit too opinionated without giving me escape hatches to wire in my own functionality. If they're working for you, then great! But they weren't working for me, so that's where SitePipe and subsequently Slick came from.

Quick Start

The easiest way to get started is to clone this repo and try out the example in the example-site directory.

You can build the example using Stack by cding into the directory and running stack build && stack exec example-site-exe site. This creates a 'dist' folder with the results of the build. A quick way to serve the site is to use Serve.

$ npm install -g serve
serve dist

Then navigate to the port which is serving (usually http://localhost:3000 or http://localhost:5000 )

Example Site:

Here's an example of using slick to render out the posts for a pretty simple blog;

module Main where

import qualified Data.Text as T
import Development.Shake
import Development.Shake.FilePath
import Data.Foldable
import Slick


-- convert a source filepath to a build filepath
-- e.g. site/css/style.css -> build/css/style.css
srcToBuild :: FilePath -> FilePath
srcToBuild path = "build" </> dropDirectory1 path

main' :: IO ()
main' =
  shakeArgs shakeOptions $ do
    -- Require all the things we need to build the site
    -- For this simplified example we'll just copy static assets and build a page for each post
    "site" ~> need ["static", "posts"]
    -- Require all static assets
    "static" ~> do
      staticFiles <- getDirectoryFiles "." ["site/css//*", "site/js//*", "site/images//*"]
      let copyStaticFile path = copyFileChanged path (srcToBuild path)
      traverse_ copyStaticFile staticFiles
     -- Find and require every post to be built
     -- this uses the `~>` 'phony' rule because it doesn't actually write any files on its own
    "posts" ~> do
      postPaths <- getDirectoryFiles "site/posts" ["*.md"]
      -- We tell shake we need to build each individual post
      -- We require each post separately so that Shake can cache them individually
      need (((-<.> "html") . srcToBuild) <$> postPaths)
     -- rule for actually building posts
    "build/posts//*.html" %> \out -> do 
      -- Recover the path where the source file for the post should be
      let srcPath = (dropDirectory1 out) -<.> "md"
      fileContents <- readFile' srcPath
      -- Load a markdown source file into an Aeson Value 
      -- The 'content' key contains an html-rendered string
      -- Any metadata from a yaml block is loaded into the appropriate keys in the Aeson object
      -- e.g. author, date, tags, etc.
      postData <- markdownToHTML . T.pack $ fileContents
      -- Load a mustache template using using cache if available
      template <- compileTemplate' "site/templates/post.html"
      -- Fill in the template using the post metadata/content
      writeFile' out . T.unpack $ substitute template postData

Not pictured above is:

  • Deserializing post metadata into an object which implements FromJSON
  • Using custom Pandoc readers to load other document types
  • Using jsonCaches to cache intermediate JSON results to improve build times and simplify logic.