diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,16 @@
 
 The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 
+## [0.1.1](https://github.com/elben/pencil/)
+
+### Added
+- Blog example.
+- Minor method changes.
+
+### Changed
+- Bounds changed for ghc 8.0.2 and 8.2.2 support.
+- Improved documentation.
+
 ## [0.1.0](https://github.com/elben/pencil/)
 
 ### Added
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
+[![Build Status](https://travis-ci.org/elben/pencil.svg?branch=master)](https://travis-ci.org/elben/pencil)
+
 # Pencil
 
 Pencil is a static site generator. Use it to generate your personal website!
@@ -10,15 +12,24 @@
 
 # Examples
 
-Checkout the [examples provided](https://github.com/elben/pencil/blob/master/test/Example/). To run the [Simple](https://github.com/elben/pencil/blob/master/test/Example/Simple) example:
+Checkout the [examples provided](https://github.com/elben/pencil/tree/master/examples). To run the [Simple](https://github.com/elben/pencil/tree/master/examples/Simple) example:
 
 ```
 stack build
 stack exec pencil-example-simple
 ```
 
-Open the `examples/Simple/out/` folder to see the rendered web pages.
+Open the `examples/Simple/out/` folder to see the rendered web pages. To serve
+the web pages (so that relative URLs work), using `python`'s built in web server
+is easiest:
 
+```
+cd examples/Simple/out/
+python -m SimpleHTTPServer 8000
+```
+
+And go to [localhost:8000](http://localhost:8000).
+
 # Development
 
 ```bash
@@ -38,4 +49,48 @@
 ```bash
 stack install hasktags
 hasktags --ignore-close-implementation --ctags .
+```
+
+## Release
+
+Make sure it builds, passes tests, and works:
+
+```
+stack build
+stack test
+stack exec pencil-example-simple
+stack exec pencil-example-blog
+stack haddock
+```
+
+Check that tutorials are updated.
+
+Update the CHANGELOG.md.
+
+Update the version number in `pencil.cabal`.
+
+Commit the changes.
+
+Tag the release:
+
+```
+git tag v0.1.0
+git push --tags
+```
+
+Push to Hackage:
+
+```
+stack sdist
+stack upload
+```
+
+## Travis CI
+
+[travis-ci.org/elben/pencil](https://travis-ci.org/elben/pencil)
+
+Note that `.travis.yml` was generated using [multi-ghc-travis](https://github.com/haskell-hvr/multi-ghc-travis) this:
+
+```
+stack exec runghc ~/code/multi-ghc-travis/make_travis_yml_2.hs pencil.cabal > .travis.yml
 ```
diff --git a/examples/Blog/Main.hs b/examples/Blog/Main.hs
new file mode 100644
--- /dev/null
+++ b/examples/Blog/Main.hs
@@ -0,0 +1,65 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import Pencil
+import Pencil.Blog
+import qualified Data.HashMap.Strict as H
+import qualified Data.Text as T
+
+websiteTitle :: T.Text
+websiteTitle = "My Blog"
+
+config :: Config
+config =
+  (updateEnv (insertText "title" websiteTitle) .
+   setSourceDir "examples/Blog/site/" .
+   setOutputDir "examples/Blog/out/") defaultConfig
+
+website :: PencilApp ()
+website = do
+  layout <- load toHtml "layout.html"
+
+  -- Load all our blog posts into `posts`, which is of type [Page]
+  postLayout <- load toHtml "post-layout.html"
+  posts <- loadBlogPosts "blog/"
+
+  -- Build tag index pages. This is a map of a Tag (which is just text)
+  -- to a Page which has the environment stuffed with blog posts that has that
+  -- tag.
+  tagPages <- buildTagPages "tag-list.html" posts
+
+  -- Render all our blog posts. So for each post in posts, we:
+  --
+  -- - injectTitle - This generates a `title` variable in our env that has the
+  --   format of "post title - My Blog"
+  --
+  -- - injectTagsEnv - This injects some environment variables so that our blog
+  --   post page knows what tags it has (via `tags` variable), AND a reference to
+  --   the tag index page that we built above (via `this.url`). Look through
+  --   post-layout.html to see how to use these tag variables.
+  --
+  -- - (layout <|| postLayout <|) - We push the generated post Page into this
+  --   structure, so that all blog posts share the same postLayout.
+  --
+  -- - render - Finally we render all of our blog posts into files.
+  --
+  render $ fmap ((layout <|| postLayout <|) . injectTagsEnv tagPages . injectTitle websiteTitle)
+                posts
+
+  -- Build our index page. Insert the blog posts into the env, so that we can
+  -- render the list of blog posts. `withEnv` tells Pencil to use the modified
+  -- environment when rendering the index page, since that's the env that has
+  -- the list of blog posts.
+  index <- load toHtml "index.html"
+  env <- asks getEnv
+  let indexEnv = insertPages "posts" posts env
+  withEnv indexEnv (render (layout <|| index))
+
+  -- Render tag list pages. This is so that we can go to /blog/tags/awesome to
+  -- see all the blog posts tagged with "awesome".
+  render $ fmap (layout <||) (H.elems tagPages)
+
+main :: IO ()
+main = run website config
+
diff --git a/pencil.cabal b/pencil.cabal
--- a/pencil.cabal
+++ b/pencil.cabal
@@ -1,5 +1,5 @@
 name:                pencil
-version:             0.1.0
+version:             0.1.1
 synopsis: Static site generator
 description:
   Pencil is a static site generator. Use it to generate your personal website!
@@ -15,27 +15,30 @@
 build-type:          Simple
 extra-source-files:  README.md, CHANGELOG.md
 cabal-version:       >=1.10
+tested-with:         GHC == 8.0.2,
+                     GHC == 8.2.2
 
 library
   hs-source-dirs:      src
   exposed-modules:     Pencil
                      , Pencil.Blog
-                     , Pencil.Internal
+                     , Pencil.Internal.Pencil
                      , Pencil.Internal.Env
                      , Pencil.Internal.Parser
-  build-depends:       base >= 4.7 && < 5
+  build-depends:       base >= 4.8 && < 5
                      , data-default >= 0.7 && < 1
-                     , directory >= 1.3 && < 1.4
+                     , directory >= 1.2.5.0 && < 1.4
                      , edit-distance >= 0.2.2.1 && < 0.3
                      , feed >= 0.3.12.0 && < 1
                      , filepath >= 1.4 && < 1.5
                      , hashable >= 1.2.6.0 && < 1.3
                      , hsass >= 0.4.0 && < 0.5
                      , mtl >= 2.2 && < 3
-                     , pandoc >= 1.19.2 && < 2
+                     , pandoc >= 1.19.2.4 && < 2
+                     , semigroups >= 0.18.2 && < 0.19
                      , parsec >= 3.1 && < 3.2
                      , text >= 1.2.2 && < 1.3
-                     , time >= 1.6 && < 1.7
+                     , time >= 1.5.0.1 && < 1.9
                      , unordered-containers >= 0.2.7.2 && < 0.3
                      , vector >= 0.12.0 && < 0.13
                      , xml >= 1.3.10 && < 1.4
@@ -46,7 +49,7 @@
   type:                exitcode-stdio-1.0
   hs-source-dirs:      test
   main-is:             Spec.hs
-  build-depends:       base
+  build-depends:       base >= 4.8 && < 5
                      , pencil
                      , doctest >= 0.11.4 && < 0.12
   ghc-options:         -Wall -threaded -rtsopts -with-rtsopts=-N
@@ -56,8 +59,18 @@
   hs-source-dirs:      examples/Simple
   main-is:             Main.hs
   ghc-options:         -Wall -threaded -rtsopts -with-rtsopts=-N
-  build-depends:       base
+  build-depends:       base >= 4.8 && < 5
                      , pencil
+  default-language:    Haskell2010
+
+executable pencil-example-blog
+  hs-source-dirs:      examples/Blog
+  main-is:             Main.hs
+  ghc-options:         -Wall -threaded -rtsopts -with-rtsopts=-N
+  build-depends:       base >= 4.8 && < 5
+                     , pencil
+                     , unordered-containers
+                     , text
   default-language:    Haskell2010
 
 source-repository head
diff --git a/src/Pencil.hs b/src/Pencil.hs
--- a/src/Pencil.hs
+++ b/src/Pencil.hs
@@ -76,7 +76,7 @@
 
   ) where
 
-import Pencil.Internal
+import Pencil.Internal.Pencil
 
 import Control.Monad.Reader as Reader
 
@@ -177,7 +177,7 @@
 -- and a @style.css@ file in the @examples\/Simple\/out\/@ folder.
 --
 -- To learn more, read through the documentation found in this module. To build
--- a blog, look at 'Pencil.Blog'.
+-- a blog, look at the Pencil.Blog module.
 
 ----------------------------------------------------------------------
 
diff --git a/src/Pencil/Blog.hs b/src/Pencil/Blog.hs
--- a/src/Pencil/Blog.hs
+++ b/src/Pencil/Blog.hs
@@ -9,6 +9,7 @@
     loadBlogPosts
   , blogPostUrl
   , injectTitle
+  , buildTagPagesWith
   , buildTagPages
   , injectTagsEnv
   ) where
@@ -25,6 +26,9 @@
 -- $gettingstarted
 --
 -- This module provides a standard way of building and generating blog posts.
+-- Check out the Blog example
+-- <https://github.com/elben/pencil/blob/master/examples/Blog/ here>.
+--
 -- To generate a blog for your website, first create a @blog/@ directory in
 -- your web page source directory.
 --
@@ -33,50 +37,38 @@
 -- > yyyy-mm-dd-title-of-blog-post.markdown
 --
 -- The files in that directory are expected to have preambles that have at
--- least the following variables:
+-- least @postTitle@ and @date@ defined. The other ones are optional.
 --
 -- > <!--PREAMBLE
 -- > postTitle: "Behind Python's unittest.main()"
 -- > date: 2010-01-30
--- > -->
---
--- You can also mark a post as a draft via the @draft@ variable (it won't be
--- loaded when you call 'loadBlogPosts'), and add tagging (see below) via
--- @tags@:
---
--- > <!--PREAMBLE
--- > ...
 -- > draft: true
 -- > tags:
 -- >   - python
 -- > -->
 --
--- Then, use 'loadBlogPosts' to load the entire @blog/@ directory.
---
--- @
--- posts <- 'loadBlogPosts' "blog/"
--- forM_ posts render
--- @
+-- You can mark a post as a draft via the @draft@ variable (it won't be
+-- loaded when you call 'loadBlogPosts'), and add tagging (see below) via
+-- @tags@. Then, use 'loadBlogPosts' to load the entire @blog/@ directory.
 --
--- You probably will want to enclose your blog posts in your web site's
--- layout, however. In the example below, @layout.html@ defines the outer HTML
--- structure (with global components like navigation), and @blog-post.html@ is
--- a generic blog post container that renders @${postTitle}@ as a header,
--- @${date}@, and @${body}@ for the post body.
+-- In the example below, @layout.html@ defines the outer HTML structure (with
+-- global components like navigation), and @blog-post.html@ is a generic blog
+-- post container that renders @${postTitle}@ as a header, @${date}@, and
+-- @${body}@ for the post body.
 --
 -- @
 -- layout <- 'load' toHtml "layout.html"
 -- postLayout <- 'load' toHtml "blog-post.html"
 -- posts <- 'loadBlogPosts' "blog/"
--- forM_ posts (\\post -> render (layout <|| postLayout <| post))
+-- render (fmap (layout <|| postLayout <|) posts)
 -- @
 --
 
--- | Loads the given directory as a series of blog posts.
+-- | Loads the given directory as a series of blog posts, sorted by the @date@
+-- PREAMBLE environment variable. Posts with @draft: true@ are filtered out.
 --
 -- @
 -- posts <- loadBlogPosts "blog/"
--- forM_ posts render
 -- @
 loadBlogPosts :: FilePath -> PencilApp [Page]
 loadBlogPosts fp = do
@@ -119,19 +111,46 @@
 
 type Tag = T.Text
 
--- | Given Pages with @tags@ variables in its environments, builds Pages that
--- contain in its environment the list of Pages that were tagged with that
--- particular tag.
+-- | Helper of 'buildTagPagesWith' defaulting to the variable name @posts@, and
+-- the tag index page file path @blog\/tags\/my-tag-name\/@.
+--
+-- @
+-- tagPages <- buildTagPages pages
+-- @
+--
 buildTagPages :: FilePath
-              -- ^ Partial to load for the Tag index pages
-              -> T.Text
-              -- ^ Variable name inserted into Tag index pages for the list of
-              -- Pages tagged with the specified tag
-              -> (Tag -> FilePath -> FilePath)
-              -- ^ Function to generate the URL of the tag pages.
               -> [Page]
               -> PencilApp (H.HashMap Tag Page)
-buildTagPages tagPageFp pagesVar fpf pages = do
+
+buildTagPages tagPageFp =
+  buildTagPagesWith
+    tagPageFp
+    "posts"
+    (\tag _ -> "blog/tags/" ++ T.unpack tag ++ "/")
+
+-- | Build the tag index pages.
+--
+-- Given blog post @Page@s with @tags@ variables in its PREAMBLE, builds @Page@s that
+-- contain in its environment the list of @Page@s that were tagged with that
+-- particular tag. Returns a map of tag of the tag index page.
+--
+-- @
+-- tagPages <- buildTagPagesWith
+--               "tag-list.html"
+--               "posts"
+--               (\tag _ -> "blog/tags/" ++ 'Data.Text.unpack' tag ++ "/")
+--               posts
+-- @
+buildTagPagesWith :: FilePath
+                  -- ^ Partial to load for the Tag index pages
+                  -> T.Text
+                  -- ^ Variable name inserted into Tag index pages for the list of
+                  -- Pages tagged with the specified tag
+                  -> (Tag -> FilePath -> FilePath)
+                  -- ^ Function to generate the URL of the tag pages.
+                  -> [Page]
+                  -> PencilApp (H.HashMap Tag Page)
+buildTagPagesWith tagPageFp pagesVar fpf pages = do
   env <- asks getEnv
 
   let tagMap = groupByElements "tags" pages
@@ -146,14 +165,15 @@
     H.empty
     (H.toList tagMap)
 
+-- | Inject the given tagging map into the given @Page@'s environment, as the
+-- @tags@ variable, whose value is a @VEnvList@.
 injectTagsEnv :: H.HashMap Tag Page -> Page -> Page
 injectTagsEnv tagMap page =
   -- Build up an env list of tag to that tag page's env. This is so that we can
   -- have access to the URL of the tag index pages.
-  let tagEnvList =
+  let envs =
         case H.lookup "tags" (getPageEnv page) of
           Just (VArray tags) ->
-            VEnvList $
               L.foldl'
                 (\acc envData ->
                   case envData of
@@ -163,11 +183,13 @@
                         _ -> acc
                     _ -> acc)
                 [] tags
-          _ -> VEnvList []
+          _ -> []
 
       -- Overwrite the VArray "tags" variable in the post Page with VEnvList of the
       -- loaded Tag index pages. This is so that when we render the blog posts, we
       -- have access to the URL of the Tag index.
-      env' = insertEnv "tags" tagEnvList (getPageEnv page)
+      env' = if null envs
+               then getPageEnv page
+               else insertEnv "tags" (VEnvList envs) (getPageEnv page)
   in setPageEnv env' page
 
diff --git a/src/Pencil/Internal.hs b/src/Pencil/Internal.hs
deleted file mode 100644
--- a/src/Pencil/Internal.hs
+++ /dev/null
@@ -1,932 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE FlexibleInstances #-}
-
-module Pencil.Internal where
-
-import Pencil.Internal.Env
-import Pencil.Internal.Parser
-
-import Control.Exception (tryJust)
-import Control.Monad (forM_, foldM, filterM, liftM)
-import Control.Monad.Except
-import Control.Monad.Reader
-import Data.Char (toLower)
-import Data.Default (Default)
-import Data.List.NonEmpty (NonEmpty(..)) -- Import the NonEmpty data constructor, (:|)
-import Data.Text.Encoding (encodeUtf8, decodeUtf8)
-import Data.Typeable (Typeable)
-import GHC.Generics (Generic)
-import GHC.IO.Exception (IOException(ioe_description, ioe_filename, ioe_type), IOErrorType(NoSuchThing))
-import Text.EditDistance (levenshteinDistance, defaultEditCosts)
-import Text.Sass.Options (defaultSassOptions)
-import Data.Hashable (Hashable)
-import qualified Data.HashMap.Strict as H
-import qualified Data.List as L
-import qualified Data.List.NonEmpty as NE
-import qualified Data.Maybe as M
-import qualified Data.Text as T
-import qualified Data.Text.IO as TIO
-import qualified Data.Yaml as A
-import qualified System.Directory as D
-import qualified System.FilePath as FP
-import qualified Text.Pandoc as P
-import qualified Text.Sass as Sass
-
--- | The main monad transformer stack for a Pencil application.
---
--- This unrolls to:
---
--- > PencilApp a = Config -> IO (Except PencilException a)
---
--- The @ExceptT@ monad allows us to catch "checked" exceptions; errors that we
--- know how to handle, in PencilException. Note that Unknown "unchecked"
--- exceptions can still go through IO.
---
-type PencilApp = ReaderT Config (ExceptT PencilException IO)
-
--- | The main @Config@ needed to build your website. Your app's @Config@ is
--- passed into the 'PencilApp' monad transformer.
---
--- Use 'defaultConfig' as a starting point, along with the config-modification
--- helpers such as 'setSourceDir'.
-data Config = Config
-  { configSourceDir :: FilePath
-  , configOutputDir :: FilePath
-  , configEnv :: Env
-  , configDisplayValue :: Value -> T.Text
-  , configSassOptions :: Sass.SassOptions
-  , configPandocReaderOptions :: P.ReaderOptions
-  , configPandocWriterOptions :: P.WriterOptions
-  }
-
--- 'Data.Default.Default' instance for 'Config'.
-instance Default Config where
-  def = defaultConfig
-
--- | This default @Config@ gives you everything you need to start.
---
--- Default values:
---
--- @
--- Config
---  { 'configSourceDir' = "site/"
---  , 'configOutputDir' = "out/"
---  , 'configEnv' = HashMap.empty
---  , 'configDisplayValue' = 'toText'
---  , 'configSassOptions' = Text.Sass.Options.defaultSassOptions
---  , 'configPandocReaderOptions' = Text.Pandoc.def
---  , 'configPandocWriterOptions' = Text.Pandoc.def { Text.Pandoc.writerHighlight = True }
---  , 'configDisplayValue = 'toText'
---  }
--- @
---
-defaultConfig :: Config
-defaultConfig = Config
-  { configSourceDir = "site/"
-  , configOutputDir = "out/"
-  , configEnv = H.empty
-  , configSassOptions = Text.Sass.Options.defaultSassOptions
-  , configPandocReaderOptions = P.def
-  , configPandocWriterOptions = P.def { P.writerHighlight = True }
-  , configDisplayValue = toText
-  }
-
--- | The directory path of your web page source files.
-getSourceDir :: Config -> FilePath
-getSourceDir = configSourceDir
-
--- | Sets the source directory of your web page source files.
-setSourceDir :: FilePath -> Config -> Config
-setSourceDir fp c = c { configSourceDir = fp }
-
--- | The directory path of your rendered web pages.
-getOutputDir :: Config -> FilePath
-getOutputDir = configOutputDir
-
--- | Sets the output directory of your rendered web pages.
-setOutputDir :: FilePath -> Config -> Config
-setOutputDir fp c = c { configOutputDir = fp }
-
--- | The environment of the @Config@, which is what the @PencilApp@ monad
--- transformer uses. This is where variables are set for rendering template
--- directives.
-getEnv :: Config -> Env
-getEnv = configEnv
-
--- | Sets the current environment. You may also want to look at 'withEnv' if you
--- want to 'render' things in a modified environment.
-setEnv :: Env -> Config -> Config
-setEnv env c = c { configEnv = env }
-
--- | Update the 'Env' inside the 'Config'.
-updateEnv :: (Env -> Env) -> Config -> Config
-updateEnv f c = c { configEnv = f (getEnv c) }
-
--- | The 'Sass.SassOptions' for rendering Sass/Scss files.
-getSassOptions :: Config -> Sass.SassOptions
-getSassOptions = configSassOptions
-
--- | Sets the 'Sass.SassOptions'.
-setSassOptions :: Sass.SassOptions -> Config -> Config
-setSassOptions env c = c { configSassOptions = env }
-
--- | The 'Text.Pandoc.ReaderOptions' for reading files that use Pandoc.
--- Supported formats by Pencil are: Markdown.
-getPandocReaderOptions :: Config -> P.ReaderOptions
-getPandocReaderOptions = configPandocReaderOptions
-
--- | Sets the 'Text.Pandoc.ReaderOptions'. For example, you may want to enable
--- some Pandoc extensions like 'Text.Pandoc.Extensions.Ext_literate_haskell':
---
--- @
--- setPandocReaderOptions
---   (Text.Pandoc.def { 'Text.Pandoc.Options.readerExtensions' = extensionsFromList [Ext_literate_haskell] })
---   config
--- @
-setPandocReaderOptions :: P.ReaderOptions -> Config -> Config
-setPandocReaderOptions o c = c { configPandocReaderOptions = o }
-
--- | The 'Text.Pandoc.WriterOptions' for rendering files that use Pandoc.
--- Supported formats by Pencil are: Markdown.
-getPandocWriterOptions :: Config -> P.WriterOptions
-getPandocWriterOptions = configPandocWriterOptions
-
--- | Sets the 'Text.Pandoc.WriterOptions'.
-setPandocWriterOptions :: P.WriterOptions -> Config -> Config
-setPandocWriterOptions o c = c { configPandocWriterOptions = o }
-
--- | The function that renders 'Value' to text.
-getDisplayValue :: Config -> Value -> T.Text
-getDisplayValue = configDisplayValue
-
--- | Sets the function that renders 'Value' to text. Overwrite this with your
--- own function if you would like to change how certain 'Value's are rendered
--- (e.g. 'VDateTime').
---
--- @
--- myRender :: Value -> T.Text
--- myRender ('VDateTime' dt) = 'T.pack' $ 'TF.formatTime' 'TF.defaultTimeLocale' "%e %B %Y" dt
--- myRender t = 'toText' t
---
--- ...
---
--- setDisplayValue myRender config
--- @
---
--- In the above example, we change the @VDateTime@ rendering to show @25
--- December 2017@. Leave everything else unchanged.
---
-setDisplayValue :: (Value -> T.Text) -> Config -> Config
-setDisplayValue f c = c { configDisplayValue = f }
-
--- | Run the Pencil app.
---
--- Note that this can throw a fatal exception.
-run :: PencilApp a -> Config -> IO ()
-run app config = do
-  e <- runExceptT $ runReaderT app config
-  case e of
-    Left err ->
-      case err of
-        FileNotFound mfp ->
-          case mfp of
-            Just fp -> do
-              e2 <- runExceptT $ runReaderT (mostSimilarFile fp) config
-              case e2 of
-                Left _ -> return ()
-                Right mBest ->
-                  case mBest of
-                    Just best -> putStrLn ("Maybe you mean this: " ++ best)
-                    Nothing -> return ()
-            Nothing -> return ()
-        VarNotInEnv var fp ->
-          putStrLn ("Variable ${" ++ T.unpack var ++ "}" ++ " not found in the environment when rendering file " ++ fp ++ ".")
-        _ -> return ()
-    Right _ -> return ()
-
--- | Given a file path, look at all file paths and find the one that seems most
--- similar.
-mostSimilarFile :: FilePath -> PencilApp (Maybe FilePath)
-mostSimilarFile fp = do
-  sitePrefix <- asks getSourceDir
-  fps <- listDir True ""
-  let fps' = map (sitePrefix ++) fps -- add site prefix for distance search
-  let costs = map (\f -> (f, levenshteinDistance defaultEditCosts fp f)) fps'
-  let sorted = L.sortBy (\(_, d1) (_, d2) -> compare d1 d2) costs
-  return $ fst <$> M.listToMaybe sorted
-
--- | Known Pencil errors that we know how to either recover from or quit
--- gracefully.
-data PencilException
-  = NotTextFile IOError
-  -- ^ Failed to read a file as a text file.
-  | FileNotFound (Maybe FilePath)
-  -- ^ File not found. We may or may not know the file we were looking for.
-  | VarNotInEnv T.Text FilePath
-  -- ^ Variable is not in the environment. Variable name, and file where the
-  -- variable was reference.
-  deriving (Typeable, Show)
-
--- | Enum for file types that can be parsed and converted by Pencil.
-data FileType = Html
-              | Markdown
-              | Css
-              | Sass
-              | Other
-  deriving (Eq, Generic)
-
--- | 'Hashable' instance of @FileType@.
-instance Hashable FileType
-
--- | A 'H.HashMap' of file extensions (e.g. @markdown@) to 'FileType'.
---
--- * 'Html': @html, htm@
--- * 'Markdown': @markdown, md@
--- * 'Css': @css@
--- * 'Sass': @sass, scss@
---
-fileTypeMap :: H.HashMap String FileType
-fileTypeMap = H.fromList
-  [ ("html", Html)
-  , ("htm", Html)
-  , ("markdown", Markdown)
-  , ("md", Markdown)
-  , ("css", Css)
-  , ("sass", Sass)
-  , ("scss", Sass)]
-
--- | Mapping of 'FileType' to the final converted format. Only contains
--- 'FileType's that Pencil will convert.
---
--- * 'Markdown': @html@
--- * 'Sass': @css@
---
-extensionMap :: H.HashMap FileType String
-extensionMap = H.fromList
-  [ (Markdown, "html")
-  , (Sass, "css")]
-
--- | Converts a 'FileType' into its converted webpage extension, if Pencil would
--- convert it (e.g. Markdown to HTML).
---
--- >>> toExtension Markdown
--- Just "html"
---
-toExtension :: FileType -> Maybe String
-toExtension ft = H.lookup ft extensionMap
-
--- | Takes a file path and returns the 'FileType', defaulting to 'Other' if it's
--- not a supported extension.
-fileType :: FilePath -> FileType
-fileType fp =
-  -- takeExtension returns ".markdown", so drop the "."
-  M.fromMaybe Other (H.lookup (map toLower (drop 1 (FP.takeExtension fp))) fileTypeMap)
-
--- | The Page is an important data type in Pencil. It contains the parsed
--- template of a file (e.g. of Markdown or HTML files). It may have template
--- directives (e.g. @${body}@) that has not yet been rendered, and an
--- environment loaded from the preamble section of the file. A Page also
--- contains 'pageFilePath', which is the output file path.
-data Page = Page
-  { pageNodes     :: [PNode]
-  , pageEnv       :: Env
-  , pageFilePath  :: FilePath
-  -- ^ The rendered output path of this page. Defaults to the input file path.
-  -- This file path is used to generate the self URL that is injected into the
-  -- environment.
-  } deriving (Eq, Show)
-
--- | Returns the 'Env' from a 'Page'.
-getPageEnv :: Page -> Env
-getPageEnv = pageEnv
-
--- | Sets the 'Env' in a 'Page'.
-setPageEnv :: Env -> Page -> Page
-setPageEnv env p = p { pageEnv = env }
-
--- | Applies the environment variables on the given pages.
---
--- The 'Structure' is expected to be ordered by inner-most content first (such
--- that the final, HTML structure layout is last in the list).
---
--- The returned Page contains the Nodes of the fully rendered page, the
--- fully-applied environment, and the URL of the last (inner-most) Page.
---
--- The variable application works by applying the outer environments down into
--- the inner environments, until it hits the lowest environment, in which the
--- page is rendered. Once done, this rendered content is saved as the @${body}@
--- variable for the parent structure, which is then applied, and so on.
---
--- As an example, there is the common scenario where we have a default layout
--- (e.g. @default.html@), with the full HTML structure, but no body. It has only
--- a @${body}@ template variable inside. This is the parent layout. There is a
--- child layout, the partial called "blog-post.html", which has HTML for
--- rendering a blog post, like usage of ${postTitle} and ${postDate}. Inside
--- this, there is another child layout, the blog post content itself, which
--- defines the variables @postTitle@ and @postDate@, and may renderer parent
--- variables such as @websiteTitle@.
---
--- > +--------------+
--- > |              | <--- default.html
--- > |              |      Defines websiteTitle
--- > |  +---------+ |
--- > |  |         |<+----- blog-post.html
--- > |  | +-----+ | |      Renders ${postTitle}, ${postDate}
--- > |  | |     | | |
--- > |  | |     | | |
--- > |  | |     |<+-+----- blog-article-content.markdown
--- > |  | |     | | |      Renders ${websiteTitle}
--- > |  | +-----+ | |      Defines postTitle, postDate
--- > |  +---------+ |
--- > +--------------+
---
--- In this case, we want to accumulate the environment variables, starting from
--- default.html, to blog-post.html, and the markdown file's variables. Combine
--- all of that, then render the blog post content. This content is then injected
--- into the parent's environment as a @${body}@ variable, for use in blog-post.html.
--- Now /that/ content is injected into the parent environment's @${body}@ variable,
--- which is then used to render the full-blown HTML page.
---
-apply :: Structure -> PencilApp Page
-apply pages = apply_ (NE.reverse pages)
-
--- | Apply @Structure@ and convert to @Page@.
---
--- It's simpler to implement if NonEmpty is ordered outer-structure first (e.g.
--- HTML layout).
-apply_ :: Structure -> PencilApp Page
-apply_ (Page nodes penv fp :| []) = do
-  env <- asks getEnv
-  let env' = merge penv env
-  nodes' <- evalNodes env' nodes `catchError` setVarNotInEnv fp
-  return $ Page nodes' env' fp
-apply_ (Page nodes penv _ :| (headp : rest)) = do
-  -- Modify the current env (in the ReaderT) with the one in the page (penv)
-  -- Then call apply_ on the inner Pages to accumulate the inner Page
-  -- environments.
-  Page nodesInner envInner fpInner <- local (\c -> setEnv (merge penv (getEnv c)) c)
-                                    (apply_ (headp :| rest))
-
-  -- Render the inner nodes, and inject into this environment's "body" var.
-  let env' = insertEnv "body" (VText (renderNodes nodesInner)) envInner
-
-  -- Evaluate this current Page's nodes with the accumualted environemnt of all
-  -- the inner Pages.
-  nodes' <- evalNodes env' nodes `catchError` setVarNotInEnv fpInner
-
-  -- Use inner-most Page's file path, as this will be the destination of the
-  -- accumluated, final, rendered page.
-  return $ Page nodes' env' fpInner
-
--- | Helper to inject a file path into a VarNotInEnv exception. Rethrow the
--- exception afterwards.
-setVarNotInEnv :: FilePath -> PencilException -> PencilApp a
-setVarNotInEnv fp (VarNotInEnv var _) = throwError $ VarNotInEnv var fp
-setVarNotInEnv _ e = throwError e
-
--- | Loads the given file as a text file. Throws an exception into the ExceptT
--- monad transformer if the file is not a text file.
-loadTextFile :: FilePath -> PencilApp T.Text
-loadTextFile fp = do
-  sitePrefix <- asks getSourceDir
-  -- Try to read the file. If it fails because it's not a text file, capture the
-  -- exception and convert it to a "checked" exception in the ExceptT stack via
-  -- 'throwError'.
-  eitherContent <- liftIO $ tryJust toPencilException (TIO.readFile (sitePrefix ++ fp))
-  case eitherContent of
-    Left e -> throwError e
-    Right a -> return a
-
--- | Converts the IOError to a known 'PencilException'.
---
--- How to test errors:
---
--- @
--- import Control.Exception
--- import qualified Data.Text.IO as TIO
---
--- (\e -> print (ioe_description (e :: IOError)) >> return "") `handle` (TIO.readFile "foo")
--- @
---
-toPencilException :: IOError -> Maybe PencilException
-toPencilException e
-  | isInvalidByteSequence e = Just (NotTextFile e)
-  | isNoSuchFile e = Just (FileNotFound (ioe_filename e))
-  | otherwise = Nothing
-
--- | Returns true if the IOError is an invalid byte sequence error. This
--- suggests that the file is a binary file.
-isInvalidByteSequence :: IOError -> Bool
-isInvalidByteSequence e = ioe_description e == "invalid byte sequence"
-
--- | Returns true if the IOError is due to missing file.
-isNoSuchFile :: IOError -> Bool
-isNoSuchFile e = ioe_type e == NoSuchThing
-
--- | Loads and parses the given file path. Converts 'Markdown' files to HTML,
--- compiles 'Sass' files into CSS, and leaves everything else alone.
-parseAndConvertTextFiles :: FilePath -> PencilApp (T.Text, [PNode])
-parseAndConvertTextFiles fp = do
-  content <- loadTextFile fp
-  content' <-
-    case fileType fp of
-      Markdown -> do
-        pandocReaderOptions <- asks getPandocReaderOptions
-        pandocWriterOptions <- asks getPandocWriterOptions
-        case P.readMarkdown pandocReaderOptions (T.unpack content) of
-          Left _ -> return content
-          Right pandoc -> return $ T.pack $ P.writeHtmlString pandocWriterOptions pandoc
-      Sass -> do
-        sassOptions <- asks getSassOptions
-        sitePrefix <- asks getSourceDir
-        -- Use compileFile so that SASS @import works
-        result <- liftIO $ Sass.compileFile (sitePrefix ++ fp) sassOptions
-        case result of
-          Left _ -> return content
-          Right byteStr -> return $ decodeUtf8 byteStr
-      _ -> return content
-  let nodes = case parseText content' of
-                Left _ -> []
-                Right n -> n
-  return (content', nodes)
-
--- | Evaluate the nodes in the given environment. Note that it returns an IO
--- because of @${partial(..)}@ calls that requires us to load a file.
-evalNodes :: Env -> [PNode] -> PencilApp [PNode]
-evalNodes _ [] = return []
-evalNodes env (PVar var : rest) = do
-  nodes <- evalNodes env rest
-  case H.lookup var env of
-    Nothing ->
-      -- Can't find var in env; throw exception.
-      throwError (VarNotInEnv var "")
-    Just envData -> do
-      displayValue <- asks getDisplayValue
-      return $ PText (displayValue envData) : nodes
-evalNodes env (PIf var nodes : rest) = do
-  rest' <- evalNodes env rest
-  case H.lookup var env of
-    Nothing ->
-      -- Can't find var in env; everything inside the if-statement is thrown away
-      return rest'
-    Just _ -> do
-      -- Render nodes inside the if-statement
-      nodes' <- evalNodes env nodes
-      return $ nodes' ++ rest'
-evalNodes env (PFor var nodes : rest) = do
-  rest' <- evalNodes env rest
-  case H.lookup var env of
-    Nothing ->
-      -- Can't find var in env; throw exception.
-      throwError (VarNotInEnv var "")
-    Just (VEnvList envs) -> do
-      -- Render the for nodes once for each given env, and append them together
-      forNodes <-
-        foldM
-          (\accNodes e -> do
-              nodes' <- evalNodes (H.union e env) nodes
-              return $ accNodes ++ nodes')
-          [] envs
-      return $ forNodes ++ rest'
-    -- Var is not an VEnvList; everything inside the for-statement is thrown away
-    Just _ -> return rest'
-evalNodes env (PPartial fp : rest) = do
-  (_, nodes) <- parseAndConvertTextFiles (T.unpack fp)
-  nodes' <- evalNodes env nodes
-  rest' <- evalNodes env rest
-  return $ nodes' ++ rest'
-evalNodes env (n : rest) = do
-  rest' <- evalNodes env rest
-  return $ n : rest'
-
--- | Sort given @Page@s by the specified ordering function.
-sortByVar :: T.Text
-          -- ^ Environment variable name.
-          -> (Value -> Value -> Ordering)
-          -- ^ Ordering function to compare Value against. If the variable is
-          -- not in the Env, the Page will be placed at the bottom of the order.
-          -> [Page]
-          -> [Page]
-sortByVar var ordering =
-  L.sortBy
-    (\(Page _ enva _) (Page _ envb _) ->
-      maybeOrdering ordering (H.lookup var enva) (H.lookup var envb))
-
--- | Filter by a variable's value in the environment.
-filterByVar :: Bool
-            -- ^ If true, include pages without the specified variable.
-            -> T.Text
-            -- ^ Environment variable name.
-            -> (Value -> Bool)
-            -> [Page]
-            -> [Page]
-filterByVar includeMissing var f =
-  L.filter
-   (\(Page _ env _) -> M.fromMaybe includeMissing (H.lookup var env >>= (Just . f)))
-
--- | Given a variable (whose value is assumed to be an array of VText) and list
--- of pages, group the pages by the VText found in the variable.
---
--- For example, say each Page has a variable "tags" that is a list of tags. The
--- first Page has a "tags" variable that is an VArray [VText "a"], and the
--- second Page has a "tags" variable that is an VArray [VText "a", VText "b"].
--- The final output would be a map fromList [("a", [page1, page2]), ("b",
--- [page2])].
-groupByElements :: T.Text
-                -- ^ Environment variable name.
-                -> [Page]
-                -> H.HashMap T.Text [Page]
-groupByElements var pages =
-  -- This outer fold takes the list of pages, and accumulates the giant HashMap.
-  L.foldl'
-    (\acc page@(Page _ env _) ->
-      let x = H.lookup var env
-      in case x of
-           Just (VArray values) ->
-             -- This fold takes each of the found values (each is a key in the
-             -- hash map), and adds the current page (from the outer fold) into
-             -- each of the key.
-             L.foldl'
-               (\hashmap envData ->
-                 case envData of
-                   -- Only insert Pages into the map if the variable is an VArray of
-                   -- VText. Alter the map to either (1) insert this current
-                   -- page into the existing list, or (2) create a new list (the
-                   -- key has never been seen) with just this page.
-                   VText val -> H.alter (\mv -> Just (page : M.fromMaybe [] mv)) val hashmap
-                   _ -> hashmap)
-               acc values
-           _ -> acc
-    )
-    H.empty
-    -- Reverse to keep ordering consistent inside hash map, since the fold
-    -- prepends into accumulated list.
-    (reverse pages)
-
--- | Loads file in given directory as 'Resource's.
-loadResources :: (FilePath -> FilePath)
-              -> Bool
-              -- ^ Recursive if @True@.
-              -> Bool
-              -- ^ Handle as pass-throughs (file copy) if @True@.
-              -> FilePath
-              -> PencilApp [Resource]
-loadResources fpf recursive pass dir = do
-  fps <- listDir recursive dir
-  if pass
-    then return $ map (\fp -> Passthrough fp fp) fps
-    else mapM (loadResource fpf) fps
-
--- | Lists files in given directory. The file paths returned is prefixed with the
--- given directory.
-listDir :: Bool
-        -- ^ Recursive if @True@.
-        -> FilePath
-        -> PencilApp [FilePath]
-listDir recursive dir = do
-  let dir' = FP.addTrailingPathSeparator dir
-  fps <- listDir_ recursive dir'
-  return $ map (dir' ++) fps
-
-listDir_ :: Bool -> FilePath -> PencilApp [FilePath]
-listDir_ recursive dir = do
-  sitePrefix <- asks getSourceDir
-  -- List files (just the filename, without the fp directory prefix)
-  listing <- liftIO $ D.listDirectory (sitePrefix ++ dir)
-  -- Filter only for files (we have to add the right directory prefixes to the
-  -- file check)
-  files <- liftIO $ filterM (\f -> D.doesFileExist (sitePrefix ++ dir ++ f)) listing
-  dirs <- liftIO $ filterM (\f -> D.doesDirectoryExist (sitePrefix ++ dir ++ f)) listing
-
-  innerFiles <- if recursive
-                  then mapM
-                         (\d -> do
-                           ff <- listDir_ recursive (dir ++ d ++ "/")
-                           -- Add the inner directory as a prefix
-                           return (map (\f -> d ++ "/" ++ f) ff))
-                         dirs
-                  else return []
-
-  return $ files ++ concat innerFiles
-
-----------------------------------------------------------------------
--- Environment modifications
-----------------------------------------------------------------------
-
--- | Merges two @Env@s together, biased towards the left-hand @Env@ on duplicates.
-merge :: Env -> Env -> Env
-merge = H.union
-
--- | Insert text into the given @Env@.
---
--- @
--- env <- asks getEnv
--- insertText "title" "My Awesome Website" env
--- @
-insertText :: T.Text
-           -- ^ Environment variable name.
-           -> T.Text
-           -- ^ Text to insert.
-           -> Env
-           -- ^ Environment to modify.
-           -> Env
-insertText var val = H.insert var (VText val)
-
--- | Insert @Page@s into the given @Env@.
---
--- @
--- posts <- 'Pencil.Blog.loadBlogPosts' "blog/"
--- env <- asks 'getEnv'
--- insertPages "posts" posts env
--- @
-insertPages :: T.Text
-            -- ^ Environment variable name.
-            -> [Page]
-            -- ^ @Page@s to insert.
-            -> Env
-            -- ^ Environment to modify.
-            -> Env
-insertPages var posts = H.insert var (VEnvList (map getPageEnv posts))
-
--- | Modify a variable in the given environment.
-updateEnvVal :: (Value -> Value)
-          -> T.Text
-          -- ^ Environment variable name.
-          -> Env
-          -> Env
-updateEnvVal = H.adjust
-
--- | Insert @Value@ into the given @Env@.
-insertEnv :: T.Text
-          -- ^ Environment variable name.
-          -> Value
-          -- ^ @Value@ to insert.
-          -> Env
-          -- ^ Environment to modify.
-          -> Env
-insertEnv = H.insert
-
--- | Convert known Aeson types into known Env types.
-maybeInsertIntoEnv :: Env -> T.Text -> A.Value -> Env
-maybeInsertIntoEnv env k v =
-  case toValue v of
-    Nothing -> env
-    Just d -> H.insert k d env
-
--- | Converts an Aeson Object to an Env.
-aesonToEnv :: A.Object -> Env
-aesonToEnv = H.foldlWithKey' maybeInsertIntoEnv H.empty
-
-
--- | Use @Resource@ to load and render files that don't need any manipulation
--- other than conversion (e.g. Sass to CSS), or for static files that you want
--- to copy as-is (e.g. binary files like images, or text files that require no
--- other processing).
---
--- Use 'passthrough', 'loadResource' and 'loadResources' to build a @Resource@
--- from a file.
---
--- In the example below, @robots.txt@ and everything in the @images/@ directory
--- will be rendered as-is.
---
--- @
--- passthrough "robots.txt" >> render
--- loadResources id True True "images/" >> render
--- @
---
-data Resource
-  = Single Page
-  | Passthrough FilePath FilePath
-  -- ^ in and out file paths
-
--- | Copy file from source to output dir.
-copyFile :: FilePath -> FilePath -> PencilApp ()
-copyFile fpIn fpOut = do
-  sitePrefix <- asks getSourceDir
-  outPrefix <- asks getOutputDir
-  liftIO $ D.createDirectoryIfMissing True (FP.takeDirectory (outPrefix ++ fpOut))
-  liftIO $ D.copyFile (sitePrefix ++ fpIn) (outPrefix ++ fpOut)
-
--- | Replaces the file path's extension with @.html@.
---
--- @
--- 'load' toHtml "about.markdown"
--- @
---
-toHtml :: FilePath -> FilePath
-toHtml fp = FP.dropExtension fp ++ ".html"
-
--- | Converts a file path into a directory name, dropping the extension.
--- Pages with a directory as its FilePath is rendered as an index file in that
--- directory. For example, the @pages/about.html@ is transformed into
--- @pages\/about\/@, which 'render' would render the 'Page' to the file path
--- @pages\/about\/index.html@.
---
-toDir :: FilePath -> FilePath
-toDir fp = FP.replaceFileName fp (FP.takeBaseName fp) ++ "/"
-
--- | Replaces the file path's extension with @.css@.
---
--- @
--- 'load' toCss "style.sass"
--- @
---
-toCss :: FilePath -> FilePath
-toCss fp = FP.dropExtension fp ++ ".css"
-
--- | Converts file path into the expected extensions. This means @.markdown@
--- become @.html@, @.sass@ becomes @.css@, and so forth. See 'extensionMap' for
--- conversion table.
---
--- @
--- -- Load everything inside the "assets/" folder, renaming converted files as
--- -- expected, and leaving everything else alone.
--- 'loadResources' toExpected True True "assets/"
--- @
-toExpected :: FilePath -> FilePath
-toExpected fp = maybe fp ((FP.dropExtension fp ++ ".") ++) (toExtension (fileType fp))
-
--- | Loads a file as a 'Resource'. Use this for binary files (e.g. images) and
--- for files without template directives. Regular files are still converted to
--- their web page formats (e.g. Markdown to HTML, SASS to CSS).
---
--- @
--- -- Loads and renders the image as-is. Underneath the hood
--- -- this is just a file copy.
--- loadResource id "images/profile.jpg" >> render
---
--- -- Loads and renders to about.index
--- loadResource toHtml "about.markdown" >> render
--- @
-loadResource :: (FilePath -> FilePath) -> FilePath -> PencilApp Resource
-loadResource fpf fp =
-  -- If we can load the Page as text file, convert to a Single. Otherwise if it
-  -- wasn't a text file, then return a Passthroguh resource. This is where we
-  -- finally handle the "checked" exception; that is, converting the Left error
-  -- case (NotTextFile) into a Right case (Passthrough).
-  liftM Single (load fpf fp)
-    `catchError` handle
-  -- 'handle' requires FlexibleContexts
-  where handle e = case e of
-                     NotTextFile _ -> return (Passthrough fp (fpf fp))
-                     _ -> throwError e
-
--- | Loads file as a pass-through. There is no content conversion, and template
--- directives are ignored. In essence this is a file copy.
---
--- @
--- passthrough "robots.txt" >> render
--- @
---
-passthrough :: FilePath -> PencilApp Resource
-passthrough fp = return $ Passthrough fp fp
-
--- | Loads a file into a Page, rendering the file (as determined by the file
--- extension) into the proper output format (e.g. Markdown rendered to
--- HTML, SCSS to CSS). Parses the template directives and preamble variables
--- into its environment. The 'Page''s 'pageFilePath' is determined by the given
--- function, which expects the original file path, and returns the designated file
--- path.
---
--- The Page's designated file path is calculated and stored in the Page's
--- environment in the variable @this.url@. This allows the template to use
--- @${this.url}@ to refer to the designated file path.
---
--- Example:
---
--- @
--- -- Loads index.markdown with the designated file path of index.html
--- load 'toHtml' "index.markdown"
---
--- -- Keep the file path as-is
--- load id "about.html"
--- @
---
-load :: (FilePath -> FilePath) -> FilePath -> PencilApp Page
-load fpf fp = do
-  (_, nodes) <- parseAndConvertTextFiles fp
-  let env = findEnv nodes
-  let fp' = "/" ++ fpf fp
-  let env' = H.insert "this.url" (VText (T.pack fp')) env
-  return $ Page nodes env' ("/" ++ fpf fp)
-
--- | Find preamble node, and load as an Env. If no preamble is found, return a
--- blank Env.
-findEnv :: [PNode] -> Env
-findEnv nodes =
-  aesonToEnv $ M.fromMaybe H.empty (findPreambleText nodes >>= (A.decode . encodeUtf8 . T.strip))
-
--- | Loads and renders file as CSS.
---
--- @
--- -- Load, convert and render as style.css.
--- renderCss "style.sass"
--- @
-renderCss :: FilePath -> PencilApp ()
-renderCss fp =
-  -- Drop .scss/sass extension and replace with .css.
-  load toCss fp >>= render
-
--- | A @Structure@ is a list of 'Page's, defining a nesting order. Think of them
--- like <https://en.wikipedia.org/wiki/Matryoshka_doll Russian nesting dolls>.
--- The first element defines the outer-most container, and subsequent elements
--- are /inside/ the previous element.
---
--- You commonly @Structure@s to insert a @Page@ containing content (e.g. a blog
--- post) into a container (e.g. a layout shared across all your web pages).
---
--- Build structures using 'structure', '<||' and '<|'.
---
--- @
--- layout <- load toHtml "layout.html"
--- index <- load toHtml "index.markdown"
--- about <- load toHtml "about.markdown"
--- render (layout <|| index)
--- render (layout <|| about)
--- @
---
--- In the example above we load a layout @Page@, which can be an HTML page
--- defining the outer structures like @\<html\>\<\/html\>@. Assuming @layout.html@
--- has the template directive @${body}@ (note that @body@ is a special variable
--- generated during structure-building), @layout <|| index@
--- tells 'render' that you want the rendered body of @index@ to be injected into
--- the @${body}@ directive inside of @layout@.
---
--- @Structure@s also control the closure of variables. Variables defined in a
--- @Page@s are accessible both by @Page@s above and below. This allows inner
--- @Page@s to define variables like the blog post title, which may be used in
--- the outer @Page@ to set the @\<title\>@ tag.
---
--- In this way, @Structure@ allows efficient @Page@ reuse. See the private
--- function 'Pencil.Internal.apply' to learn more about how @Structure@s are
--- evaluated.
---
--- Note that this differs than the @${partial(...)}@ directive, which has no
--- such variable closures. The partial directive is much simpler—think of them
--- as copy-and-pasting snippets from one file to another. The partial has has
--- the same environment as the parent context.
-type Structure = NonEmpty Page
-
--- | Creates a new @Structure@ from two @Page@s.
---
--- @
--- layout <- load toHtml "layout.html"
--- index <- load toHtml "index.markdown"
--- render (layout <|| index)
--- @
-(<||) :: Page -> Page -> Structure
-(<||) x y = y :| [x]
-
--- | Pushes @Page@ into @Structure@.
---
--- @
--- layout <- load toHtml "layout.html"
--- blogLayout <- load toHtml "blog-layout.html"
--- blogPost <- load toHtml "myblogpost.markdown"
--- render (layout <|| blogLayout <| blogPost)
--- @
-(<|) :: Structure -> Page -> Structure
-(<|) ne x = NE.cons x ne
-
--- | Converts a @Page@ into a @Structure@.
-structure :: Page -> Structure
-structure p = p :| []
-
--- | Runs the computation with the given environment. This is useful when you
--- want to render a 'Page' or 'Structure' with a modified environment.
---
--- @
--- withEnv ('insertText' "newvar" "newval" env) ('render' page)
--- @
---
-withEnv :: Env -> PencilApp a -> PencilApp a
-withEnv env = local (setEnv env)
-
--- | To render something is to create the output web pages, rendering template
--- directives into their final form using the current environment.
-class Render a where
-  -- | Renders 'a' as web page(s).
-  render :: a -> PencilApp ()
-
-instance Render Resource where
-  render (Single page) = render page
-  render (Passthrough fpIn fpOut) = copyFile fpIn fpOut
-
--- This requires FlexibleInstances.
-instance Render [Resource] where
-  render resources = forM_ resources render
-
--- This requires FlexibleInstances.
-instance Render Structure where
-  render s = apply s >>= render
-
-instance Render Page where
-  render (Page nodes _ fpOut) = do
-    outPrefix <- asks getOutputDir
-    let noFileName = FP.takeBaseName fpOut == ""
-    let fpOut' = outPrefix ++ if noFileName then fpOut ++ "index.html" else fpOut
-    liftIO $ D.createDirectoryIfMissing True (FP.takeDirectory fpOut')
-    liftIO $ TIO.writeFile fpOut' (renderNodes nodes)
-
diff --git a/src/Pencil/Internal/Env.hs b/src/Pencil/Internal/Env.hs
--- a/src/Pencil/Internal/Env.hs
+++ b/src/Pencil/Internal/Env.hs
@@ -9,12 +9,9 @@
 import qualified Data.Vector as V
 import qualified Data.Yaml as A
 
--- We should use that hack that allows ppl to extend this with their own types?
--- Example: we want a "tags" type for a list of blog post tags
--- https://two-wrongs.com/dynamic-dispatch-in-haskell-how-to-make-code-extendable
+-- | Represents the data types found in an environment.
 --
--- Represents the data types found in an environment. This includes at least
--- Data.Aeson Value type
+-- This includes at least Data.Aeson Value type
 -- (https://hackage.haskell.org/package/aeson-1.2.3.0/docs/Data-Aeson.html#t:Value),
 -- plus other useful ones.
 data Value =
@@ -26,8 +23,10 @@
   | VEnvList [Env]
   deriving (Eq, Show)
 
+-- | Environment map of variables to 'Value's.
 type Env = H.HashMap T.Text Value
 
+-- | Converts an Aeson @Value@ to a Pencil 'Value'.
 toValue :: A.Value -> Maybe Value
 toValue A.Null = Just VNull
 toValue (A.Bool b) = Just $ VBool b
@@ -82,6 +81,8 @@
 dateOrdering (VDateTime a) (VDateTime b) = compare b a
 dateOrdering _ _ = EQ
 
+-- | Returns true if the given @Value@ is a @VArray@ that contains the given
+-- string.
 arrayContainsString :: T.Text -> Value -> Bool
 arrayContainsString t (VArray arr) =
   any (\d -> case d of
diff --git a/src/Pencil/Internal/Pencil.hs b/src/Pencil/Internal/Pencil.hs
new file mode 100644
--- /dev/null
+++ b/src/Pencil/Internal/Pencil.hs
@@ -0,0 +1,935 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+
+module Pencil.Internal.Pencil where
+
+import Pencil.Internal.Env
+import Pencil.Internal.Parser
+
+import Control.Exception (tryJust)
+import Control.Monad (forM_, foldM, filterM, liftM)
+import Control.Monad.Except
+import Control.Monad.Reader
+import Data.Char (toLower)
+import Data.Default (Default)
+import Data.List.NonEmpty (NonEmpty(..)) -- Import the NonEmpty data constructor, (:|)
+import Data.Text.Encoding (encodeUtf8, decodeUtf8)
+import Data.Typeable (Typeable)
+import GHC.Generics (Generic)
+import GHC.IO.Exception (IOException(ioe_description, ioe_filename, ioe_type), IOErrorType(NoSuchThing))
+import Text.EditDistance (levenshteinDistance, defaultEditCosts)
+import Text.Sass.Options (defaultSassOptions)
+import Data.Hashable (Hashable)
+import qualified Data.HashMap.Strict as H
+import qualified Data.List as L
+import qualified Data.List.NonEmpty as NE
+import qualified Data.Maybe as M
+import qualified Data.Text as T
+import qualified Data.Text.IO as TIO
+import qualified Data.Yaml as A
+import qualified System.Directory as D
+import qualified System.FilePath as FP
+import qualified Text.Pandoc as P
+import qualified Text.Sass as Sass
+
+-- | The main monad transformer stack for a Pencil application.
+--
+-- This unrolls to:
+--
+-- > PencilApp a = Config -> IO (Except PencilException a)
+--
+-- The @ExceptT@ monad allows us to catch "checked" exceptions; errors that we
+-- know how to handle, in PencilException. Note that Unknown "unchecked"
+-- exceptions can still go through IO.
+--
+type PencilApp = ReaderT Config (ExceptT PencilException IO)
+
+-- | The main @Config@ needed to build your website. Your app's @Config@ is
+-- passed into the 'PencilApp' monad transformer.
+--
+-- Use 'defaultConfig' as a starting point, along with the config-modification
+-- helpers such as 'setSourceDir'.
+data Config = Config
+  { configSourceDir :: FilePath
+  , configOutputDir :: FilePath
+  , configEnv :: Env
+  , configDisplayValue :: Value -> T.Text
+  , configSassOptions :: Sass.SassOptions
+  , configPandocReaderOptions :: P.ReaderOptions
+  , configPandocWriterOptions :: P.WriterOptions
+  }
+
+-- 'Data.Default.Default' instance for 'Config'.
+instance Default Config where
+  def = defaultConfig
+
+-- | This default @Config@ gives you everything you need to start.
+--
+-- Default values:
+--
+-- @
+-- Config
+--  { 'configSourceDir' = "site/"
+--  , 'configOutputDir' = "out/"
+--  , 'configEnv' = HashMap.empty
+--  , 'configDisplayValue' = 'toText'
+--  , 'configSassOptions' = Text.Sass.Options.defaultSassOptions
+--  , 'configPandocReaderOptions' = Text.Pandoc.def
+--  , 'configPandocWriterOptions' = Text.Pandoc.def { Text.Pandoc.writerHighlight = True }
+--  , 'configDisplayValue = 'toText'
+--  }
+-- @
+--
+defaultConfig :: Config
+defaultConfig = Config
+  { configSourceDir = "site/"
+  , configOutputDir = "out/"
+  , configEnv = H.empty
+  , configSassOptions = Text.Sass.Options.defaultSassOptions
+  , configPandocReaderOptions = P.def
+  , configPandocWriterOptions = P.def { P.writerHighlight = True }
+  , configDisplayValue = toText
+  }
+
+-- | The directory path of your web page source files.
+getSourceDir :: Config -> FilePath
+getSourceDir = configSourceDir
+
+-- | Sets the source directory of your web page source files.
+setSourceDir :: FilePath -> Config -> Config
+setSourceDir fp c = c { configSourceDir = fp }
+
+-- | The directory path of your rendered web pages.
+getOutputDir :: Config -> FilePath
+getOutputDir = configOutputDir
+
+-- | Sets the output directory of your rendered web pages.
+setOutputDir :: FilePath -> Config -> Config
+setOutputDir fp c = c { configOutputDir = fp }
+
+-- | The environment of the @Config@, which is what the @PencilApp@ monad
+-- transformer uses. This is where variables are set for rendering template
+-- directives.
+getEnv :: Config -> Env
+getEnv = configEnv
+
+-- | Sets the current environment. You may also want to look at 'withEnv' if you
+-- want to 'render' things in a modified environment.
+setEnv :: Env -> Config -> Config
+setEnv env c = c { configEnv = env }
+
+-- | Update the 'Env' inside the 'Config'.
+updateEnv :: (Env -> Env) -> Config -> Config
+updateEnv f c = c { configEnv = f (getEnv c) }
+
+-- | The 'Sass.SassOptions' for rendering Sass/Scss files.
+getSassOptions :: Config -> Sass.SassOptions
+getSassOptions = configSassOptions
+
+-- | Sets the 'Sass.SassOptions'.
+setSassOptions :: Sass.SassOptions -> Config -> Config
+setSassOptions env c = c { configSassOptions = env }
+
+-- | The 'Text.Pandoc.ReaderOptions' for reading files that use Pandoc.
+-- Supported formats by Pencil are: Markdown.
+getPandocReaderOptions :: Config -> P.ReaderOptions
+getPandocReaderOptions = configPandocReaderOptions
+
+-- | Sets the 'Text.Pandoc.ReaderOptions'. For example, you may want to enable
+-- some Pandoc extensions like 'Text.Pandoc.Extensions.Ext_literate_haskell':
+--
+-- @
+-- setPandocReaderOptions
+--   (Text.Pandoc.def { 'Text.Pandoc.Options.readerExtensions' = extensionsFromList [Ext_literate_haskell] })
+--   config
+-- @
+setPandocReaderOptions :: P.ReaderOptions -> Config -> Config
+setPandocReaderOptions o c = c { configPandocReaderOptions = o }
+
+-- | The 'Text.Pandoc.WriterOptions' for rendering files that use Pandoc.
+-- Supported formats by Pencil are: Markdown.
+getPandocWriterOptions :: Config -> P.WriterOptions
+getPandocWriterOptions = configPandocWriterOptions
+
+-- | Sets the 'Text.Pandoc.WriterOptions'.
+setPandocWriterOptions :: P.WriterOptions -> Config -> Config
+setPandocWriterOptions o c = c { configPandocWriterOptions = o }
+
+-- | The function that renders 'Value' to text.
+getDisplayValue :: Config -> Value -> T.Text
+getDisplayValue = configDisplayValue
+
+-- | Sets the function that renders 'Value' to text. Overwrite this with your
+-- own function if you would like to change how certain 'Value's are rendered
+-- (e.g. 'VDateTime').
+--
+-- @
+-- myRender :: Value -> T.Text
+-- myRender ('VDateTime' dt) = 'T.pack' $ 'TF.formatTime' 'TF.defaultTimeLocale' "%e %B %Y" dt
+-- myRender t = 'toText' t
+--
+-- ...
+--
+-- setDisplayValue myRender config
+-- @
+--
+-- In the above example, we change the @VDateTime@ rendering to show @25
+-- December 2017@. Leave everything else unchanged.
+--
+setDisplayValue :: (Value -> T.Text) -> Config -> Config
+setDisplayValue f c = c { configDisplayValue = f }
+
+-- | Run the Pencil app.
+--
+-- Note that this can throw a fatal exception.
+run :: PencilApp a -> Config -> IO ()
+run app config = do
+  e <- runExceptT $ runReaderT app config
+  case e of
+    Left err ->
+      case err of
+        FileNotFound mfp ->
+          case mfp of
+            Just fp -> do
+              e2 <- runExceptT $ runReaderT (mostSimilarFile fp) config
+              case e2 of
+                Left _ -> return ()
+                Right mBest ->
+                  case mBest of
+                    Just best -> putStrLn ("Maybe you mean this: " ++ best)
+                    Nothing -> return ()
+            Nothing -> return ()
+        VarNotInEnv var fp ->
+          putStrLn ("Variable ${" ++ T.unpack var ++ "}" ++ " not found in the environment when rendering file " ++ fp ++ ".")
+        _ -> return ()
+    Right _ -> return ()
+
+-- | Given a file path, look at all file paths and find the one that seems most
+-- similar.
+mostSimilarFile :: FilePath -> PencilApp (Maybe FilePath)
+mostSimilarFile fp = do
+  sitePrefix <- asks getSourceDir
+  fps <- listDir True ""
+  let fps' = map (sitePrefix ++) fps -- add site prefix for distance search
+  let costs = map (\f -> (f, levenshteinDistance defaultEditCosts fp f)) fps'
+  let sorted = L.sortBy (\(_, d1) (_, d2) -> compare d1 d2) costs
+  return $ fst <$> M.listToMaybe sorted
+
+-- | Known Pencil errors that we know how to either recover from or quit
+-- gracefully.
+data PencilException
+  = NotTextFile IOError
+  -- ^ Failed to read a file as a text file.
+  | FileNotFound (Maybe FilePath)
+  -- ^ File not found. We may or may not know the file we were looking for.
+  | VarNotInEnv T.Text FilePath
+  -- ^ Variable is not in the environment. Variable name, and file where the
+  -- variable was reference.
+  deriving (Typeable, Show)
+
+-- | Enum for file types that can be parsed and converted by Pencil.
+data FileType = Html
+              | Markdown
+              | Css
+              | Sass
+              | Other
+  deriving (Eq, Generic)
+
+-- | 'Hashable' instance of @FileType@.
+instance Hashable FileType
+
+-- | A 'H.HashMap' of file extensions (e.g. @markdown@) to 'FileType'.
+--
+-- * 'Html': @html, htm@
+-- * 'Markdown': @markdown, md@
+-- * 'Css': @css@
+-- * 'Sass': @sass, scss@
+--
+fileTypeMap :: H.HashMap String FileType
+fileTypeMap = H.fromList
+  [ ("html", Html)
+  , ("htm", Html)
+  , ("markdown", Markdown)
+  , ("md", Markdown)
+  , ("css", Css)
+  , ("sass", Sass)
+  , ("scss", Sass)]
+
+-- | Mapping of 'FileType' to the final converted format. Only contains
+-- 'FileType's that Pencil will convert.
+--
+-- * 'Markdown': @html@
+-- * 'Sass': @css@
+--
+extensionMap :: H.HashMap FileType String
+extensionMap = H.fromList
+  [ (Markdown, "html")
+  , (Sass, "css")]
+
+-- | Converts a 'FileType' into its converted webpage extension, if Pencil would
+-- convert it (e.g. Markdown to HTML).
+--
+-- >>> toExtension Markdown
+-- Just "html"
+--
+toExtension :: FileType -> Maybe String
+toExtension ft = H.lookup ft extensionMap
+
+-- | Takes a file path and returns the 'FileType', defaulting to 'Other' if it's
+-- not a supported extension.
+fileType :: FilePath -> FileType
+fileType fp =
+  -- takeExtension returns ".markdown", so drop the "."
+  M.fromMaybe Other (H.lookup (map toLower (drop 1 (FP.takeExtension fp))) fileTypeMap)
+
+-- | The Page is an important data type in Pencil. It contains the parsed
+-- template of a file (e.g. of Markdown or HTML files). It may have template
+-- directives (e.g. @${body}@) that has not yet been rendered, and an
+-- environment loaded from the preamble section of the file. A Page also
+-- contains 'pageFilePath', which is the output file path.
+data Page = Page
+  { pageNodes     :: [PNode]
+  , pageEnv       :: Env
+  , pageFilePath  :: FilePath
+  -- ^ The rendered output path of this page. Defaults to the input file path.
+  -- This file path is used to generate the self URL that is injected into the
+  -- environment.
+  } deriving (Eq, Show)
+
+-- | Returns the 'Env' from a 'Page'.
+getPageEnv :: Page -> Env
+getPageEnv = pageEnv
+
+-- | Sets the 'Env' in a 'Page'.
+setPageEnv :: Env -> Page -> Page
+setPageEnv env p = p { pageEnv = env }
+
+-- | Applies the environment variables on the given pages.
+--
+-- The 'Structure' is expected to be ordered by inner-most content first (such
+-- that the final, HTML structure layout is last in the list).
+--
+-- The returned Page contains the Nodes of the fully rendered page, the
+-- fully-applied environment, and the URL of the last (inner-most) Page.
+--
+-- The variable application works by applying the outer environments down into
+-- the inner environments, until it hits the lowest environment, in which the
+-- page is rendered. Once done, this rendered content is saved as the @${body}@
+-- variable for the parent structure, which is then applied, and so on.
+--
+-- As an example, there is the common scenario where we have a default layout
+-- (e.g. @default.html@), with the full HTML structure, but no body. It has only
+-- a @${body}@ template variable inside. This is the parent layout. There is a
+-- child layout, the partial called "blog-post.html", which has HTML for
+-- rendering a blog post, like usage of ${postTitle} and ${postDate}. Inside
+-- this, there is another child layout, the blog post content itself, which
+-- defines the variables @postTitle@ and @postDate@, and may renderer parent
+-- variables such as @websiteTitle@.
+--
+-- > +--------------+
+-- > |              | <--- default.html
+-- > |              |      Defines websiteTitle
+-- > |  +---------+ |
+-- > |  |         |<+----- blog-post.html
+-- > |  | +-----+ | |      Renders ${postTitle}, ${postDate}
+-- > |  | |     | | |
+-- > |  | |     | | |
+-- > |  | |     |<+-+----- blog-article-content.markdown
+-- > |  | |     | | |      Renders ${websiteTitle}
+-- > |  | +-----+ | |      Defines postTitle, postDate
+-- > |  +---------+ |
+-- > +--------------+
+--
+-- In this case, we want to accumulate the environment variables, starting from
+-- default.html, to blog-post.html, and the markdown file's variables. Combine
+-- all of that, then render the blog post content. This content is then injected
+-- into the parent's environment as a @${body}@ variable, for use in blog-post.html.
+-- Now /that/ content is injected into the parent environment's @${body}@ variable,
+-- which is then used to render the full-blown HTML page.
+--
+apply :: Structure -> PencilApp Page
+apply pages = apply_ (NE.reverse pages)
+
+-- | Apply @Structure@ and convert to @Page@.
+--
+-- It's simpler to implement if NonEmpty is ordered outer-structure first (e.g.
+-- HTML layout).
+apply_ :: Structure -> PencilApp Page
+apply_ (Page nodes penv fp :| []) = do
+  env <- asks getEnv
+  let env' = merge penv env
+  nodes' <- evalNodes env' nodes `catchError` setVarNotInEnv fp
+  return $ Page nodes' env' fp
+apply_ (Page nodes penv _ :| (headp : rest)) = do
+  -- Modify the current env (in the ReaderT) with the one in the page (penv)
+  -- Then call apply_ on the inner Pages to accumulate the inner Page
+  -- environments.
+  Page nodesInner envInner fpInner <- local (\c -> setEnv (merge penv (getEnv c)) c)
+                                    (apply_ (headp :| rest))
+
+  -- Render the inner nodes, and inject into this environment's "body" var.
+  let env' = insertEnv "body" (VText (renderNodes nodesInner)) envInner
+
+  -- Evaluate this current Page's nodes with the accumualted environemnt of all
+  -- the inner Pages.
+  nodes' <- evalNodes env' nodes `catchError` setVarNotInEnv fpInner
+
+  -- Use inner-most Page's file path, as this will be the destination of the
+  -- accumluated, final, rendered page.
+  return $ Page nodes' env' fpInner
+
+-- | Helper to inject a file path into a VarNotInEnv exception. Rethrow the
+-- exception afterwards.
+setVarNotInEnv :: FilePath -> PencilException -> PencilApp a
+setVarNotInEnv fp (VarNotInEnv var _) = throwError $ VarNotInEnv var fp
+setVarNotInEnv _ e = throwError e
+
+-- | Loads the given file as a text file. Throws an exception into the ExceptT
+-- monad transformer if the file is not a text file.
+loadTextFile :: FilePath -> PencilApp T.Text
+loadTextFile fp = do
+  sitePrefix <- asks getSourceDir
+  -- Try to read the file. If it fails because it's not a text file, capture the
+  -- exception and convert it to a "checked" exception in the ExceptT stack via
+  -- 'throwError'.
+  eitherContent <- liftIO $ tryJust toPencilException (TIO.readFile (sitePrefix ++ fp))
+  case eitherContent of
+    Left e -> throwError e
+    Right a -> return a
+
+-- | Converts the IOError to a known 'PencilException'.
+--
+-- How to test errors:
+--
+-- @
+-- import Control.Exception
+-- import qualified Data.Text.IO as TIO
+--
+-- (\e -> print (ioe_description (e :: IOError)) >> return "") `handle` (TIO.readFile "foo")
+-- @
+--
+toPencilException :: IOError -> Maybe PencilException
+toPencilException e
+  | isInvalidByteSequence e = Just (NotTextFile e)
+  | isNoSuchFile e = Just (FileNotFound (ioe_filename e))
+  | otherwise = Nothing
+
+-- | Returns true if the IOError is an invalid byte sequence error. This
+-- suggests that the file is a binary file.
+isInvalidByteSequence :: IOError -> Bool
+isInvalidByteSequence e = ioe_description e == "invalid byte sequence"
+
+-- | Returns true if the IOError is due to missing file.
+isNoSuchFile :: IOError -> Bool
+isNoSuchFile e = ioe_type e == NoSuchThing
+
+-- | Loads and parses the given file path. Converts 'Markdown' files to HTML,
+-- compiles 'Sass' files into CSS, and leaves everything else alone.
+parseAndConvertTextFiles :: FilePath -> PencilApp (T.Text, [PNode])
+parseAndConvertTextFiles fp = do
+  content <- loadTextFile fp
+  content' <-
+    case fileType fp of
+      Markdown -> do
+        pandocReaderOptions <- asks getPandocReaderOptions
+        pandocWriterOptions <- asks getPandocWriterOptions
+        case P.readMarkdown pandocReaderOptions (T.unpack content) of
+          Left _ -> return content
+          Right pandoc -> return $ T.pack $ P.writeHtmlString pandocWriterOptions pandoc
+      Sass -> do
+        sassOptions <- asks getSassOptions
+        sitePrefix <- asks getSourceDir
+        -- Use compileFile so that SASS @import works
+        result <- liftIO $ Sass.compileFile (sitePrefix ++ fp) sassOptions
+        case result of
+          Left _ -> return content
+          Right byteStr -> return $ decodeUtf8 byteStr
+      _ -> return content
+  let nodes = case parseText content' of
+                Left _ -> []
+                Right n -> n
+  return (content', nodes)
+
+-- | Evaluate the nodes in the given environment. Note that it returns an IO
+-- because of @${partial(..)}@ calls that requires us to load a file.
+evalNodes :: Env -> [PNode] -> PencilApp [PNode]
+evalNodes _ [] = return []
+evalNodes env (PVar var : rest) = do
+  nodes <- evalNodes env rest
+  case H.lookup var env of
+    Nothing ->
+      -- Can't find var in env. Skip over it for now and we'll just render the
+      -- directive to help user debug missing variables. Later on we'll do some
+      -- nice error handling w/o crashing the system (throw warnings instead of
+      -- errors).
+      return $ PVar var : nodes
+    Just envData -> do
+      displayValue <- asks getDisplayValue
+      return $ PText (displayValue envData) : nodes
+evalNodes env (PIf var nodes : rest) = do
+  rest' <- evalNodes env rest
+  case H.lookup var env of
+    Nothing ->
+      -- Can't find var in env; everything inside the if-statement is thrown away
+      return rest'
+    Just _ -> do
+      -- Render nodes inside the if-statement
+      nodes' <- evalNodes env nodes
+      return $ nodes' ++ rest'
+evalNodes env (PFor var nodes : rest) = do
+  rest' <- evalNodes env rest
+  case H.lookup var env of
+    Nothing ->
+      -- Can't find var in env; everything inside the for-statement is throw away
+      return rest'
+    Just (VEnvList envs) -> do
+      -- Render the for nodes once for each given env, and append them together
+      forNodes <-
+        foldM
+          (\accNodes e -> do
+              nodes' <- evalNodes (H.union e env) nodes
+              return $ accNodes ++ nodes')
+          [] envs
+      return $ forNodes ++ rest'
+    -- Var is not an VEnvList; everything inside the for-statement is thrown away
+    Just _ -> return rest'
+evalNodes env (PPartial fp : rest) = do
+  (_, nodes) <- parseAndConvertTextFiles (T.unpack fp)
+  nodes' <- evalNodes env nodes
+  rest' <- evalNodes env rest
+  return $ nodes' ++ rest'
+evalNodes env (n : rest) = do
+  rest' <- evalNodes env rest
+  return $ n : rest'
+
+-- | Sort given @Page@s by the specified ordering function.
+sortByVar :: T.Text
+          -- ^ Environment variable name.
+          -> (Value -> Value -> Ordering)
+          -- ^ Ordering function to compare Value against. If the variable is
+          -- not in the Env, the Page will be placed at the bottom of the order.
+          -> [Page]
+          -> [Page]
+sortByVar var ordering =
+  L.sortBy
+    (\(Page _ enva _) (Page _ envb _) ->
+      maybeOrdering ordering (H.lookup var enva) (H.lookup var envb))
+
+-- | Filter by a variable's value in the environment.
+filterByVar :: Bool
+            -- ^ If true, include pages without the specified variable.
+            -> T.Text
+            -- ^ Environment variable name.
+            -> (Value -> Bool)
+            -> [Page]
+            -> [Page]
+filterByVar includeMissing var f =
+  L.filter
+   (\(Page _ env _) -> M.fromMaybe includeMissing (H.lookup var env >>= (Just . f)))
+
+-- | Given a variable (whose value is assumed to be an array of VText) and list
+-- of pages, group the pages by the VText found in the variable.
+--
+-- For example, say each Page has a variable "tags" that is a list of tags. The
+-- first Page has a "tags" variable that is an VArray [VText "a"], and the
+-- second Page has a "tags" variable that is an VArray [VText "a", VText "b"].
+-- The final output would be a map fromList [("a", [page1, page2]), ("b",
+-- [page2])].
+groupByElements :: T.Text
+                -- ^ Environment variable name.
+                -> [Page]
+                -> H.HashMap T.Text [Page]
+groupByElements var pages =
+  -- This outer fold takes the list of pages, and accumulates the giant HashMap.
+  L.foldl'
+    (\acc page@(Page _ env _) ->
+      let x = H.lookup var env
+      in case x of
+           Just (VArray values) ->
+             -- This fold takes each of the found values (each is a key in the
+             -- hash map), and adds the current page (from the outer fold) into
+             -- each of the key.
+             L.foldl'
+               (\hashmap envData ->
+                 case envData of
+                   -- Only insert Pages into the map if the variable is an VArray of
+                   -- VText. Alter the map to either (1) insert this current
+                   -- page into the existing list, or (2) create a new list (the
+                   -- key has never been seen) with just this page.
+                   VText val -> H.alter (\mv -> Just (page : M.fromMaybe [] mv)) val hashmap
+                   _ -> hashmap)
+               acc values
+           _ -> acc
+    )
+    H.empty
+    -- Reverse to keep ordering consistent inside hash map, since the fold
+    -- prepends into accumulated list.
+    (reverse pages)
+
+-- | Loads file in given directory as 'Resource's.
+loadResources :: (FilePath -> FilePath)
+              -> Bool
+              -- ^ Recursive if @True@.
+              -> Bool
+              -- ^ Handle as pass-throughs (file copy) if @True@.
+              -> FilePath
+              -> PencilApp [Resource]
+loadResources fpf recursive pass dir = do
+  fps <- listDir recursive dir
+  if pass
+    then return $ map (\fp -> Passthrough fp fp) fps
+    else mapM (loadResource fpf) fps
+
+-- | Lists files in given directory. The file paths returned is prefixed with the
+-- given directory.
+listDir :: Bool
+        -- ^ Recursive if @True@.
+        -> FilePath
+        -> PencilApp [FilePath]
+listDir recursive dir = do
+  let dir' = FP.addTrailingPathSeparator dir
+  fps <- listDir_ recursive dir'
+  return $ map (dir' ++) fps
+
+-- | 'listDir' helper.
+listDir_ :: Bool -> FilePath -> PencilApp [FilePath]
+listDir_ recursive dir = do
+  sitePrefix <- asks getSourceDir
+  -- List files (just the filename, without the fp directory prefix)
+  listing <- liftIO $ D.listDirectory (sitePrefix ++ dir)
+  -- Filter only for files (we have to add the right directory prefixes to the
+  -- file check)
+  files <- liftIO $ filterM (\f -> D.doesFileExist (sitePrefix ++ dir ++ f)) listing
+  dirs <- liftIO $ filterM (\f -> D.doesDirectoryExist (sitePrefix ++ dir ++ f)) listing
+
+  innerFiles <- if recursive
+                  then mapM
+                         (\d -> do
+                           ff <- listDir_ recursive (dir ++ d ++ "/")
+                           -- Add the inner directory as a prefix
+                           return (map (\f -> d ++ "/" ++ f) ff))
+                         dirs
+                  else return []
+
+  return $ files ++ concat innerFiles
+
+----------------------------------------------------------------------
+-- Environment modifications
+----------------------------------------------------------------------
+
+-- | Merges two @Env@s together, biased towards the left-hand @Env@ on duplicates.
+merge :: Env -> Env -> Env
+merge = H.union
+
+-- | Insert text into the given @Env@.
+--
+-- @
+-- env <- asks getEnv
+-- insertText "title" "My Awesome Website" env
+-- @
+insertText :: T.Text
+           -- ^ Environment variable name.
+           -> T.Text
+           -- ^ Text to insert.
+           -> Env
+           -- ^ Environment to modify.
+           -> Env
+insertText var val = H.insert var (VText val)
+
+-- | Insert @Page@s into the given @Env@.
+--
+-- @
+-- posts <- 'Pencil.Blog.loadBlogPosts' "blog/"
+-- env <- asks 'getEnv'
+-- insertPages "posts" posts env
+-- @
+insertPages :: T.Text
+            -- ^ Environment variable name.
+            -> [Page]
+            -- ^ @Page@s to insert.
+            -> Env
+            -- ^ Environment to modify.
+            -> Env
+insertPages var posts = H.insert var (VEnvList (map getPageEnv posts))
+
+-- | Modify a variable in the given environment.
+updateEnvVal :: (Value -> Value)
+          -> T.Text
+          -- ^ Environment variable name.
+          -> Env
+          -> Env
+updateEnvVal = H.adjust
+
+-- | Insert @Value@ into the given @Env@.
+insertEnv :: T.Text
+          -- ^ Environment variable name.
+          -> Value
+          -- ^ @Value@ to insert.
+          -> Env
+          -- ^ Environment to modify.
+          -> Env
+insertEnv = H.insert
+
+-- | Convert known Aeson types into known Env types.
+maybeInsertIntoEnv :: Env -> T.Text -> A.Value -> Env
+maybeInsertIntoEnv env k v =
+  case toValue v of
+    Nothing -> env
+    Just d -> H.insert k d env
+
+-- | Converts an Aeson Object to an Env.
+aesonToEnv :: A.Object -> Env
+aesonToEnv = H.foldlWithKey' maybeInsertIntoEnv H.empty
+
+
+-- | Use @Resource@ to load and render files that don't need any manipulation
+-- other than conversion (e.g. Sass to CSS), or for static files that you want
+-- to copy as-is (e.g. binary files like images, or text files that require no
+-- other processing).
+--
+-- Use 'passthrough', 'loadResource' and 'loadResources' to build a @Resource@
+-- from a file.
+--
+-- In the example below, @robots.txt@ and everything in the @images/@ directory
+-- will be rendered as-is.
+--
+-- @
+-- passthrough "robots.txt" >> render
+-- loadResources id True True "images/" >> render
+-- @
+--
+data Resource
+  = Single Page
+  | Passthrough FilePath FilePath
+  -- ^ in and out file paths
+
+-- | Copy file from source to output dir.
+copyFile :: FilePath -> FilePath -> PencilApp ()
+copyFile fpIn fpOut = do
+  sitePrefix <- asks getSourceDir
+  outPrefix <- asks getOutputDir
+  liftIO $ D.createDirectoryIfMissing True (FP.takeDirectory (outPrefix ++ fpOut))
+  liftIO $ D.copyFile (sitePrefix ++ fpIn) (outPrefix ++ fpOut)
+
+-- | Replaces the file path's extension with @.html@.
+--
+-- @
+-- 'load' toHtml "about.markdown"
+-- @
+--
+toHtml :: FilePath -> FilePath
+toHtml fp = FP.dropExtension fp ++ ".html"
+
+-- | Converts a file path into a directory name, dropping the extension.
+-- Pages with a directory as its FilePath is rendered as an index file in that
+-- directory. For example, the @pages/about.html@ is transformed into
+-- @pages\/about\/@, which 'render' would render the 'Page' to the file path
+-- @pages\/about\/index.html@.
+--
+toDir :: FilePath -> FilePath
+toDir fp = FP.replaceFileName fp (FP.takeBaseName fp) ++ "/"
+
+-- | Replaces the file path's extension with @.css@.
+--
+-- @
+-- 'load' toCss "style.sass"
+-- @
+--
+toCss :: FilePath -> FilePath
+toCss fp = FP.dropExtension fp ++ ".css"
+
+-- | Converts file path into the expected extensions. This means @.markdown@
+-- become @.html@, @.sass@ becomes @.css@, and so forth. See 'extensionMap' for
+-- conversion table.
+--
+-- @
+-- -- Load everything inside the "assets/" folder, renaming converted files as
+-- -- expected, and leaving everything else alone.
+-- 'loadResources' toExpected True True "assets/"
+-- @
+toExpected :: FilePath -> FilePath
+toExpected fp = maybe fp ((FP.dropExtension fp ++ ".") ++) (toExtension (fileType fp))
+
+-- | Loads a file as a 'Resource'. Use this for binary files (e.g. images) and
+-- for files without template directives. Regular files are still converted to
+-- their web page formats (e.g. Markdown to HTML, SASS to CSS).
+--
+-- @
+-- -- Loads and renders the image as-is. Underneath the hood
+-- -- this is just a file copy.
+-- loadResource id "images/profile.jpg" >> render
+--
+-- -- Loads and renders to about.index
+-- loadResource toHtml "about.markdown" >> render
+-- @
+loadResource :: (FilePath -> FilePath) -> FilePath -> PencilApp Resource
+loadResource fpf fp =
+  -- If we can load the Page as text file, convert to a Single. Otherwise if it
+  -- wasn't a text file, then return a Passthroguh resource. This is where we
+  -- finally handle the "checked" exception; that is, converting the Left error
+  -- case (NotTextFile) into a Right case (Passthrough).
+  liftM Single (load fpf fp)
+    `catchError` handle
+  -- 'handle' requires FlexibleContexts
+  where handle e = case e of
+                     NotTextFile _ -> return (Passthrough fp (fpf fp))
+                     _ -> throwError e
+
+-- | Loads file as a pass-through. There is no content conversion, and template
+-- directives are ignored. In essence this is a file copy.
+--
+-- @
+-- passthrough "robots.txt" >> render
+-- @
+--
+passthrough :: FilePath -> PencilApp Resource
+passthrough fp = return $ Passthrough fp fp
+
+-- | Loads a file into a Page, rendering the file (as determined by the file
+-- extension) into the proper output format (e.g. Markdown rendered to
+-- HTML, SCSS to CSS). Parses the template directives and preamble variables
+-- into its environment. The 'Page''s 'pageFilePath' is determined by the given
+-- function, which expects the original file path, and returns the designated file
+-- path.
+--
+-- The Page's designated file path is calculated and stored in the Page's
+-- environment in the variable @this.url@. This allows the template to use
+-- @${this.url}@ to refer to the designated file path.
+--
+-- Example:
+--
+-- @
+-- -- Loads index.markdown with the designated file path of index.html
+-- load 'toHtml' "index.markdown"
+--
+-- -- Keep the file path as-is
+-- load id "about.html"
+-- @
+--
+load :: (FilePath -> FilePath) -> FilePath -> PencilApp Page
+load fpf fp = do
+  (_, nodes) <- parseAndConvertTextFiles fp
+  let env = findEnv nodes
+  let fp' = "/" ++ fpf fp
+  let env' = H.insert "this.url" (VText (T.pack fp')) env
+  return $ Page nodes env' ("/" ++ fpf fp)
+
+-- | Find preamble node, and load as an Env. If no preamble is found, return a
+-- blank Env.
+findEnv :: [PNode] -> Env
+findEnv nodes =
+  aesonToEnv $ M.fromMaybe H.empty (findPreambleText nodes >>= (A.decode . encodeUtf8 . T.strip))
+
+-- | Loads and renders file as CSS.
+--
+-- @
+-- -- Load, convert and render as style.css.
+-- renderCss "style.sass"
+-- @
+renderCss :: FilePath -> PencilApp ()
+renderCss fp =
+  -- Drop .scss/sass extension and replace with .css.
+  load toCss fp >>= render
+
+-- | A @Structure@ is a list of 'Page's, defining a nesting order. Think of them
+-- like <https://en.wikipedia.org/wiki/Matryoshka_doll Russian nesting dolls>.
+-- The first element defines the outer-most container, and subsequent elements
+-- are /inside/ the previous element.
+--
+-- You commonly @Structure@s to insert a @Page@ containing content (e.g. a blog
+-- post) into a container (e.g. a layout shared across all your web pages).
+--
+-- Build structures using 'structure', '<||' and '<|'.
+--
+-- @
+-- layout <- load toHtml "layout.html"
+-- index <- load toHtml "index.markdown"
+-- about <- load toHtml "about.markdown"
+-- render (layout <|| index)
+-- render (layout <|| about)
+-- @
+--
+-- In the example above we load a layout @Page@, which can be an HTML page
+-- defining the outer structures like @\<html\>\<\/html\>@. Assuming @layout.html@
+-- has the template directive @${body}@ (note that @body@ is a special variable
+-- generated during structure-building), @layout <|| index@
+-- tells 'render' that you want the rendered body of @index@ to be injected into
+-- the @${body}@ directive inside of @layout@.
+--
+-- @Structure@s also control the closure of variables. Variables defined in a
+-- @Page@s are accessible both by @Page@s above and below. This allows inner
+-- @Page@s to define variables like the blog post title, which may be used in
+-- the outer @Page@ to set the @\<title\>@ tag.
+--
+-- In this way, @Structure@ allows efficient @Page@ reuse. See the private
+-- function 'apply' to learn more about how @Structure@s are
+-- evaluated.
+--
+-- Note that this differs than the @${partial(...)}@ directive, which has no
+-- such variable closures. The partial directive is much simpler—think of them
+-- as copy-and-pasting snippets from one file to another. The partial has has
+-- the same environment as the parent context.
+type Structure = NonEmpty Page
+
+-- | Creates a new @Structure@ from two @Page@s.
+--
+-- @
+-- layout <- load toHtml "layout.html"
+-- index <- load toHtml "index.markdown"
+-- render (layout <|| index)
+-- @
+(<||) :: Page -> Page -> Structure
+(<||) x y = y :| [x]
+
+-- | Pushes @Page@ into @Structure@.
+--
+-- @
+-- layout <- load toHtml "layout.html"
+-- blogLayout <- load toHtml "blog-layout.html"
+-- blogPost <- load toHtml "myblogpost.markdown"
+-- render (layout <|| blogLayout <| blogPost)
+-- @
+(<|) :: Structure -> Page -> Structure
+(<|) ne x = NE.cons x ne
+
+-- | Converts a @Page@ into a @Structure@.
+structure :: Page -> Structure
+structure p = p :| []
+
+-- | Runs the computation with the given environment. This is useful when you
+-- want to render a 'Page' or 'Structure' with a modified environment.
+--
+-- @
+-- withEnv ('insertText' "newvar" "newval" env) ('render' page)
+-- @
+--
+withEnv :: Env -> PencilApp a -> PencilApp a
+withEnv env = local (setEnv env)
+
+-- | To render something is to create the output web pages, rendering template
+-- directives into their final form using the current environment.
+class Render a where
+  -- | Renders 'a' as web page(s).
+  render :: a -> PencilApp ()
+
+instance Render Resource where
+  render (Single page) = render page
+  render (Passthrough fpIn fpOut) = copyFile fpIn fpOut
+
+-- This requires FlexibleInstances.
+instance Render Structure where
+  render s = apply s >>= render
+
+instance Render Page where
+  render (Page nodes _ fpOut) = do
+    outPrefix <- asks getOutputDir
+    let noFileName = FP.takeBaseName fpOut == ""
+    let fpOut' = outPrefix ++ if noFileName then fpOut ++ "index.html" else fpOut
+    liftIO $ D.createDirectoryIfMissing True (FP.takeDirectory fpOut')
+    liftIO $ TIO.writeFile fpOut' (renderNodes nodes)
+
+-- This requires FlexibleInstances.
+instance Render r => Render [r] where
+  render rs = forM_ rs render
