Static site generator built on Shake configured in Haskell
Want to get started quickly? Check out the Slick site template!
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.
If you would rather see live examples than documentation, you can check out:
Here's a quick overview of what Slick can do:
Development.Shake.Forward; it auto-discovers which resources it should cache as you go! This means a blazing fast static site builder without all the annoying dependency tracking.
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.
Want to get started quickly? Check out the Slick site template and follow the steps there.
Here's an example of using slick to build an ENTIRE blog with full automatic asset caching.
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE OverloadedStrings #-}module Main where
import Control.Lens import Control.Monad import Data.Aeson as A import Data.Aeson.Lens import Development.Shake import Development.Shake.Classes import Development.Shake.Forward import Development.Shake.FilePath import GHC.Generics (Generic) import Slick import qualified Data.Text as T
outputFolder :: FilePath outputFolder = "docs/"
-- | Data for the index page data IndexInfo = IndexInfo { posts :: [Post] } deriving (Generic, Show, FromJSON, ToJSON)
-- | Data for a blog post data Post = Post { title :: String , author :: String , content :: String , url :: String , date :: String , image :: Maybe String } deriving (Generic, Eq, Ord, Show, FromJSON, ToJSON, Binary)
-- | given a list of posts this will build a table of contents buildIndex :: [Post] -> Action () buildIndex posts' = do indexT "index.html") indexHTML
-- | Find and build all posts buildPosts :: Action [Post] buildPosts = do pPaths Action Post buildPost srcPath = cacheAction ("build" :: T.Text, srcPath) $ do liftIO . putStrLn $ "Rebuilding post: " <> srcPath postContent "html" withPostUrl = _Object . at "url" ?~ String postUrl -- Add additional metadata we've been able to compute let fullPostData = withPostUrl $ postData template T.unpack postUrl) . T.unpack $ substitute template fullPostData -- Convert the metadata into a Post object convert fullPostData
-- | Copy all static files from the listed folders to their destination copyStaticFiles :: Action () copyStaticFiles = do filepaths copyFileChanged ("site" > filepath) (outputFolder > filepath)
-- | Specific build rules for the Shake system -- defines workflow to build the website buildRules :: Action () buildRules = do allPosts
Not pictured above is:
Shake takes care of most of the tricky parts, but there're still a few things you need to know.
Cache-busting in Slick works using
Development.Shake.Forward. The idea is that you can wrap actions with
cacheAction, providing an unique identifier for each time it runs. Shake will track any dependencies which are triggered during the first run of that action and can use them to detect when that particular action must be re-run. Typically you'll want to cache an action for each "thing" you have to load, e.g. when you load a post, or when you build a page. You can also nest these caches if you like.
When using
cacheActionShake will automatically serialize and store the results of that action to disk so that on a later build it can simply 'hydrate' that asset without running the command. For this reason, your data models should probably implement
Binary. Here's an example data model:
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveAnyClass #-}import Data.Aeson (ToJSON, FromJSON) import Development.Shake.Classes (Binary) import GHC.Generics (Generic)
-- | Data for a blog post data Post = Post { title :: String , author :: String , content :: String , url :: String , date :: String , image :: Maybe String } deriving (Generic, Eq, Ord, Show, FromJSON, ToJSON, Binary)
If you need to run arbitrary shell commands you can use
cache; it will do its best to track file use during the run of the command and cache-bust on that; results may vary. It's likely better to use explicit tracking commands like
readFile'when possible, (or even just use
readFile'on the files you depend on, then throw away the results. It's equivalent to explicitly depending on the file contents).
Shake has many dependency tracking combinators available; whenever possible you should use the shake variants of these (e.g.
copyFileChanged,
readFile',
writeFile', etc.). This will allow shake to detect when and what it needs to rebuild.
Note: You'll likely need to delete
.shakein your working directory after editing your
Main.hsfile as shake can get confused if rules change without it noticing.