diff --git a/hakyll.cabal b/hakyll.cabal
--- a/hakyll.cabal
+++ b/hakyll.cabal
@@ -1,5 +1,5 @@
 Name:    hakyll
-Version: 3.2.6.2
+Version: 3.2.7.0
 
 Synopsis: A static website compiler library
 Description:
@@ -179,3 +179,21 @@
     text        >= 0.11   && < 0.12,
     time        >= 1.1    && < 1.5,
     unix        >= 2.4    && < 2.6
+
+  Other-modules:
+    Hakyll.Web.Util.Html.Tests
+    Hakyll.Web.Urls.Relativize.Tests
+    Hakyll.Web.Urls.Tests
+    Hakyll.Web.Template.Tests
+    Hakyll.Web.Page.Metadata.Tests
+    Hakyll.Web.Page.Tests
+    Hakyll.Core.Compiler.Tests
+    Hakyll.Core.Identifier.Tests
+    Hakyll.Core.Util.Arrow.Tests
+    Hakyll.Core.Util.String.Tests
+    Hakyll.Core.UnixFilter.Tests
+    Hakyll.Core.Routes.Tests
+    Hakyll.Core.Store.Tests
+    Hakyll.Core.Rules.Tests
+    Hakyll.Core.DependencyAnalyzer.Tests
+    TestSuite.Util
diff --git a/src/Hakyll/Web/Page/Read.hs b/src/Hakyll/Web/Page/Read.hs
--- a/src/Hakyll/Web/Page/Read.hs
+++ b/src/Hakyll/Web/Page/Read.hs
@@ -24,7 +24,7 @@
 metadataField :: Parser (String, String)
 metadataField = do
     key <- manyTill alphaNum $ char ':'
-    skipMany1 inlineSpace
+    skipMany1 inlineSpace <?> "space followed by metadata for: " ++ key
     value <- manyTill anyChar newline
     trailing' <- many trailing
     return (key, trim $ value ++ concat trailing')
diff --git a/src/Hakyll/Web/Pandoc.hs b/src/Hakyll/Web/Pandoc.hs
--- a/src/Hakyll/Web/Pandoc.hs
+++ b/src/Hakyll/Web/Pandoc.hs
@@ -55,6 +55,7 @@
         readPandocWith state {stateLiterateHaskell = True} t id'
     Markdown          -> readMarkdown state
     Rst               -> readRST state
+    Textile           -> readTextile state
     t                 -> error $
         "Hakyll.Web.readPandocWith: I don't know how to read a file of the " ++
         "type " ++ show t ++ fromMaybe "" (fmap ((" for: " ++) . show) id')
diff --git a/src/Hakyll/Web/Pandoc/FileType.hs b/src/Hakyll/Web/Pandoc/FileType.hs
--- a/src/Hakyll/Web/Pandoc/FileType.hs
+++ b/src/Hakyll/Web/Pandoc/FileType.hs
@@ -25,6 +25,7 @@
     | OrgMode
     | PlainText
     | Rst
+    | Textile
     deriving (Eq, Ord, Show, Read)
 
 -- | Get the file type for a certain file. The type is determined by extension.
@@ -48,6 +49,7 @@
     fileType' ".rst"      = Rst
     fileType' ".tex"      = LaTeX
     fileType' ".text"     = PlainText
+    fileType' ".textile"  = Textile
     fileType' ".txt"      = PlainText
     fileType' _           = Binary  -- Treat unknown files as binary
 
diff --git a/tests/Hakyll/Core/Compiler/Tests.hs b/tests/Hakyll/Core/Compiler/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hakyll/Core/Compiler/Tests.hs
@@ -0,0 +1,36 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Hakyll.Core.Compiler.Tests
+    ( tests
+    ) where
+
+import qualified Data.Map as M
+
+import Test.Framework (Test)
+import Test.Framework.Providers.HUnit (testCase)
+import qualified Test.HUnit as H
+
+import Hakyll.Core.Compiler
+import Hakyll.Core.Resource.Provider.Dummy
+import Hakyll.Core.Util.Arrow
+import TestSuite.Util
+
+tests :: [Test]
+tests =
+    [ testCase "byExtension" byExtensionTest
+    ]
+
+byExtensionTest :: H.Assertion
+byExtensionTest = do
+    provider <- dummyResourceProvider $ M.empty
+    txt      <- runCompilerJobTest compiler "foo.txt" provider uni
+    css      <- runCompilerJobTest compiler "bar.css" provider uni
+    html     <- runCompilerJobTest compiler "qux.html" provider uni
+    H.assertEqual "byExtension" "txt" txt
+    H.assertEqual "byExtension" "css" css
+    H.assertEqual "byExtension" "unknown" html
+  where
+    uni      = ["foo.txt", "bar.css", "qux.html"]
+    compiler = byExtension (constA ("unknown" :: String))
+        [ (".txt", constA "txt")
+        , (".css", constA "css")
+        ]
diff --git a/tests/Hakyll/Core/DependencyAnalyzer/Tests.hs b/tests/Hakyll/Core/DependencyAnalyzer/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hakyll/Core/DependencyAnalyzer/Tests.hs
@@ -0,0 +1,70 @@
+module Hakyll.Core.DependencyAnalyzer.Tests where
+
+import Control.Arrow (second)
+import qualified Data.Set as S
+import Data.Monoid (mempty)
+
+import Test.Framework
+import Test.Framework.Providers.HUnit
+import Test.HUnit hiding (Test)
+
+import Hakyll.Core.DirectedGraph
+import Hakyll.Core.DependencyAnalyzer
+
+tests :: [Test]
+tests =
+    [ testCase "step [1]" step1
+    , testCase "step [2]" step2
+    ]
+
+step1 :: Assertion
+step1 = Just (S.fromList [1, 2, 5, 6, 7, 8, 9]) @?=
+    stepAll (makeDependencyAnalyzer graph isOutOfDate prev)
+  where
+    node = curry $ second S.fromList
+
+    graph = fromList
+        [ node (8 :: Int) [2, 4, 6]
+        , node 2 [4, 3]
+        , node 4 [3]
+        , node 6 [4]
+        , node 3 []
+        , node 9 [5]
+        , node 5 [7]
+        , node 1 [7]
+        , node 7 []
+        ]
+
+    prev = fromList
+        [ node 8 [2, 4, 6]
+        , node 2 [4, 3]
+        , node 4 [3]
+        , node 6 [4]
+        , node 3 []
+        , node 9 [5]
+        , node 5 [7]
+        , node 1 [7]
+        , node 7 [8]
+        ]
+
+    isOutOfDate = (`elem` [5, 2, 6])
+
+step2 :: Assertion
+step2 = Nothing @?= stepAll (makeDependencyAnalyzer graph isOutOfDate mempty)
+  where
+    node = curry $ second S.fromList
+
+    -- Cycle: 4 -> 7 -> 5 -> 9 -> 4
+    graph = fromList
+        [ node (1 :: Int) [6]
+        , node 2 [3]
+        , node 3 []
+        , node 4 [1, 7, 8]
+        , node 5 [9]
+        , node 6 [3]
+        , node 7 [5]
+        , node 8 [2]
+        , node 9 [4]
+        ]
+
+    isOutOfDate = const True
diff --git a/tests/Hakyll/Core/Identifier/Tests.hs b/tests/Hakyll/Core/Identifier/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hakyll/Core/Identifier/Tests.hs
@@ -0,0 +1,46 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Hakyll.Core.Identifier.Tests
+    ( tests
+    ) where
+
+import Test.Framework
+import Test.HUnit hiding (Test)
+
+import Hakyll.Core.Identifier
+import Hakyll.Core.Identifier.Pattern
+import TestSuite.Util
+
+tests :: [Test]
+tests = concat
+    [ captureTests
+    , matchesTests
+    ]
+
+captureTests :: [Test]
+captureTests = fromAssertions "capture"
+    [ Just ["bar"]              @=? capture "foo/**" "foo/bar"
+    , Just ["foo/bar"]          @=? capture "**" "foo/bar"
+    , Nothing                   @=? capture "*" "foo/bar"
+    , Just []                   @=? capture "foo" "foo"
+    , Just ["foo"]              @=? capture "*/bar" "foo/bar"
+    , Just ["foo/bar"]          @=? capture "**/qux" "foo/bar/qux"
+    , Just ["foo/bar", "qux"]   @=? capture "**/*" "foo/bar/qux"
+    , Just ["foo", "bar/qux"]   @=? capture "*/**" "foo/bar/qux"
+    , Just ["foo"]              @=? capture "*.html" "foo.html"
+    , Nothing                   @=? capture "*.html" "foo/bar.html"
+    , Just ["foo/bar"]          @=? capture "**.html" "foo/bar.html"
+    , Just ["foo/bar", "wut"]   @=? capture "**/qux/*" "foo/bar/qux/wut"
+    , Just ["lol", "fun/large"] @=? capture "*cat/**.jpg" "lolcat/fun/large.jpg"
+    , Just []                   @=? capture "\\*.jpg" "*.jpg"
+    , Nothing                   @=? capture "\\*.jpg" "foo.jpg"
+    ]
+
+matchesTests :: [Test]
+matchesTests = fromAssertions "matches"
+    [ True  @=? matches (list ["foo.markdown"]) "foo.markdown"
+    , False @=? matches (list ["foo"]) (Identifier (Just "foo") "foo")
+    , True  @=? matches (regex "^foo/[^x]*$") "foo/bar"
+    , False @=? matches (regex "^foo/[^x]*$") "foo/barx"
+    , True  @=? matches (complement "foo.markdown") "bar.markdown"
+    , False @=? matches (complement "foo.markdown") "foo.markdown"
+    ]
diff --git a/tests/Hakyll/Core/Routes/Tests.hs b/tests/Hakyll/Core/Routes/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hakyll/Core/Routes/Tests.hs
@@ -0,0 +1,24 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Hakyll.Core.Routes.Tests
+    ( tests
+    ) where
+
+import Test.Framework
+import Test.HUnit hiding (Test)
+
+import Hakyll.Core.Routes
+import TestSuite.Util
+
+tests :: [Test]
+tests = fromAssertions "runRoutes"
+    [ Just "foo.html" @=? runRoutes (setExtension "html") "foo"
+    , Just "foo.html" @=? runRoutes (setExtension ".html") "foo"
+    , Just "foo.html" @=? runRoutes (setExtension "html") "foo.markdown"
+    , Just "foo.html" @=? runRoutes (setExtension ".html") "foo.markdown"
+
+    , Just "tags/bar.xml" @=?
+        runRoutes (gsubRoute "rss/" (const "")) "tags/rss/bar.xml"
+    , Just "tags/bar.xml" @=?
+        runRoutes (gsubRoute "rss/" (const "") `composeRoutes`
+            setExtension "xml") "tags/rss/bar"
+    ]
diff --git a/tests/Hakyll/Core/Rules/Tests.hs b/tests/Hakyll/Core/Rules/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hakyll/Core/Rules/Tests.hs
@@ -0,0 +1,67 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
+module Hakyll.Core.Rules.Tests
+    ( tests
+    ) where
+
+import qualified Data.Map as M
+import qualified Data.Set as S
+
+import Test.Framework
+import Test.Framework.Providers.HUnit
+import Test.HUnit hiding (Test)
+
+import Hakyll.Core.Rules
+import Hakyll.Core.Rules.Internal
+import Hakyll.Core.Identifier
+import Hakyll.Core.Routes
+import Hakyll.Core.Compiler
+import Hakyll.Core.Resource.Provider
+import Hakyll.Core.Resource.Provider.Dummy
+import Hakyll.Web.Page
+
+tests :: [Test]
+tests =
+    [ testCase "runRules" rulesTest
+    ]
+
+-- | Main test
+--
+rulesTest :: Assertion
+rulesTest = do
+    p <- provider
+    let ruleSet = runRules rules p
+    assert $ expected == S.fromList (map fst (rulesCompilers ruleSet))
+  where
+    expected = S.fromList
+        [ Identifier Nothing "posts/a-post.markdown"
+        , Identifier Nothing "posts/some-other-post.markdown"
+        , Identifier (Just "raw") "posts/a-post.markdown"
+        , Identifier (Just "raw") "posts/some-other-post.markdown"
+        ]
+
+-- | Dummy resource provider
+--
+provider :: IO ResourceProvider
+provider = dummyResourceProvider $ M.fromList $ map (flip (,) "No content")
+    [ "posts/a-post.markdown"
+    , "posts/some-other-post.markdown"
+    ]
+
+-- | Example rules
+--
+rules :: Rules
+rules = do
+    -- Compile some posts
+    match "posts/*" $ do
+        route $ setExtension "html"
+        compile pageCompiler
+
+    -- Compile them, raw
+    group "raw" $ do
+        route idRoute
+        match "posts/*" $ do
+            route $ setExtension "html"
+            compile getResourceString
+
+    return ()
diff --git a/tests/Hakyll/Core/Store/Tests.hs b/tests/Hakyll/Core/Store/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hakyll/Core/Store/Tests.hs
@@ -0,0 +1,67 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Hakyll.Core.Store.Tests
+    ( tests
+    ) where
+
+import Control.Applicative ((<$>))
+import Control.Monad (replicateM)
+
+import Test.Framework
+import Test.Framework.Providers.QuickCheck2
+import Test.Framework.Providers.HUnit
+import Test.QuickCheck
+import Test.QuickCheck.Monadic
+import qualified Test.HUnit as H
+
+import Hakyll.Core.Identifier
+import Hakyll.Core.Store
+import TestSuite.Util
+
+tests :: [Test]
+tests =
+    [ testProperty "simple storeGet . storeSet" simpleSetGet
+    , testProperty "persistent storeGet . storeSet" persistentSetGet
+    , testCase     "WrongType storeGet . storeSet" wrongType
+    ]
+
+simpleSetGet :: Property
+simpleSetGet = monadicIO $ do
+    identifier <- parseIdentifier . unFileName <$> pick arbitrary
+    FileName name <- pick arbitrary
+    value <- pick arbitrary
+    store <- run $ makeStoreTest
+    run $ storeSet store name identifier (value :: String)
+    value' <- run $ storeGet store name identifier
+    assert $ Found value == value'
+
+persistentSetGet :: Property
+persistentSetGet = monadicIO $ do
+    identifier <- parseIdentifier . unFileName <$> pick arbitrary
+    FileName name <- pick arbitrary
+    value <- pick arbitrary
+    store1 <- run $ makeStoreTest
+    run $ storeSet store1 name identifier (value :: String)
+    -- Now Create another store from the same dir to test persistence
+    store2 <- run $ makeStoreTest
+    value' <- run $ storeGet store2 name identifier
+    assert $ Found value == value'
+
+wrongType :: H.Assertion
+wrongType = do
+    store <- makeStoreTest
+    -- Store a string and try to fetch an int
+    storeSet store "foo" "bar" ("qux" :: String)
+    value <- storeGet store "foo" "bar" :: IO (StoreGet Int)
+    H.assert $ case value of WrongType _ _ -> True
+                             _             -> False
+
+newtype FileName = FileName {unFileName :: String}
+                 deriving (Show)
+
+instance Arbitrary FileName where
+    arbitrary = do
+        length' <- choose (5, 100)
+        str <- replicateM length' $ elements cs
+        return $ FileName str
+      where
+        cs = ['a' .. 'z'] ++ ['A' .. 'Z'] ++ ['0' .. '9'] ++ ".- "
diff --git a/tests/Hakyll/Core/UnixFilter/Tests.hs b/tests/Hakyll/Core/UnixFilter/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hakyll/Core/UnixFilter/Tests.hs
@@ -0,0 +1,50 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Hakyll.Core.UnixFilter.Tests
+    where
+
+import Control.Arrow ((>>>))
+import qualified Data.Map as M
+
+import Test.Framework (Test)
+import Test.Framework.Providers.HUnit (testCase)
+import qualified Test.HUnit as H
+import qualified Data.Text.Lazy as TL
+import qualified Data.Text.Lazy.Encoding as TL
+
+import Hakyll.Core.Compiler
+import Hakyll.Core.Resource.Provider.Dummy
+import Hakyll.Core.UnixFilter
+import TestSuite.Util
+
+tests :: [Test]
+tests =
+    [ testCase "unixFilter rev" unixFilterRev
+    ]
+
+unixFilterRev :: H.Assertion
+unixFilterRev = do
+    provider <- dummyResourceProvider $ M.singleton "foo" $
+        TL.encodeUtf8 $ TL.pack text
+    output <- runCompilerJobTest compiler "foo" provider ["foo"]
+    H.assert $ rev text == lines output
+  where
+    compiler = getResource >>> getResourceString >>> unixFilter "rev" []
+    rev = map reverse . lines
+
+text :: String
+text = unlines
+    [ "Статья 18"
+    , ""
+    , "Каждый человек имеет право на свободу мысли, совести и религии; это"
+    , "право включает свободу менять свою религию или убеждения и свободу"
+    , "исповедовать свою религию или убеждения как единолично, так и сообща с"
+    , "другими, публичным или частным порядком в учении, богослужении и"
+    , "выполнении религиозных и ритуальных обрядов."
+    , ""
+    , "Статья 19"
+    , ""
+    , "Каждый человек имеет право на свободу убеждений и на свободное выражение"
+    , "их; это право включает свободу беспрепятственно придерживаться своих"
+    , "убеждений и свободу искать, получать и распространять информацию и идеи"
+    , "любыми средствами и независимо от государственных границ."
+    ]
diff --git a/tests/Hakyll/Core/Util/Arrow/Tests.hs b/tests/Hakyll/Core/Util/Arrow/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hakyll/Core/Util/Arrow/Tests.hs
@@ -0,0 +1,14 @@
+module Hakyll.Core.Util.Arrow.Tests
+    ( tests
+    ) where
+
+import Test.Framework (Test)
+import Test.HUnit ((@=?))
+
+import Hakyll.Core.Util.Arrow
+import TestSuite.Util
+
+tests :: [Test]
+tests = fromAssertions "sequenceA"
+    [ [8, 20, 1] @=? sequenceA [(+ 4), (* 5), signum] (4 :: Int)
+    ]
diff --git a/tests/Hakyll/Core/Util/String/Tests.hs b/tests/Hakyll/Core/Util/String/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hakyll/Core/Util/String/Tests.hs
@@ -0,0 +1,28 @@
+module Hakyll.Core.Util.String.Tests
+    ( tests
+    ) where
+
+import Test.Framework (Test)
+import Test.HUnit ((@=?))
+
+import Hakyll.Core.Util.String
+import TestSuite.Util
+
+tests :: [Test]
+tests = concat
+    [ fromAssertions "trim"
+        [ "foo" @=? trim " foo\n\t "
+        ]
+
+    , fromAssertions "replaceAll"
+        [ "32 & 131" @=? replaceAll "0x[0-9]+" (show . readInt) "0x20 & 0x83"
+        ]
+
+    , fromAssertions "splitAll"
+        [ ["λ", "∀x.x", "hi"] @=? splitAll ", *" "λ, ∀x.x,  hi"
+        ]
+    ]
+
+  where
+    readInt :: String -> Int
+    readInt = read
diff --git a/tests/Hakyll/Web/Page/Metadata/Tests.hs b/tests/Hakyll/Web/Page/Metadata/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hakyll/Web/Page/Metadata/Tests.hs
@@ -0,0 +1,74 @@
+module Hakyll.Web.Page.Metadata.Tests
+    ( tests
+    ) where
+
+import Test.Framework
+import Test.HUnit hiding (Test)
+
+import qualified Data.Map as M
+import Data.Monoid (mempty)
+import Data.Char (toLower)
+
+import Hakyll.Web.Page
+import Hakyll.Web.Page.Metadata
+import TestSuite.Util
+
+tests :: [Test]
+tests = concat $
+    [ fromAssertions "getField"
+        [ "bar" @=? getField "foo" (Page (M.singleton "foo" "bar") "body\n")
+        , ""    @=? getField "foo" (Page M.empty "body")
+        ]
+
+    , fromAssertions "getFieldMaybe"
+        [ Just "bar" @=? getFieldMaybe "foo" (Page (M.singleton "foo" "bar") "")
+        , Nothing    @=? getFieldMaybe "foo" (Page M.empty "body")
+        ]
+
+    , fromAssertions "setField"
+        [ (Page (M.singleton "bar" "foo") "") @=? setField "bar" "foo" mempty
+        , (Page (M.singleton "bar" "foo") "") @=?
+            setField "bar" "foo" (Page (M.singleton "bar" "qux") "")
+        ]
+
+    , fromAssertions "trySetField"
+        [ (Page (M.singleton "bar" "foo") "") @=? trySetField "bar" "foo" mempty
+        , (Page (M.singleton "bar" "qux") "") @=?
+            trySetField "bar" "foo" (Page (M.singleton "bar" "qux") "")
+        ]
+
+    , fromAssertions "setFieldA"
+        [ (Page (M.singleton "bar" "foo") "") @=?
+            setFieldA "bar" (map toLower) (mempty, "FOO")
+        ]
+
+    , fromAssertions "renderDateField"
+        [ (@=?) "January 31, 2010" $ getField "date" $ renderDateField
+            "date" "%B %e, %Y" "Date unknown" $ Page
+                (M.singleton "path" "/posts/2010-01-31-a-post.mkdwn") ""
+        , (@=?) "Date unknown" $ getField "date" $ renderDateField
+            "date" "%B %e, %Y" "Date unknown" $ Page
+                (M.singleton "path" "/posts/a-post.mkdwn") ""
+        , (@=?) "February 20, 2000" $ getField "date" $ renderDateField
+            "date" "%B %e, %Y" "Date unknown" $ flip Page "" $ M.fromList
+                [ ("path",      "/posts/2010-01-31-a-post.mkdwn")
+                , ("published", "February 20, 2000 1:00 PM")
+                ]
+        ]
+
+    , fromAssertions "copyBodyToField"
+        [ (Page (M.singleton "bar" "foo") "foo") @=?
+            copyBodyToField "bar" (Page M.empty "foo")
+        ]
+
+    , fromAssertions "copyBodyFromField"
+        [ (Page (M.singleton "bar" "foo") "foo") @=?
+            copyBodyFromField "bar" (Page (M.singleton "bar" "foo") "qux")
+        ]
+
+    , fromAssertions "comparePagesByDate"
+        [ GT @=? comparePagesByDate
+            (Page (M.singleton "path" "/posts/1990-08-26-foo.mkdwn") "")
+            (Page (M.singleton "path" "/posts/1990-06-18-qux.mkdwn") "")
+        ]
+    ]
diff --git a/tests/Hakyll/Web/Page/Tests.hs b/tests/Hakyll/Web/Page/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hakyll/Web/Page/Tests.hs
@@ -0,0 +1,40 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Hakyll.Web.Page.Tests
+    ( tests
+    ) where
+
+import Test.Framework
+import Test.HUnit hiding (Test)
+
+import qualified Data.Map as M
+
+import Hakyll.Web.Page
+import Hakyll.Web.Page.Read
+import TestSuite.Util
+
+tests :: [Test]
+tests = fromAssertions "readPage"
+    [ Page (M.singleton "foo" "bar") "body" @=? readPage
+        "---        \n\
+        \foo: bar   \n\
+        \---        \n\
+        \body"
+
+    , Page M.empty "line one\nlijn twee" @=? readPage
+        "line one\n\
+        \lijn twee"
+
+    , Page (M.fromList [("field1", "unos"), ("veld02", "deux")]) "" @=? readPage
+        "---\n\
+        \veld02: deux\n\
+        \field1: unos\n\
+        \---\n"
+
+    , Page (M.fromList [("author", "jasper"), ("title", "lol")]) "O hai\n"
+        @=? readPage
+        "---\n\
+        \author: jasper\n\
+        \title: lol\n\
+        \...\n\
+        \O hai\n"
+    ]
diff --git a/tests/Hakyll/Web/Template/Tests.hs b/tests/Hakyll/Web/Template/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hakyll/Web/Template/Tests.hs
@@ -0,0 +1,54 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Hakyll.Web.Template.Tests
+    ( tests
+    ) where
+
+import Test.Framework
+import Test.HUnit hiding (Test)
+
+import qualified Data.Map as M
+
+import Hakyll.Web.Page
+import Hakyll.Web.Template
+import Hakyll.Web.Template.Read
+import TestSuite.Util
+
+tests :: [Test]
+tests = fromAssertions "applyTemplate"
+    -- Hakyll templates
+    [ applyTemplateAssertion readTemplate applyTemplate
+        "bar" "$foo$" [("foo", "bar")]
+
+    , applyTemplateAssertion readTemplate applyTemplate
+        "$ barqux" "$$ $foo$$bar$" [("foo", "bar"), ("bar", "qux")]
+
+    , applyTemplateAssertion readTemplate applyTemplate
+        "$foo$" "$foo$" []
+
+    -- Hamlet templates
+    , applyTemplateAssertion readHamletTemplate applyTemplate
+        "<head><title>notice</title></head><body>A paragraph</body>"
+        "<head\n\
+        \    <title>#{title}\n\
+        \<body\n\
+        \    A paragraph\n"
+        [("title", "notice")]
+
+    -- Missing keys
+    , let missing "foo" = "bar"
+          missing "bar" = "qux"
+          missing x     = reverse x
+      in applyTemplateAssertion readTemplate (applyTemplateWith missing)
+        "bar foo ver" "$foo$ $bar$ $rev$" [("bar", "foo")]
+    ]
+
+-- | Utility function to create quick template tests
+--
+applyTemplateAssertion :: (String -> Template)
+                       -> (Template -> Page String -> Page String)
+                       -> String
+                       -> String
+                       -> [(String, String)]
+                       -> Assertion
+applyTemplateAssertion parser apply expected template page =
+    expected @=? pageBody (apply (parser template) (fromMap $ M.fromList page))
diff --git a/tests/Hakyll/Web/Urls/Relativize/Tests.hs b/tests/Hakyll/Web/Urls/Relativize/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hakyll/Web/Urls/Relativize/Tests.hs
@@ -0,0 +1,25 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Hakyll.Web.Urls.Relativize.Tests
+    ( tests
+    ) where
+
+import Test.Framework
+import Test.HUnit hiding (Test)
+
+import Hakyll.Web.Urls.Relativize
+import TestSuite.Util
+
+tests :: [Test]
+tests = fromAssertions "relativizeUrls"
+    [ "<a href=\"../foo\">bar</a>" @=?
+        relativizeUrls ".." "<a href=\"/foo\">bar</a>"
+    , "<img src=\"../../images/lolcat.png\"></img>" @=?
+        relativizeUrls "../.." "<img src=\"/images/lolcat.png\" />"
+    , "<a href=\"http://haskell.org\">Haskell</a>" @=?
+        relativizeUrls "../.." "<a href=\"http://haskell.org\">Haskell</a>"
+    , "<a href=\"http://haskell.org\">Haskell</a>" @=?
+        relativizeUrls "../.." "<a href=\"http://haskell.org\">Haskell</a>"
+    , "<script src=\"//ajax.googleapis.com/jquery.min.js\"></script>" @=?
+        relativizeUrls "../.."
+            "<script src=\"//ajax.googleapis.com/jquery.min.js\"></script>"
+    ]
diff --git a/tests/Hakyll/Web/Urls/Tests.hs b/tests/Hakyll/Web/Urls/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hakyll/Web/Urls/Tests.hs
@@ -0,0 +1,44 @@
+module Hakyll.Web.Urls.Tests
+    ( tests
+    ) where
+
+import Data.Char (toUpper)
+
+import Test.Framework
+import Test.HUnit hiding (Test)
+
+import Hakyll.Web.Urls
+import TestSuite.Util
+
+tests :: [Test]
+tests = concat
+    [ fromAssertions "withUrls"
+        [ "<a href=\"FOO\">bar</a>" @=?
+            withUrls (map toUpper) "<a href=\"foo\">bar</a>"
+        , "<img src=\"OH BAR\">" @=?
+            withUrls (map toUpper) "<img src=\"oh bar\">"
+
+        -- Test escaping
+        , "<script>\"sup\"</script>" @=?
+            withUrls id "<script>\"sup\"</script>"
+        , "<code>&lt;stdio&gt;</code>" @=?
+            withUrls id "<code>&lt;stdio&gt;</code>"
+        ]
+    , fromAssertions "toUrl"
+        [ "/foo/bar.html"    @=? toUrl "foo/bar.html"
+        , "/"                @=? toUrl "/"
+        , "/funny-pics.html" @=? toUrl "/funny-pics.html"
+        ]
+    , fromAssertions "toSiteRoot"
+        [ ".."    @=? toSiteRoot "/foo/bar.html"
+        , "."     @=? toSiteRoot "index.html"
+        , "."     @=? toSiteRoot "/index.html"
+        , "../.." @=? toSiteRoot "foo/bar/qux"
+        ]
+    , fromAssertions "isExternal"
+        [ assert (isExternal "http://reddit.com")
+        , assert (isExternal "https://mail.google.com")
+        , assert (not (isExternal "../header.png"))
+        , assert (not (isExternal "/foo/index.html"))
+        ]
+    ]
diff --git a/tests/Hakyll/Web/Util/Html/Tests.hs b/tests/Hakyll/Web/Util/Html/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hakyll/Web/Util/Html/Tests.hs
@@ -0,0 +1,22 @@
+module Hakyll.Web.Util.Html.Tests
+    ( tests
+    ) where
+
+import Test.Framework
+import Test.HUnit hiding (Test)
+
+import Hakyll.Web.Util.Html
+import TestSuite.Util
+
+tests :: [Test]
+tests = concat
+    [ fromAssertions "stripTags"
+        [ "foo"     @=? stripTags "<p>foo</p>"
+        , "foo bar" @=? stripTags "<p>foo</p> bar"
+        , "foo"     @=? stripTags "<p>foo</p"
+        ]
+    , fromAssertions "escapeHtml"
+        [ "Me &amp; Dean" @=? escapeHtml "Me & Dean"
+        , "&lt;img&gt;"   @=? escapeHtml "<img>"
+        ]
+    ]
diff --git a/tests/TestSuite/Util.hs b/tests/TestSuite/Util.hs
new file mode 100644
--- /dev/null
+++ b/tests/TestSuite/Util.hs
@@ -0,0 +1,44 @@
+-- | Test utilities
+--
+module TestSuite.Util
+    ( fromAssertions
+    , makeStoreTest
+    , runCompilerJobTest
+    ) where
+
+import Data.Monoid (mempty)
+
+import Test.Framework
+import Test.Framework.Providers.HUnit
+import Test.HUnit hiding (Test)
+
+import Hakyll.Core.Compiler.Internal
+import Hakyll.Core.Identifier
+import Hakyll.Core.Logger
+import Hakyll.Core.Resource.Provider
+import Hakyll.Core.Store
+
+fromAssertions :: String       -- ^ Name
+               -> [Assertion]  -- ^ Cases
+               -> [Test]       -- ^ Result tests
+fromAssertions name = zipWith testCase names
+  where
+    names = map (\n -> name ++ " [" ++ show n ++ "]") [1 :: Int ..]
+
+-- | Create a store for testing
+--
+makeStoreTest :: IO Store
+makeStoreTest = makeStore "_store"
+
+-- | Testing for 'runCompilerJob'
+--
+runCompilerJobTest :: Compiler () a
+                   -> Identifier ()
+                   -> ResourceProvider
+                   -> [Identifier ()]
+                   -> IO a
+runCompilerJobTest compiler id' provider uni = do
+    store <- makeStoreTest
+    logger <- makeLogger $ const $ return ()
+    Right x <- runCompilerJob compiler id' provider uni mempty store True logger
+    return x
