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!
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.
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
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
- Using custom Pandoc readers to load other document types
jsonCaches to cache intermediate JSON results to improve build times and simplify logic.