diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,9 @@
 # Changelog for sitepipe-shake
 
+## 1.0.0.0
+- Deprecate Slick.Caching, Simplify all other exports down.
+- Switch to recommending `Shake.Development.Forward`
+
 ## 0.2.0.0
 - Allow IO in `makePandocReader` and `makePandocWriter` to allow use of complex filters, etc.
 
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
 # Slick
 
+Want to get started quickly? Check out the [Slick site template](https://github.com/ChrisPenner/slick-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](https://github.com/chrispenner/SitePipe/); but is faster, simpler, and more
 easily used in combination with other tools.
@@ -13,19 +15,17 @@
 
 See the [hackage docs](https://hackage.haskell.org/package/slick) for in depth help on available combinators.
 
-Also check out the [example site](https://github.com/ChrisPenner/Slick/blob/master/example-site/app/Main.hs)!
-
 # 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;
+- Slick uses the Shake build tool; the same used by ghcide! We recommend using `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.
+-   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.
+        [Aeson](https://hackage.haskell.org/package/aeson) Value object) which you can manipulate as you please.
 - Slick provides combinators for rendering [Mustache templates](https://mustache.github.io/)
     - Slick wraps Justus Adam's [Mustache](http://hackage.haskell.org/package/mustache-2.3.0/docs/Text-Mustache.html)
         library and provides cached template rendering with awareness of changes to templates, partials, and Mustache
@@ -52,76 +52,141 @@
 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 `cd`ing 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](https://www.npmjs.com/package/serve).
-
-```shell
-$ npm install -g serve
-serve dist
-```
-
-Then navigate to the port which is serving (usually http://localhost:3000 or http://localhost:5000 )
+Want to get started quickly? Check out the [Slick site template](https://github.com/ChrisPenner/slick-template) and follow the steps there.
 
 
 # Example Site:
 
-Here's an example of using slick to render out the posts for a pretty simple blog;
+Here's an example of using slick to build an ENTIRE blog with full automatic asset caching.
 
 ```haskell
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE OverloadedStrings #-}
+
 module Main where
 
-import qualified Data.Text as T
-import Development.Shake
-import Development.Shake.FilePath
-import Data.Foldable
-import Slick
+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/"
 
--- 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
+-- | Data for the index page
+data IndexInfo =
+  IndexInfo
+    { posts :: [Post]
+    } deriving (Generic, Show, FromJSON, ToJSON)
 
-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
+-- | 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 <- compileTemplate' "site/templates/index.html"
+  let indexInfo = IndexInfo {posts = posts'}
+      indexHTML = T.unpack $ substitute indexT (toJSON indexInfo)
+  writeFile' (outputFolder </> "index.html") indexHTML
+
+-- | Find and build all posts
+buildPosts :: Action [Post]
+buildPosts = do
+  pPaths <- getDirectoryFiles "." ["site/posts//*.md"]
+  forP pPaths buildPost
+
+-- | Load a post, process metadata, write it to output, then return the post object
+-- Detects changes to either post content or template
+buildPost :: FilePath -> Action Post
+buildPost srcPath = cacheAction ("build" :: T.Text, srcPath) $ do
+  liftIO . putStrLn $ "Rebuilding post: " <> srcPath
+  postContent <- readFile' srcPath
+  -- load post content and metadata as JSON blob
+  postData <- markdownToHTML . T.pack $ postContent
+  let postUrl = T.pack . dropDirectory1 $ srcPath -<.> "html"
+      withPostUrl = _Object . at "url" ?~ String postUrl
+  -- Add additional metadata we've been able to compute
+  let fullPostData = withPostUrl $ postData
+  template <- compileTemplate' "site/templates/post.html"
+  writeFile' (outputFolder </> 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 <- getDirectoryFiles "./site/" ["images//*", "css//*", "js//*"]
+    void $ forP filepaths $ \filepath ->
+        copyFileChanged ("site" </> filepath) (outputFolder </> filepath)
+
+-- | Specific build rules for the Shake system
+--   defines workflow to build the website
+buildRules :: Action ()
+buildRules = do
+  allPosts <- buildPosts
+  buildIndex allPosts
+  copyStaticFiles
+
+-- | Kick it all off
+main :: IO ()
+main = do
+  let shOpts = forwardOptions $ shakeOptions { shakeVerbosity = Chatty}
+  shakeArgsForward shOpts buildRules
 ```
 
 Not pictured above is:
 
-- Deserializing post metadata into an object which implements `FromJSON`
-- Using custom Pandoc readers to load other document types
-- Using `jsonCache`s to cache intermediate JSON results to improve build times and simplify logic.
+- Using custom Pandoc readers to load other document types, there are many helpers for this in the [slick library](https://hackage.haskell.org/package/slick)
+- Using custom build tools like sassy css or js minifiers; you can do these things using [Shake](https://hackage.haskell.org/package/shake) directly.
+
+
+# Caching guide
+
+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`](https://hackage.haskell.org/package/shake/docs/Development-Shake-Forward.html). The idea is that you can wrap actions with [`cacheAction`](https://hackage.haskell.org/package/shake-0.18.3/docs/Development-Shake-Forward.html#v: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 `cacheAction` Shake 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:
+
+```haskell
+{-# 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`](https://hackage.haskell.org/package/shake-0.18.3/docs/Development-Shake-Forward.html#v: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 `.shake` in your working directory after editing your `Main.hs` file as shake can get confused if rules change without it noticing.
diff --git a/slick.cabal b/slick.cabal
--- a/slick.cabal
+++ b/slick.cabal
@@ -1,19 +1,21 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.31.0.
+-- This file has been generated from package.yaml by hpack version 0.31.2.
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 3cc544477064cc4c1a83dee37ce64b9559692a238162f8704a62c1fb10265bed
+-- hash: 0def596009a546aa4cc3a910ba96020819375b351a32b7ff351c592cb3813f17
 
 name:           slick
-version:        0.2.0.0
+version:        1.0.0.0
+synopsis:       A quick & easy static site builder built with shake and pandoc.
 description:    Please see the README on GitHub at <https://github.com/ChrisPenner/slick#readme>
+category:       Web
 homepage:       https://github.com/ChrisPenner/slick#readme
 bug-reports:    https://github.com/ChrisPenner/slick/issues
 author:         Chris Penner
 maintainer:     example@example.com
-copyright:      2018 Chris Penner
+copyright:      2019 Chris Penner
 license:        BSD3
 license-file:   LICENSE
 build-type:     Simple
@@ -31,45 +33,22 @@
       Slick.Caching
       Slick.Mustache
       Slick.Pandoc
+      Slick.Shake
+      Slick.Utils
   other-modules:
       Paths_slick
   hs-source-dirs:
       src
+  ghc-options: -Wall
   build-depends:
       aeson
     , base >=4.7 && <5
-    , binary
     , bytestring
-    , containers
-    , lens
-    , lens-aeson
+    , directory
+    , extra
     , mustache
     , pandoc
     , shake
     , text
-    , time
-  default-language: Haskell2010
-
-test-suite slick-test
-  type: exitcode-stdio-1.0
-  main-is: Spec.hs
-  other-modules:
-      Paths_slick
-  hs-source-dirs:
-      test
-  ghc-options: -threaded -rtsopts -with-rtsopts=-N
-  build-depends:
-      aeson
-    , base >=4.7 && <5
-    , binary
-    , bytestring
-    , containers
-    , lens
-    , lens-aeson
-    , mustache
-    , pandoc
-    , shake
-    , slick
-    , text
-    , time
+    , unordered-containers
   default-language: Haskell2010
diff --git a/src/Slick.hs b/src/Slick.hs
--- a/src/Slick.hs
+++ b/src/Slick.hs
@@ -1,38 +1,35 @@
+{-|
+Module      : Slick
+Description : A quick & simple static site builder built on Shake and Pandoc
+Copyright   : (c) Chris Penner, 2019
+License     : BSD3
+-}
 module Slick
   (
   -- * Slick
-  -- | This module re-exports everything you need to use Slick
-
-  -- ** Mustache
-    compileTemplate'
+  -- | This module re-exports the basics you need to run slick.
+  --  For more complex tasks look into "Slick.Pandoc".
+  --
+  --  To get started use the <https://github.com/ChrisPenner/slick-template Slick Template>.
 
-  -- ** Pandoc
-  , PandocReader
-  , PandocWriter
+  -- ** Basics
+    slick
+  , slickWithOpts
   , markdownToHTML
   , markdownToHTML'
-  , makePandocReader
-  , makePandocReader'
-  , loadUsing
-  , loadUsing'
-  , markdownOptions
-  , html5Options
 
-  -- ** Aeson
-  , convert
-
-  -- ** Shake
-  , simpleJsonCache
-  , simpleJsonCache'
-  , jsonCache
-  , jsonCache'
+  -- ** Mustache Templating
+  , compileTemplate'
+  , substitute
 
-  -- ** Re-exported
-  , module Text.Mustache
+  -- ** Utils
+  , getDirectoryPaths
+  , convert
   )
 where
 
-import           Slick.Caching
-import           Slick.Mustache
-import           Slick.Pandoc
-import           Text.Mustache                     hiding ( (~>) )
+import Slick.Mustache
+import Slick.Pandoc
+import Slick.Shake
+import Slick.Utils
+import Text.Mustache
diff --git a/src/Slick/Caching.hs b/src/Slick/Caching.hs
--- a/src/Slick/Caching.hs
+++ b/src/Slick/Caching.hs
@@ -1,9 +1,15 @@
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeFamilies #-}
-{-# LANGUAGE DeriveGeneric #-}
+{-|
+Module      : Slick.Shake
+Description : DEPRECATED -- Advanced caching tools for using slick with shake
+Copyright   : (c) Chris Penner, 2019
+License     : BSD3
+-}
+{-# LANGUAGE DeriveGeneric              #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE TypeFamilies               #-}
 
-module Slick.Caching
+module Slick.Caching {-# DEPRECATED "No longer necessary with slick >= 0.3.0.0" #-}
   ( simpleJsonCache
   , simpleJsonCache'
   , jsonCache
@@ -11,12 +17,13 @@
   )
 where
 
-import           Data.Aeson                    as A
-import           Data.ByteString.Lazy
-import           Development.Shake                 hiding ( Resource )
-import           Development.Shake.Classes
-import           GHC.Generics                             ( Generic )
+import Data.Aeson                 as A
+import Data.ByteString.Lazy
+import Development.Shake          hiding (Resource)
+import Development.Shake.Classes
+import GHC.Generics               (Generic)
 
+--------------------------------------------------------------------------------
 
 newtype CacheQuery q =
   CacheQuery q
@@ -24,18 +31,20 @@
 
 type instance RuleResult (CacheQuery q) = ByteString
 
--- | A wrapper around 'addOracleCache' which given a @q@ which is a 'ShakeValue'
+-- | Note that you probably don't need this if you're using the recommended @Development.Shake.Forward@ module. It can do a lot of caching for you, otherwise look at 'Development.Shake.Forward.cacheAction'.
+--
+-- A wrapper around 'addOracleCache' which given a @q@ which is a 'ShakeValue'
 -- allows caching and retrieving 'Value's within Shake. See documentation on
 -- 'addOracleCache' or see Slick examples for more info.
--- 
+--
 -- > -- We need to define a unique datatype as our cache key
 -- > newtype PostFilePath =
 -- >   PostFilePath String
--- > -- We can derive the classes we need (using GeneralizedNewtypeDeriving) 
+-- > -- We can derive the classes we need (using GeneralizedNewtypeDeriving)
 -- > -- so long as the underlying type implements them
 -- >   deriving (Show, Eq, Hashable, Binary, NFData)
 -- > -- now in our shake rules we can create a cache by providing a loader action
--- > 
+-- >
 -- > do
 -- > postCache <- jsonCache $ \(PostFilePath path) ->
 -- >   readFile' path >>= markdownToHTML . Text.pack
@@ -61,12 +70,14 @@
       Left  err -> fail err
       Right res -> pure res
 
--- | A wrapper around 'jsonCache' which simplifies caching of values which do NOT
+-- | Note that you probably don't need this if you're using the recommended @Development.Shake.Forward@ module. It can do a lot of caching for you, otherwise look at 'Development.Shake.Forward.cacheAction'.
+--
+-- A wrapper around 'jsonCache' which simplifies caching of values which do NOT
 -- depend on an input parameter. Unfortunately Shake still requires that the
--- key type implement several typeclasses, however this is easily accomplished 
+-- key type implement several typeclasses, however this is easily accomplished
 -- using @GeneralizedNewtypeDeriving@ and a wrapper around @()@.
 -- example usage:
--- 
+--
 -- > {-# LANGUAGE GeneralizedNewtypeDeriving #-}
 -- > module Main where
 -- > newtype ProjectList = ProjectList ()
diff --git a/src/Slick/Mustache.hs b/src/Slick/Mustache.hs
--- a/src/Slick/Mustache.hs
+++ b/src/Slick/Mustache.hs
@@ -1,3 +1,9 @@
+{-|
+Module      : Slick.Mustache
+Description : Slick utilities for working with mustache
+Copyright   : (c) Chris Penner, 2019
+License     : BSD3
+-}
 module Slick.Mustache
   ( compileTemplate'
   )
@@ -7,9 +13,7 @@
 import           Text.Mustache
 import           Text.Mustache.Compile
 
-
--- | Like 'compileTemplate' but tracks changes to template files and partials
--- within Shake.
+-- | Like 'compileTemplate' from <http://hackage.haskell.org/package/mustache mustache> but tracks changes to template files and partials within Shake for cache-busting.
 compileTemplate' :: FilePath -> Action Template
 compileTemplate' fp = do
   need [fp]
diff --git a/src/Slick/Pandoc.hs b/src/Slick/Pandoc.hs
--- a/src/Slick/Pandoc.hs
+++ b/src/Slick/Pandoc.hs
@@ -1,48 +1,67 @@
+{-|
+Module      : Slick.Pandoc
+Description : Slick utilities for working with Pandoc
+Copyright   : (c) Chris Penner, 2019
+License     : BSD3
+-}
 {-# LANGUAGE OverloadedStrings #-}
 
 module Slick.Pandoc
   ( markdownToHTML
   , markdownToHTML'
+  , markdownToHTMLWithOpts
+  , markdownToHTMLWithOpts' 
   , makePandocReader
   , makePandocReader'
+  , PandocReader
+  , PandocWriter
   , loadUsing
   , loadUsing'
+  , defaultMarkdownOptions
+  , defaultHtml5Options
   , convert
-  , html5Options
-  , markdownOptions
-  , PandocReader
-  , PandocWriter
+  , flattenMeta
   ) where
 
-import Control.Lens
-import Control.Monad
 import Data.Aeson
-import Data.Aeson.Lens
-import qualified Data.Text as T
-import Development.Shake hiding (Resource)
+import Development.Shake
 import Text.Pandoc
 import Text.Pandoc.Highlighting
 import Text.Pandoc.Shared
+import Slick.Utils
+import Data.HashMap.Strict as HM
 
--- | Reasonable options for reading a markdown file
-markdownOptions :: ReaderOptions
-markdownOptions = def { readerExtensions = exts }
- where
-  exts = mconcat
-    [ extensionsFromList
-      [ Ext_yaml_metadata_block
-      , Ext_fenced_code_attributes
-      , Ext_auto_identifiers
-      ]
-    , githubMarkdownExtensions
-    ]
+import qualified Data.Text                  as T
 
--- | Reasonable options for rendering to HTML
-html5Options :: WriterOptions
-html5Options = def { writerHighlightStyle = Just tango
-                   , writerExtensions     = writerExtensions def
-                   }
+--------------------------------------------------------------------------------
 
+type PandocReader textType = textType -> PandocIO Pandoc
+type PandocWriter = Pandoc -> PandocIO T.Text
+
+-- | Reasonable options for reading a markdown file. Behaves similar to Github Flavoured
+-- Markdown
+defaultMarkdownOptions :: ReaderOptions
+defaultMarkdownOptions =
+  def { readerExtensions = exts }
+  where
+    exts = mconcat
+     [ extensionsFromList
+       [ Ext_yaml_metadata_block
+       , Ext_fenced_code_attributes
+       , Ext_auto_identifiers
+       ]
+     , githubMarkdownExtensions
+     ]
+
+-- | Reasonable options for rendering to HTML. Includes default code highlighting rules
+defaultHtml5Options :: WriterOptions
+defaultHtml5Options =
+  def { writerHighlightStyle = Just tango
+      , writerExtensions     = writerExtensions def
+      }
+
+--------------------------------------------------------------------------------
+
 -- | Handle possible pandoc failure within the Action Monad
 unPandocM :: PandocIO a -> Action a
 unPandocM p = do
@@ -50,69 +69,99 @@
   either (fail . show) return result
 
 -- | Convert markdown text into a 'Value';
--- The 'Value'  has a "content" key containing rendered HTML
--- Metadata is assigned on the respective keys in the 'Value'
-markdownToHTML :: T.Text -> Action Value
-markdownToHTML =
-  loadUsing (readMarkdown markdownOptions) (writeHtml5String html5Options)
+--
+--   The 'Value'  has a "content" key containing rendered HTML.
+--
+--   Metadata is assigned on the respective keys in the 'Value'
+markdownToHTML :: T.Text
+               -> Action Value
+markdownToHTML txt =
+    markdownToHTMLWithOpts defaultMarkdownOptions defaultHtml5Options txt
 
 -- | Like 'markdownToHTML' but allows returning any JSON serializable object
-markdownToHTML' :: (FromJSON a) => T.Text -> Action a
-markdownToHTML' = markdownToHTML >=> convert
+markdownToHTML' :: (FromJSON a)
+                => T.Text
+                -> Action a
+markdownToHTML' txt =
+    markdownToHTML txt >>= convert
 
-type PandocReader textType = textType -> PandocIO Pandoc
+-- | Like 'markdownToHTML' but allows returning any JSON serializable object
+markdownToHTMLWithOpts
+    :: ReaderOptions  -- ^ Pandoc reader options to specify extensions or other functionality
+    -> WriterOptions  -- ^ Pandoc writer options to modify output
+    -> T.Text         -- ^ Text for conversion
+    -> Action Value
+markdownToHTMLWithOpts rops wops txt =
+  loadUsing
+    (readMarkdown rops)
+    (writeHtml5String wops)
+    txt
 
-type PandocWriter = Pandoc -> PandocIO T.Text
+-- | Like 'markdownToHTML' but allows returning any JSON serializable object
+markdownToHTMLWithOpts'
+    :: (FromJSON a)
+    => ReaderOptions  -- ^ Pandoc reader options to specify extensions or other functionality
+    -> WriterOptions  -- ^ Pandoc writer options to modify output
+    -> T.Text         -- ^ Text for conversion
+    -> Action a
+markdownToHTMLWithOpts' rops wops txt =
+    markdownToHTMLWithOpts rops wops txt >>= convert
 
 -- | Given a reader from 'Text.Pandoc.Readers' this creates a loader which
--- given the source document will read its metadata into a 'Value'
--- returning both the 'Pandoc' object and the metadata within an 'Action'
-makePandocReader :: PandocReader textType -> textType -> Action (Pandoc, Value)
+--   given the source document will read its metadata into a 'Value'
+--   returning both the 'Pandoc' object and the metadata within an 'Action'
+makePandocReader :: PandocReader textType
+                 -> textType
+                 -> Action (Pandoc, Value)
 makePandocReader readerFunc text = do
   pdoc@(Pandoc meta _) <- unPandocM $ readerFunc text
   return (pdoc, flattenMeta meta)
 
 -- | Like 'makePandocReader' but will deserialize the metadata into any object
--- which implements 'FromJSON'. Failure to deserialize will fail the Shake
--- build.
-makePandocReader'
-  :: (FromJSON a) => PandocReader textType -> textType -> Action (Pandoc, a)
+--   which implements 'FromJSON'. Failure to deserialize will fail the Shake
+--   build.
+makePandocReader' :: (FromJSON a) => PandocReader textType
+                  -> textType
+                  -> Action (Pandoc, a)
 makePandocReader' readerFunc text = do
   (pdoc, meta)  <- makePandocReader readerFunc text
   convertedMeta <- convert meta
   return (pdoc, convertedMeta)
 
+--------------------------------------------------------------------------------
+
 -- | Load in a source document using the given 'PandocReader', then render the 'Pandoc'
--- into text using the given 'PandocWriter'.
--- Returns a 'Value' wherein the rendered text is set to the "content" key and 
--- any metadata is set to its respective key in the 'Value'
-loadUsing :: PandocReader textType -> PandocWriter -> textType -> Action Value
+--   into text using the given 'PandocWriter'.
+--   Returns a 'Value' wherein the rendered text is set to the "content" key and
+--   any metadata is set to its respective key in the 'Value'
+loadUsing :: PandocReader textType
+          -> PandocWriter
+          -> textType
+          -> Action Value
 loadUsing reader writer text = do
   (pdoc, meta) <- makePandocReader reader text
   outText      <- unPandocM $ writer pdoc
-  let withContent = meta & _Object . at "content" ?~ String outText
+  withContent <- case meta of
+      Object m -> return . Object $ HM.insert "content" (String outText) m
+          -- meta & _Object . at "content" ?~ String outText
+      _ -> fail "Failed to parse metadata"
   return withContent
 
 -- | Like 'loadUsing' but allows also deserializes the 'Value' into any object
--- which implements 'FromJSON'.  Failure to deserialize will fail the Shake
--- build.
-loadUsing'
-  :: (FromJSON a)
-  => PandocReader textType
-  -> PandocWriter
-  -> textType
-  -> Action a
-loadUsing' reader writer text = loadUsing reader writer text >>= convert
+--   which implements 'FromJSON'.  Failure to deserialize will fail the Shake
+--   build.
+loadUsing' :: (FromJSON a)
+           => PandocReader textType
+           -> PandocWriter
+           -> textType
+           -> Action a
+loadUsing' reader writer text =
+  loadUsing reader writer text >>= convert
 
--- | Attempt to convert between two JSON serializable objects (or 'Value's).
--- Failure to deserialize fails the Shake build.
-convert :: (FromJSON a, ToJSON a, FromJSON b) => a -> Action b
-convert a = case fromJSON (toJSON a) of
-  Success r   -> pure r
-  Error   err -> fail $ "json conversion error:" ++ err
+--------------------------------------------------------------------------------
 
 -- | Flatten a Pandoc 'Meta' into a well-structured JSON object, rendering Pandoc
--- text objects into plain strings along the way.
+--   text objects into plain strings along the way.
 flattenMeta :: Meta -> Value
 flattenMeta (Meta meta) = toJSON $ fmap go meta
  where
diff --git a/src/Slick/Shake.hs b/src/Slick/Shake.hs
new file mode 100644
--- /dev/null
+++ b/src/Slick/Shake.hs
@@ -0,0 +1,27 @@
+{-|
+Module      : Slick.Shake
+Description : Slick utilities for working with shake
+Copyright   : (c) Chris Penner, 2019
+License     : BSD3
+-}
+module Slick.Shake
+    ( slick
+    , slickWithOpts
+    ) where
+
+import Development.Shake
+import Development.Shake.Forward
+
+-- | Build your slick site. This is a good candidate for your 'main' function.
+--
+-- Calls through to 'shakeArgsForward' with extra verbosity
+slick :: Action () -> IO ()
+slick buildAction =
+    slickWithOpts (shakeOptions { shakeVerbosity = Chatty }) buildAction
+
+-- | Build your slick site with the provided shake options. This is a good candidate for your 'main' function.
+--
+-- | Calls through to 'shakeArgsForward' with the provided options
+slickWithOpts :: ShakeOptions -> Action () -> IO ()
+slickWithOpts opts buildAction =
+    shakeArgsForward opts buildAction
diff --git a/src/Slick/Utils.hs b/src/Slick/Utils.hs
new file mode 100644
--- /dev/null
+++ b/src/Slick/Utils.hs
@@ -0,0 +1,38 @@
+{-|
+Module      : Slick.Utils
+Description : Slick helper utilities
+Copyright   : (c) Chris Penner, 2019
+License     : BSD3
+-}
+module Slick.Utils
+  ( getDirectoryPaths
+  , convert
+  ) where
+
+import Data.Aeson
+import Development.Shake
+import Development.Shake.FilePath
+
+------------------------------------------------------------------------------
+-- Helper functions
+
+-- | Given a list of extensions and directories,
+--   find all files that match, and return full paths.
+getDirectoryPaths :: [FilePath]         -- ^ file pattern like *.md
+                  -> [FilePath]         -- ^ directories to look at
+                  -> Action [FilePath]
+getDirectoryPaths extensions dirs =
+  concat <$> mapM getPaths dirs
+    where
+      getPaths :: FilePath -> Action [FilePath]
+      getPaths dir =
+        fmap (dir </>) <$>
+          getDirectoryFiles dir extensions
+
+-- | Attempt to convert between two JSON serializable objects (or 'Value's).
+--   Failure to deserialize fails the Shake build.
+convert :: (FromJSON a, ToJSON a, FromJSON b) => a -> Action b
+convert a = case fromJSON (toJSON a) of
+  Success r   -> pure r
+  Error   err -> fail $ "json conversion error:" ++ err
+
diff --git a/test/Spec.hs b/test/Spec.hs
deleted file mode 100644
--- a/test/Spec.hs
+++ /dev/null
@@ -1,2 +0,0 @@
-main :: IO ()
-main = putStrLn "Test suite not yet implemented"
