diff --git a/package.yaml b/package.yaml
index 399a138..b272820 100644
--- a/package.yaml
+++ b/package.yaml
@@ -20,6 +20,7 @@ dependencies:
 - pandoc
 - pandoc-types
 - text
+- time
 - twitter-conduit
 - xml-conduit
 - xml-types
@@ -29,9 +30,9 @@ library:
   dependencies:
   - aeson
   - authenticate-oauth
-  - boomerang
   - directory
   - filepath
+  - here
   - http-conduit
   - lens
   - lifted-base
@@ -42,14 +43,9 @@ library:
   - shakespeare
   - split
   - tagsoup
-  - time
   - transformers
   - twitter-types
   - unix
-  - web-routes
-  - web-routes-boomerang
-  - web-routes-happstack
-  - web-routes-th
 executables:
   multiblog:
     main: Main.hs
@@ -71,7 +67,7 @@ tests:
     - multiblog
     - HTF
     - HUnit
-    - derive
+    - generic-arbitrary
     - QuickCheck
     - quickcheck-instances
 stability: Experimental
diff --git a/src/App.hs b/src/App.hs
index c94960e..66cc85f 100644
--- a/src/App.hs
+++ b/src/App.hs
@@ -22,10 +22,6 @@ import System.Directory
 import System.Environment
 import System.FilePath
 
-import Web.Routes
-import Web.Routes.Boomerang (boomerangSiteRouteT)
-import Web.Routes.Happstack
-
 import Cache
 import Import
 import Models
@@ -39,7 +35,7 @@ import Views.Feed
 
 type App = StateT AppCache (ReaderT AppData IO)
 
-type AppPart a = RouteT Sitemap (ServerPartT App) a
+type AppPart a = ServerPartT App a
 
 loadApp ::
        String -- ^ directory to load from
@@ -87,33 +83,19 @@ site :: ServerPartT App Response
 site = do
     address <- lift $ asks appAddress
     appDir <- lift $ asks appDirectory
-    let routedSite = boomerangSiteRouteT handler sitemap
     let staticDir = appDir </> "static"
     let staticSite = serveDirectory DisableBrowsing ["index.html"] staticDir
-    implSite address "" routedSite `mplus` staticSite
-
--- Run an action in application routing context
-runRoute :: RouteT Sitemap m a -> m a
-runRoute act
-    -- Supply a known good URL (root) to run the site,
-    -- producing the result of the given action
- =
-    let (Right res) = runSite "" (boomerangSiteRouteT (const act) sitemap) []
-    in res
-
-parseRoute :: T.Text -> Either String Sitemap
-parseRoute =
-    fmap runIdentity . runSite "" (boomerangSiteRouteT pure sitemap) . segments
-  where
-    segments = decodePathInfo . T.encodeUtf8
+    let mainSite = do
+                       method GET
+                       uriRest $ \uri -> case parseURL (T.pack uri) of
+                                             Nothing -> mzero
+                                             Just route -> handler route
+    mainSite `mplus` staticSite
 
 handler :: Sitemap -> AppPart Response
 handler route =
     case route of
         Index -> index
-        Yearly y -> yearlyIndex y
-        Monthly y m -> monthlyIndex y m
-        Daily d -> dailyIndex d
         ArticleView d s -> article d s
         MetaView s f -> meta s f
         Feed lang -> feedIndex lang
@@ -124,15 +106,6 @@ handler route =
 index :: AppPart Response
 index = articleList $ const True
 
-yearlyIndex :: Integer -> AppPart Response
-yearlyIndex = articleList . byYear
-
-monthlyIndex :: Integer -> Int -> AppPart Response
-monthlyIndex year month = articleList $ byYearMonth year month
-
-dailyIndex :: Day -> AppPart Response
-dailyIndex = articleList . byDate
-
 -- Find the most relevant language preference in a request
 -- Includes: explicit GET parameter, cookie and Accept-Language header
 languageHeaderM :: AppPart LanguagePreference
@@ -169,12 +142,11 @@ articleList articleFilter = do
 
 meta :: Text -> Maybe PageFormat -> AppPart Response
 meta slug format' = do
-    let format = fromMaybe Html format'
     language <- languageHeaderM
     m <- askMeta slug
-    case format of
-        Html -> metaDisplay language m >>= okResponse
-        _ -> metaExport format language m >>= okResponse
+    case format' of
+        Nothing -> metaDisplay language m >>= okResponse
+        Just format -> metaExport format language m >>= okResponse
 
 feedIndex :: Language -> AppPart Response
 feedIndex language = do
@@ -189,4 +161,4 @@ printStylesheet :: AppPart Response
 printStylesheet = renderPrintStylesheet >>= okResponse
 
 codeStylesheet :: AppPart Response
-codeStylesheet = renderCodeStylesheet >>= okResponse
+codeStylesheet = okResponse renderCodeStylesheet
diff --git a/src/CrossPost.hs b/src/CrossPost.hs
index ce83f92..b7cee35 100644
--- a/src/CrossPost.hs
+++ b/src/CrossPost.hs
@@ -21,7 +21,6 @@ import qualified Data.Text as T
 
 import Network.HTTP.Conduit (newManager, tlsManagerSettings)
 
-import Web.Routes
 import Web.Twitter.Conduit (OAuth, TWInfo(..), call, twCredential, twOAuth)
 import Web.Twitter.Conduit.Api
 import Web.Twitter.Conduit.Parameters hiding (map)
@@ -40,11 +39,11 @@ import Views
 crossPost :: App ()
 crossPost = do
     liftIO $ putStrLn "Cross-posting new articles..."
-    runRoute crossPostTwitter
+    crossPostTwitter
     liftIO $ putStrLn "All new articles cross-posted."
 
 crossPostTwitter ::
-       (MonadRoute m, URL m ~ Sitemap, MonadIO m, MonadReader AppData m) => m ()
+       (MonadIO m, MonadReader AppData m) => m ()
 crossPostTwitter = do
     mgr <- liftIO $ newManager tlsManagerSettings
     address <- asks appAddress
@@ -84,7 +83,7 @@ twitterArticleLinks statuses = do
     -- Filter out the ones for the article route
     let articleLinks =
             [ (date, slug)
-            | Right (ArticleView date slug) <- map parseRoute urls
+            | Just (ArticleView date slug) <- map parseURL urls
             ]
     -- Get the matched articles
     let articleFilter art = any (\(d, s) -> byDateSlug d s art) articleLinks
diff --git a/src/Import.hs b/src/Import.hs
index 5298740..fd633e6 100644
--- a/src/Import.hs
+++ b/src/Import.hs
@@ -94,7 +94,7 @@ parseContent dir = do
             case M.lookup (T.pack $ tail ext) readers of
                 Nothing -> pure Nothing
                 Just reader -> do
-                    lang <- parseLanguage fileName
+                    lang <- parseLanguage (T.pack fileName)
                     case reader (T.decodeUtf8 $ sfContent file) of
                         Left err -> throwError $ show err
                         Right res -> pure (Just (lang, res))
diff --git a/src/Routes.hs b/src/Routes.hs
index f0282fb..35f81e6 100644
--- a/src/Routes.hs
+++ b/src/Routes.hs
@@ -1,6 +1,6 @@
+{-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TemplateHaskell #-}
-{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE QuasiQuotes #-}
 
 module Routes where
 
@@ -8,30 +8,31 @@ import Prelude hiding ((.))
 
 import Control.Category (Category((.)))
 
+import Data.String.Here (i)
+
+import Data.Char (isDigit)
 import Data.Text (Text)
-import qualified Data.Text as T
+import qualified Data.Text as Text
 import Data.Time
 
-import Text.Boomerang.TH (makeBoomerangs)
+import Debug.Trace
+
+import GHC.Generics
 
-import Web.Routes.Boomerang
+import Text.Read
 
 import Types.Language
 
 data PageFormat
-    = Html
-    | Pdf
+    = Pdf
     | Docx
-    deriving (Eq, Ord, Show)
+    deriving (Eq, Ord, Show, Generic)
 
--- TODO: use Boomerang for these
 formatToStr :: PageFormat -> Text
-formatToStr Html = "pdf"
 formatToStr Pdf = "pdf"
 formatToStr Docx = "docx"
 
 strToFormat :: Text -> Maybe PageFormat
-strToFormat "html" = Just Html
 strToFormat "pdf" = Just Pdf
 strToFormat "docx" = Just Docx
 strToFormat _ = Nothing
@@ -40,10 +41,6 @@ type MaybeFormat = Maybe PageFormat
 
 data Sitemap
     = Index
-    | Yearly Integer
-    | Monthly Integer
-              Int
-    | Daily Day
     | ArticleView Day
                   Text
     | MetaView Text
@@ -52,89 +49,57 @@ data Sitemap
     | SiteScript
     | PrintStylesheet
     | CodeStylesheet
-    deriving (Eq, Ord, Show)
-
-makeBoomerangs ''Sitemap
-
-rDay :: Boomerang TextsError [Text] r (Day :- r)
-rDay = xpure mkDay parseDay . (integer </> int </> int)
-  where
-    mkDay (y :- m :- d :- x) = fromGregorian y m d :- x
-    parseDay (day :- x) =
-        let (y, m, d) = toGregorian day
-        in Just $ y :- m :- d :- x
-
--- TODO: This will error on strings which are not language codes
-rLanguage :: Boomerang TextsError [Text] r (Language :- r)
-rLanguage = xpure mkLang parseLang . anyString
-  where
-    mkLang (str :- x) =
-        let Just lang = parseLanguageM str
-        in lang :- x
-    parseLang (lang :- x) = Just $ showLanguage lang :- x
-
-rString :: Boomerang e tok i (Text :- o) -> Boomerang e tok i (String :- o)
-rString = xmaph T.unpack (Just . T.pack)
-
-anyString :: Boomerang TextsError [Text] o (String :- o)
-anyString = rString anyText
-
-rExtension ::
-       Boomerang e tok i (Text :- o)
-    -> Boomerang e tok i (Text :- Maybe Text :- o)
-rExtension = xmap splitExt' (Just . joinExt')
-  where
-    splitExt' :: Text :- o -> Text :- Maybe Text :- o
-    splitExt' (seg :- o) =
-        let (seg', ext) = splitExt seg
-        in seg' :- ext :- o
-    joinExt' :: Text :- Maybe Text :- o -> Text :- o
-    joinExt' (seg :- ext :- o) = joinExt seg ext :- o
-
--- Swap the top 2 components in a Boomerang
-xflip :: Boomerang e tok i (a :- b :- o) -> Boomerang e tok i (b :- a :- o)
-xflip = xmap pflip (Just . pflip)
-  where
-    pflip (a :- b :- o) = b :- a :- o
-
--- Apply a transformation to the second topmost component of a Boomerang
-xmaph2 ::
-       (b -> c)
-    -> (c -> Maybe b)
-    -> Boomerang e tok i (a :- b :- o)
-    -> Boomerang e tok i (a :- c :- o)
-xmaph2 f g = xflip . xmaph f g . xflip
-
--- Split a path component into basename and extension
-splitExt :: Text -> (Text, Maybe Text)
-splitExt segment =
-    case T.splitOn "." segment of
-        [] -> ("", Nothing)
-        [s] -> (s, Nothing)
-        ss -> (T.intercalate "." $ init ss, Just $ last ss)
+    deriving (Eq, Ord, Show, Generic)
+
+routeURL :: Sitemap -> Text
+routeURL Index = ""
+routeURL (ArticleView date text) = let (year, month, day) = toGregorian date in [i|/${pad 2 year}-${pad 2 month}-${pad 2 day}/${text}|]
+routeURL (MetaView text Nothing) = [i|/${text}|]
+routeURL (MetaView text (Just format)) = [i|/${text}.${formatToStr format}|]
+routeURL (Feed language) = [i|/feed/${showLanguage language}|]
+routeURL SiteScript = "/assets/site.js"
+routeURL PrintStylesheet = "/assets/print.css"
+routeURL CodeStylesheet = "/assets/code.css"
+
+pad :: Show a => Int -> a -> Text
+pad len str = let printed = Text.pack (show str)
+                  padding = Text.replicate (len - Text.length printed) "0"
+               in padding <> printed
+
+splitNonEmpty :: Text -> Text -> [Text]
+splitNonEmpty splitter = filter (not . Text.null) . Text.splitOn splitter
+
+readText :: Read a => Text -> Maybe a
+readText = readMaybe . Text.unpack
+
+parseURL :: Text -> Maybe Sitemap
+parseURL = parseSegments . splitNonEmpty "/" . dropQueryParams
+    where
+        parseSegments [] = Just Index
+        parseSegments [metaText] = case splitNonEmpty "." metaText of
+                                                              [text] -> Just $ MetaView text Nothing
+                                                              [text, formatStr] -> do
+                                                                                    format <- strToFormat formatStr
+                                                                                    pure $ MetaView text (Just format)
+                                                              _ -> Nothing
+        parseSegments [date, articleText] | dateLike date = parseArticle (splitNonEmpty "-" date) articleText
+        parseSegments [year, month, day, articleText] = parseArticle [year, month, day] articleText
+        parseSegments ["assets", "site.js"] = Just SiteScript
+        parseSegments ["assets", "print.css"] = Just PrintStylesheet
+        parseSegments ["assets", "code.css"] = Just CodeStylesheet
+        parseSegments ["feed", lang] = Feed <$> parseLanguageM lang
+        parseSegments _ = Nothing
+        dateLike = Text.all $ \c -> isDigit c || c == '-'
+        parseArticle [yearStr, monthStr, dayStr] text = do
+                                                   year <- readText yearStr
+                                                   month <- readText monthStr
+                                                   day <- readText dayStr
+                                                   date <- fromGregorianValid year month day
+                                                   pure $ ArticleView date text
+        parseArticle _ _ = Nothing
+        dropQueryParams = Text.takeWhile (/= '?')
 
 -- Join a basename and extension together
 joinExt :: Text -> Maybe Text -> Text
 joinExt segment Nothing = segment
 joinExt segment (Just ext) = segment <> "." <> ext
-
--- Convert the second topmost component into a MaybeFormat
-xFormat ::
-       Boomerang e tok i (Text :- Maybe Text :- o)
-    -> Boomerang e tok i (Text :- MaybeFormat :- o)
-xFormat = xmaph2 (strToFormat =<<) (Just . fmap formatToStr)
-
-sitemap :: Boomerang TextsError [Text] r (Sitemap :- r)
-sitemap =
-    mconcat
-        [ rIndex
-        , rYearly . integer
-        , rMonthly . integer </> int
-        , rDaily . rDay
-        , rFeed . "feed" </> rLanguage
-        , rSiteScript . "site.js"
-        , rPrintStylesheet . "print.css"
-        , rCodeStylesheet . "code.css"
-        , rArticleView . rDay </> anyText
-        , rMetaView . xFormat (rExtension anyText)
-        ]
diff --git a/src/Types/Content.hs b/src/Types/Content.hs
index 975ba2e..ba1dd68 100644
--- a/src/Types/Content.hs
+++ b/src/Types/Content.hs
@@ -1,6 +1,7 @@
 {-|
 Types for the blog content - articles and metas.
 -}
+{-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE OverloadedStrings #-}
 
 module Types.Content where
@@ -15,6 +16,8 @@ import Data.Text (Text)
 import Data.Time
 import Data.Yaml
 
+import GHC.Generics (Generic)
+
 import Text.Blaze.Html (Html)
 
 import Text.Pandoc hiding (Meta)
@@ -40,7 +43,7 @@ data Article = Article
     { arSlug :: Text
     , arContent :: LanguageContent
     , arAuthored :: UTCTime
-    } deriving (Eq, Show)
+    } deriving (Eq, Show, Generic)
 
 instance Ord Article where
     a `compare` b = (arAuthored a, arSlug a) `compare` (arAuthored b, arSlug b)
@@ -55,7 +58,7 @@ instance HasSlug Article where
 data Layout
     = BaseLayout
     | PresentationLayout
-    deriving (Eq, Show)
+    deriving (Eq, Show, Generic)
 
 instance FromJSON Layout where
     parseJSON (String v)
@@ -69,7 +72,7 @@ data Meta = Meta
     , mtLayout :: Layout
     , mtExportSlugOverride :: Maybe Text
     , mtContent :: LanguageContent
-    } deriving (Eq, Show)
+    } deriving (Eq, Show, Generic)
 
 mtExportSlug :: Meta -> Text
 mtExportSlug meta = fromMaybe (mtSlug meta) (mtExportSlugOverride meta)
@@ -85,7 +88,7 @@ data Link
     = MetaLink { lnName :: Text }
     | ExternalLink { lnUrl :: Text
                    , lnText :: LanguageString }
-    deriving (Eq, Show)
+    deriving (Eq, Show, Generic)
 
 instance FromJSON Link where
     parseJSON (Object v) =
diff --git a/src/Types/Language.hs b/src/Types/Language.hs
index 69ab13d..2f3c34f 100644
--- a/src/Types/Language.hs
+++ b/src/Types/Language.hs
@@ -4,6 +4,7 @@ Language-related types.
 {-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings #-}
 {-# OPTIONS_GHC -fno-warn-orphans #-}
 
 module Types.Language where
@@ -16,18 +17,20 @@ import Data.Function
 import Data.LanguageCodes
 import Data.List
 import Data.List.Split
-import qualified Data.Map as M
+import Data.Map (Map)
+import qualified Data.Map as Map
 import Data.Maybe
-import qualified Data.Text as T
+import Data.Text (Text)
+import qualified Data.Text as Text
 
 import Utils
 
 type Language = ISO639_1
 
-type LanguageMap = M.Map Language
+type LanguageMap = Map Language
 
-mapKeysM :: (Monad m, Ord k2) => (k1 -> m k2) -> M.Map k1 a -> m (M.Map k2 a)
-mapKeysM kfunc = fmap M.fromList . traverse kvfunc . M.toList
+mapKeysM :: (Monad m, Ord k2) => (k1 -> m k2) -> Map k1 a -> m (Map k2 a)
+mapKeysM kfunc = fmap Map.fromList . traverse kvfunc . Map.toList
   where
     kvfunc (k, v) = (,) <$> kfunc k <*> pure v
 
@@ -36,7 +39,7 @@ instance A.FromJSON ISO639_1 where
     parseJSON _ = mzero
 
 instance A.FromJSONKey ISO639_1 where
-    fromJSONKey = A.FromJSONKeyTextParser $ parseLanguageM . T.unpack
+    fromJSONKey = A.FromJSONKeyTextParser parseLanguageM
 
 -- | Newtype for parsing either a single value or a map of values
 newtype LanguageChoices a =
@@ -46,7 +49,7 @@ instance A.FromJSON v => A.FromJSON (LanguageChoices v) where
     parseJSON v@(A.Object _) =
         LanguageChoices <$> (A.parseJSON v >>= mapKeysM parseLanguageM)
     parseJSON v@(A.String _) =
-        LanguageChoices . M.singleton defaultLanguage <$> A.parseJSON v
+        LanguageChoices . Map.singleton defaultLanguage <$> A.parseJSON v
     parseJSON _ = mzero
 
 newtype LanguagePreference = LanguagePreference
@@ -57,67 +60,70 @@ defaultLanguage :: Language
 defaultLanguage = EN
 
 singleLanguage :: Language -> LanguagePreference
-singleLanguage lang = LanguagePreference $ M.singleton lang 1
+singleLanguage lang = LanguagePreference $ Map.singleton lang 1
 
 bestLanguage :: LanguagePreference -> Language
 bestLanguage =
     fromMaybe defaultLanguage .
     listToMaybe .
     fmap fst .
-    sortBy (reverseCompare `on` snd) . M.toList . unLanguagePreference
+    sortBy (reverseCompare `on` snd) . Map.toList . unLanguagePreference
 
 rankLanguage :: Language -> LanguagePreference -> Float
-rankLanguage lang = fromMaybe 0 . M.lookup lang . unLanguagePreference
+rankLanguage lang = fromMaybe 0 . Map.lookup lang . unLanguagePreference
 
 matchLanguage :: LanguagePreference -> LanguageMap a -> Maybe a
 matchLanguage = matchLanguageFunc (const 1)
 
 matchLanguageFunc ::
        (a -> Float) -> LanguagePreference -> LanguageMap a -> Maybe a
-matchLanguageFunc quality pref values = fst <$> M.maxView ranked
+matchLanguageFunc quality pref values = fst <$> Map.maxView ranked
   where
-    ranked = M.fromList $ M.elems $ M.mapWithKey rank values
+    ranked = Map.fromList $ Map.elems $ Map.mapWithKey rank values
     rank lang value = (rankLanguage lang pref * quality value, value)
 
-parseLanguage :: MonadError String m => String -> m Language
+parseLanguage :: MonadError String m => Text -> m Language
 parseLanguage langStr =
     case parseLanguageM langStr :: Maybe Language of
         (Just lang) -> pure lang
-        Nothing -> throwError $ langStr ++ " is not a valid language code."
-
-parseLanguageM :: MonadPlus m => String -> m Language
-parseLanguageM [c1, c2] =
-    case fromChars c1 c2 of
-        Just lang -> return lang
-        Nothing -> mzero
-parseLanguageM (c1:c2:'-':_) = parseLanguageM [c1, c2]
-parseLanguageM _ = mzero
-
-showLanguage :: Language -> String
-showLanguage = (\(a, b) -> [a, b]) . toChars
-
-iso3166 :: Language -> String
+        Nothing -> throwError $ Text.unpack $ langStr <> " is not a valid language code."
+
+parseLanguageM :: MonadPlus m => Text -> m Language
+parseLanguageM = parseLanguageStr . Text.unpack
+    where
+        parseLanguageStr [c1, c2] =
+            case fromChars c1 c2 of
+                Just lang -> return lang
+                Nothing -> mzero
+        parseLanguageStr (c1:c2:'-':_) = parseLanguageStr [c1, c2]
+        parseLanguageStr _ = mzero
+
+showLanguage :: Language -> Text
+showLanguage = Text.pack . (\(a, b) -> [a, b]) . toChars
+
+iso3166 :: Language -> Text
 iso3166 EN = "gb"
 iso3166 ZH = "cn"
 iso3166 x = showLanguage x
 
 -- TODO: Parsec or library
 languageHeader :: Maybe String -> LanguagePreference
-languageHeader Nothing = LanguagePreference $ M.singleton defaultLanguage 1
+languageHeader Nothing = LanguagePreference $ Map.singleton defaultLanguage 1
 languageHeader (Just str) =
-    LanguagePreference $ M.fromList $ mapMaybe parsePref $ splitOn "," str
+    LanguagePreference $ Map.fromList $ mapMaybe parsePref $ splitOn "," str
   where
     parsePref pref =
         case splitOn ";q=" pref of
-            [lang] -> pairWith 1 <$> parseLanguageM lang
-            [lang, qvalue] -> pairWith (read qvalue) <$> parseLanguageM lang
+            [lang] -> pairWith 1 <$> parseLanguageM (Text.pack lang)
+            [lang, qvalue] -> pairWith (read qvalue) <$> parseLanguageM (Text.pack lang)
             _ -> Nothing
     pairWith y x = (x, y)
 
 instance Show LanguagePreference where
     show =
-        intercalate "," .
-        map (uncurry showPref) . M.toList . unLanguagePreference
+        Text.unpack .
+        Text.intercalate "," .
+        map (uncurry showPref) . Map.toList . unLanguagePreference
       where
         showPref lang 1 = showLanguage lang
-        showPref lang qvalue = showLanguage lang ++ ";q=" ++ show qvalue
+        showPref lang qvalue = showLanguage lang <> ";q=" <> Text.pack (show qvalue)
diff --git a/src/Types/Services.hs b/src/Types/Services.hs
index 0a4be29..68d704e 100644
--- a/src/Types/Services.hs
+++ b/src/Types/Services.hs
@@ -49,7 +49,7 @@ instance FromJSON AppServices where
 
 newtype AppAuth =
     AppAuthTwitter TwitterAuth
-    deriving (Eq, Show)
+    deriving (Eq, Show, Generic)
 
 class ToJSON a => ServiceAuth a where
     toAppAuth :: a -> AppAuth
@@ -59,7 +59,7 @@ class ToJSON a => ServiceAuth a where
 data TwitterAuth = TwitterAuth
     { taToken :: BS.ByteString
     , taSecret :: BS.ByteString
-    } deriving (Eq, Show)
+    } deriving (Eq, Show, Generic)
 
 instance ServiceAuth TwitterAuth where
     toAppAuth = AppAuthTwitter
@@ -98,7 +98,7 @@ instance ToJSON AppAuth where
 data CrossPost = CrossPost
     { cpLanguage :: Language
     , cpServiceDetails :: AppAuth
-    } deriving (Eq, Show)
+    } deriving (Eq, Show, Generic)
 
 type AppCrossPost = [CrossPost]
 
diff --git a/src/Views.hs b/src/Views.hs
index 93e09ed..5960b60 100644
--- a/src/Views.hs
+++ b/src/Views.hs
@@ -29,8 +29,6 @@ import Text.Pandoc hiding (Meta, Reader)
 import Text.Pandoc.Highlighting
 import Text.Pandoc.Walk
 
-import Web.Routes
-
 import Models
 import Render
 import Routes
@@ -49,8 +47,6 @@ instance Linkable Article where
 instance Linkable Meta where
     link m = MetaView (mtSlug m) Nothing
 
-type AppRoute m = (MonadRoute m, URL m ~ Sitemap)
-
 data PageContent = PageContent
     { pcTitle :: Maybe Text
     , pcLayout :: Layout
@@ -85,13 +81,8 @@ paginate size page allItems = Paginated prev items next
         | null rest = Nothing
         | otherwise = Just (page + 1)
 
-convRender :: (url -> [(a, Maybe b)] -> c) -> url -> [(a, b)] -> c
-convRender maybeF url params = maybeF url $ map (A.second Just) params
-
-render :: MonadRoute m => HtmlUrl (URL m) -> m Markup
-render html = do
-    route <- fmap convRender askRouteFn
-    return $ html route
+render :: MonadReader AppData m => HtmlUrl Sitemap -> m Markup
+render html = html <$> routeURLParams
 
 askLangStringFn ::
        MonadReader AppData m => LanguagePreference -> m (Text -> Text)
@@ -115,15 +106,14 @@ linkTitle lang (MetaLink slug) = do
     pure $ langTitle lang meta
 
 linkDestination ::
-       (AppRoute m, MonadReader AppData m, MonadPlus m) => Link -> m Text
+       (MonadReader AppData m, MonadPlus m) => Link -> m Text
 linkDestination (ExternalLink url _) = pure url
 linkDestination (MetaLink slug) = do
     meta <- askMeta slug
-    route <- askRouteFn
-    pure $ route (link meta) []
+    linkTo meta
 
 template ::
-       (AppRoute m, MonadReader AppData m, MonadPlus m)
+       (MonadReader AppData m, MonadPlus m)
     => LanguagePreference
     -> PageContent
     -> m Markup
@@ -144,7 +134,7 @@ template lang page = do
             render $(hamletFile "templates/base_presentation.hamlet")
 
 articleListDisplay ::
-       (AppRoute m, MonadReader AppData m, MonadPlus m)
+       (MonadReader AppData m, MonadPlus m)
     => LanguagePreference
     -> Paginated Article
     -> m Markup
@@ -154,7 +144,7 @@ articleListDisplay lang articles = do
     template lang $ def {pcContent = $(hamletFile "templates/list.hamlet")}
 
 articleDisplay ::
-       (AppRoute m, MonadReader AppData m, MonadPlus m)
+       (MonadReader AppData m, MonadPlus m)
     => LanguagePreference
     -> Article
     -> m Markup
@@ -166,7 +156,7 @@ articleDisplay lang article =
     }
 
 metaDisplay ::
-       (AppRoute m, MonadReader AppData m, MonadPlus m)
+       (MonadReader AppData m, MonadPlus m)
     => LanguagePreference
     -> Meta
     -> m Markup
@@ -185,20 +175,20 @@ metaDisplay lang meta =
                 $(hamletFile "templates/meta_presentation.hamlet")
 
 -- Generate a link to some content
-linkTo :: (Linkable a, AppRoute m) => a -> m Text
+linkTo :: (MonadReader AppData m, Linkable a) => a -> m Text
 linkTo a = do
-    routeFn <- askRouteFn
-    pure $ routeFn (link a) []
+    r <- routeURLParams
+    pure $ r (link a) []
 
 -- Modify the content to have a link to itself and have no anchors
 linkedContent ::
-       (HasContent a, Linkable a, AppRoute m)
+       (MonadReader AppData m, HasContent a, Linkable a)
     => LanguagePreference
     -> a
     -> m Pandoc
 linkedContent lang a = do
-    target <- linkTo a
-    pure $ linkedHeader target $ langContent lang a
+    lnk <- linkTo a
+    pure $ linkedHeader lnk $ langContent lang a
 
 -- Modify the first header to be a link to a given place
 -- and remove all anchors from headers
@@ -220,18 +210,19 @@ linkedHeader target doc = evalState (walkM linkHeader doc) True
         return $ Header n ("", [], []) text'
     linkHeader x = return x
 
-renderSiteScript :: MonadRoute m => m JavaScript
-renderSiteScript = do
-    route <- fmap convRender askRouteFn
-    return $ JavaScript $ renderJavascriptUrl route $(juliusFile "templates/site.julius")
+routeURLParams :: MonadReader AppData m => m (Render Sitemap)
+routeURLParams = do
+    address <- asks appAddress
+    pure $ \r _ -> address <> routeURL r
+
+renderSiteScript :: MonadReader AppData m => m JavaScript
+renderSiteScript = routeURLParams >>= \r -> pure $ JavaScript $ renderJavascriptUrl r $(juliusFile "templates/site.julius")
 
-renderPrintStylesheet :: MonadRoute m => m Stylesheet
-renderPrintStylesheet = do
-    route <- fmap convRender askRouteFn
-    return $ Stylesheet $ renderCssUrl route $(luciusFile "templates/print.lucius")
+renderPrintStylesheet :: MonadReader AppData m => m Stylesheet
+renderPrintStylesheet = routeURLParams >>= \r -> pure $ Stylesheet $ renderCssUrl r $(luciusFile "templates/print.lucius")
 
-renderCodeStylesheet :: MonadRoute m => m Stylesheet
-renderCodeStylesheet = pure $ Stylesheet $ TL.pack $ styleToCss highlightingStyle
+renderCodeStylesheet :: Stylesheet
+renderCodeStylesheet = Stylesheet $ TL.pack $ styleToCss highlightingStyle
 
 -- | Presentation can only render the pipe tables. Disable the other kinds
 presentationOptions :: WriterOptions
diff --git a/src/Views/Export.hs b/src/Views/Export.hs
index 8e7371c..9c98ecc 100644
--- a/src/Views/Export.hs
+++ b/src/Views/Export.hs
@@ -31,8 +31,6 @@ import Text.StringLike
 
 import Text.Pandoc hiding (Meta)
 
-import Web.Routes
-
 import Cache
 import Models
 import Render
@@ -43,9 +41,7 @@ import Views
 
 -- Export a meta into one of the supported formats
 metaExport ::
-       ( MonadRoute m
-       , URL m ~ Sitemap
-       , MonadReader AppData m
+       ( MonadReader AppData m
        , MonadState AppCache m
        , MonadIO m
        )
@@ -56,7 +52,6 @@ metaExport ::
 -- Pandoc uses TeX to render PDFs, which requires a lot of packages for Unicode
 -- support, etc. Use wkhtmltopdf instead
 metaExport Pdf lang meta = pdfExport lang meta
-metaExport Html _ _ = error "HTML is not an export format"
 metaExport Docx lang meta = do
     let content = langContent lang meta
     pure $
@@ -68,12 +63,11 @@ metaExportFileName format meta = Text.intercalate "." [metaName, fileExtension f
   where
     metaName = mtExportSlug meta
     fileExtension Docx = "docx"
-    fileExtension Html = error "HTML is not an export format"
     fileExtension Pdf = "pdf"
 
 -- Export a PDF using wkhtmltopdf
 pdfExport ::
-       (MonadRoute m, URL m ~ Sitemap, MonadState AppCache m, MonadIO m)
+       (MonadReader AppData m, MonadState AppCache m, MonadIO m)
     => LanguagePreference
     -> Meta
     -> m (Export LB.ByteString)
diff --git a/src/Views/Feed.hs b/src/Views/Feed.hs
index 45645b1..296b0ee 100644
--- a/src/Views/Feed.hs
+++ b/src/Views/Feed.hs
@@ -39,8 +39,6 @@ import Text.Blaze.Renderer.Text (renderMarkup)
 
 import qualified Text.XML as C
 
-import Web.Routes
-
 import Models
 import Routes
 import Types.Content
@@ -60,7 +58,7 @@ atomDate :: UTCTime -> Date
 atomDate = (<> "T00:00:00Z") . Text.pack . showGregorian . utctDay
 
 articleEntry ::
-       (MonadRoute m, URL m ~ Sitemap, MonadReader AppData m)
+       MonadReader AppData m
     => Language
     -> Article
     -> m Entry
@@ -84,7 +82,7 @@ articleEntry lang article = do
         }
 
 feedDisplay ::
-       (MonadRoute m, URL m ~ Sitemap, MonadReader AppData m)
+       MonadReader AppData m
     => Language
     -> [Article]
     -> m Response
diff --git a/stack.yaml b/stack.yaml
index ff829f4..986860c 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -4,14 +4,9 @@ packages:
 ghc-options:
   '$everything': -threaded
 extra-deps:
-  - boomerang-1.4.5.5
-  - derive-2.6.4
   - twitter-conduit-0.3.0
   - twitter-types-0.7.2.2
   - twitter-types-lens-0.7.2
-  - web-routes-boomerang-0.28.4.2
-  - web-routes-happstack-0.23.11
-  - web-routes-th-0.22.6.3
 flags:
   pandoc:
     embed_data_files: true
diff --git a/stack.yaml.lock b/stack.yaml.lock
index 4945754..f242b91 100644
--- a/stack.yaml.lock
+++ b/stack.yaml.lock
@@ -4,20 +4,6 @@
 #   https://docs.haskellstack.org/en/stable/lock_files
 
 packages:
-- completed:
-    hackage: boomerang-1.4.5.5@sha256:a6ea7b0cdc82e7d9d8c66bd63047da13453e20acd5e299e92fa8d286f8663041,1951
-    pantry-tree:
-      size: 791
-      sha256: 71b50b11f18cc0e27d040802a8d811caee9066a4bf40dd7e8b44cc57c8133fee
-  original:
-    hackage: boomerang-1.4.5.5
-- completed:
-    hackage: derive-2.6.4@sha256:f697cbe62828b5d777ea148814978940bd8707cb2ad4adff9415fc6139a7c05d,3184
-    pantry-tree:
-      size: 3967
-      sha256: cb33c5025c4629ea5377dd0fc521700cac79f8a88a5d062cfbb0cd7f9f37bcb5
-  original:
-    hackage: derive-2.6.4
 - completed:
     hackage: twitter-conduit-0.3.0@sha256:67b68511fc19563ff4159ae37188ac3db7dbc6603d81f11c6f1c2dd8ec1f5bd8,4318
     pantry-tree:
@@ -39,27 +25,6 @@ packages:
       sha256: 2b4a738a9ca4f6c98bc82f39b92e09f18e298f524f7121e568582e8f0f4dfe2c
   original:
     hackage: twitter-types-lens-0.7.2
-- completed:
-    hackage: web-routes-boomerang-0.28.4.2@sha256:5885435f8527a056d820630e26f76567d4106c8e13fe67a0bc191149dc9f4242,1060
-    pantry-tree:
-      size: 228
-      sha256: bac758a18c07d100c5a259e4e9c49eea3412c0d8d3b96fa1660a8b07dd97d30f
-  original:
-    hackage: web-routes-boomerang-0.28.4.2
-- completed:
-    hackage: web-routes-happstack-0.23.11@sha256:c61a497a13810c15e3b3a17db4fef05357cbf31a4a56bd73cdfbb9f9ae521b10,1044
-    pantry-tree:
-      size: 228
-      sha256: 37f74334eadaf5380f76f9f42e0da6ff678ab5961f7e72b35bbe28638e069523
-  original:
-    hackage: web-routes-happstack-0.23.11
-- completed:
-    hackage: web-routes-th-0.22.6.3@sha256:9a71de3770de13445bf116b16a5468d12e3dac0b91b8d3546088333a96bd235d,1533
-    pantry-tree:
-      size: 267
-      sha256: d2d7656d3cc35177ae51689dbe849d5c48d339339ba97018921d858f0a2bca0b
-  original:
-    hackage: web-routes-th-0.22.6.3
 snapshots:
 - completed:
     size: 508835
diff --git a/testsuite/Data/LanguageCodes/Arbitrary.hs b/testsuite/Data/LanguageCodes/Arbitrary.hs
index aa9f470..3af5935 100644
--- a/testsuite/Data/LanguageCodes/Arbitrary.hs
+++ b/testsuite/Data/LanguageCodes/Arbitrary.hs
@@ -1,13 +1,14 @@
-{-# LANGUAGE TemplateHaskell #-}
 {-# OPTIONS_GHC -fno-warn-orphans #-}
 
 module Data.LanguageCodes.Arbitrary where
 
-import Prelude hiding (LT)
+import Data.LanguageCodes (ISO639_1(..), fromChars)
 
-import Data.DeriveTH
-import Data.LanguageCodes (ISO639_1(..))
+import Data.Maybe
 
 import Test.QuickCheck
 
-derive makeArbitrary ''ISO639_1
+instance Arbitrary ISO639_1 where
+    arbitrary = elements $ catMaybes $ fromChars <$> letter <*> letter
+        where
+            letter = ['a'..'z']
diff --git a/testsuite/Integration/Base.hs b/testsuite/Integration/Base.hs
index 13ebdee..6af85ec 100644
--- a/testsuite/Integration/Base.hs
+++ b/testsuite/Integration/Base.hs
@@ -184,7 +184,7 @@ withLang1 = withLang . singleLanguage
 withLangCookie :: Language -> TestRequest -> TestRequest
 withLangCookie lang req =
     req
-    {trCookies = M.insert "lang" (T.pack $ showLanguage lang) (trCookies req)}
+    {trCookies = M.insert "lang" (showLanguage lang) (trCookies req)}
 
 -- Extract contents from a response
 responseContent :: Response -> IO LB.ByteString
diff --git a/testsuite/Integration/TestHome.hs b/testsuite/Integration/TestHome.hs
index 3fcbfa2..38e4a26 100644
--- a/testsuite/Integration/TestHome.hs
+++ b/testsuite/Integration/TestHome.hs
@@ -53,10 +53,10 @@ test_home_next_page = do
 
 test_css :: IO ()
 test_css = do
-    css <- makeRequestText $ simpleRequest "/code.css"
+    css <- makeRequestText $ simpleRequest "/assets/code.css"
     assertTextContains "color" css
 
 test_js :: IO ()
 test_js = do
-    js <- makeRequestText $ simpleRequest "/site.js"
+    js <- makeRequestText $ simpleRequest "/assets/site.js"
     assertTextContains "function" js
diff --git a/testsuite/Integration/TestMeta.hs b/testsuite/Integration/TestMeta.hs
index 42873f7..8a9abb2 100644
--- a/testsuite/Integration/TestMeta.hs
+++ b/testsuite/Integration/TestMeta.hs
@@ -17,11 +17,6 @@ test_meta = do
     meta <- makeRequestText $ simpleRequest "/meta"
     assertTextContains "<h1>Test Meta</h1>" meta
 
-test_meta_html :: IO ()
-test_meta_html = do
-    meta <- makeRequestText $ simpleRequest "/meta.html"
-    assertTextContains "<h1>Test Meta</h1>" meta
-
 test_meta_pdf :: IO ()
 test_meta_pdf = do
     meta_pdf <- makeRequest $ simpleRequest "/meta.pdf"
diff --git a/testsuite/TestLanguage.hs b/testsuite/TestLanguage.hs
index 743236d..395bf2c 100644
--- a/testsuite/TestLanguage.hs
+++ b/testsuite/TestLanguage.hs
@@ -1,5 +1,6 @@
 {-# OPTIONS_GHC -F -pgmF htfpp #-}
 {-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# LANGUAGE OverloadedStrings #-}
 
 module TestLanguage where
 
diff --git a/testsuite/TestModels.hs b/testsuite/TestModels.hs
index 98e80dd..59e8d97 100644
--- a/testsuite/TestModels.hs
+++ b/testsuite/TestModels.hs
@@ -1,13 +1,9 @@
 {-# OPTIONS_GHC -F -pgmF htfpp #-}
 {-# OPTIONS_GHC -fno-warn-orphans #-}
 {-# OPTIONS_GHC -fno-warn-missing-signatures #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TemplateHaskell #-}
 
 module TestModels where
 
-import Data.DeriveTH
-
 import Data.LanguageCodes.Arbitrary ()
 
 import qualified Data.Map as M
@@ -22,17 +18,23 @@ import Types.Content
 import Types.Services
 
 import Test.Framework
+import Test.QuickCheck.Arbitrary.Generic
 import Test.QuickCheck.Instances ()
 
-derive makeArbitrary ''Article
+instance Arbitrary Article where
+    arbitrary = genericArbitrary
 
-derive makeArbitrary ''Layout
+instance Arbitrary Layout where
+    arbitrary = genericArbitrary
 
-derive makeArbitrary ''Meta
+instance Arbitrary Meta where
+    arbitrary = genericArbitrary
 
-derive makeArbitrary ''Link
+instance Arbitrary Link where
+    arbitrary = genericArbitrary
 
-derive makeArbitrary ''Analytics
+instance Arbitrary Analytics where
+    arbitrary = genericArbitrary
 
 instance Arbitrary TW.OAuth where
     arbitrary = do
@@ -44,15 +46,20 @@ instance Arbitrary TW.OAuth where
             , TW.oauthConsumerSecret = secret
             }
 
-derive makeArbitrary ''AppServices
+instance Arbitrary AppServices where
+    arbitrary = genericArbitrary
 
-derive makeArbitrary ''TwitterAuth
+instance Arbitrary TwitterAuth where
+    arbitrary = genericArbitrary
 
-derive makeArbitrary ''AppAuth
+instance Arbitrary AppAuth where
+    arbitrary = genericArbitrary
 
-derive makeArbitrary ''CrossPost
+instance Arbitrary CrossPost where
+    arbitrary = genericArbitrary
 
-derive makeArbitrary ''AppData
+instance Arbitrary AppData where
+    arbitrary = genericArbitrary
 
 fall :: [a] -> (a -> Bool) -> Bool
 fall = flip all
diff --git a/testsuite/TestRoutes.hs b/testsuite/TestRoutes.hs
index 1e21c7b..ddc5cd5 100644
--- a/testsuite/TestRoutes.hs
+++ b/testsuite/TestRoutes.hs
@@ -3,18 +3,54 @@
 
 module TestRoutes where
 
+import Data.LanguageCodes
+import Data.LanguageCodes.Arbitrary ()
+
+import Data.Text (Text)
+import qualified Data.Text as Text
+
+import Data.Time.Calendar
+
 import Routes
 
 import Test.Framework
+import Test.QuickCheck
+import Test.QuickCheck.Arbitrary.Generic
 import Test.QuickCheck.Instances ()
 
-test_splitExt = do
-    assertEqual ("segment", Nothing) (splitExt "segment")
-    assertEqual ("segment", Just "ext") (splitExt "segment.ext")
-    assertEqual
-        ("segment.more.dots", Just "ext")
-        (splitExt "segment.more.dots.ext")
+instance Arbitrary PageFormat where
+    arbitrary = genericArbitrary
+
+arbitraryName :: Gen Text
+arbitraryName = Text.pack <$> listOf1 (elements ['a'..'z'])
+
+instance Arbitrary Sitemap where
+    arbitrary = oneof [ pure Index
+                      , ArticleView <$> arbitrary <*> arbitraryName
+                      , MetaView <$> arbitraryName <*> arbitrary
+                      , Feed <$> arbitrary
+                      , pure SiteScript
+                      , pure PrintStylesheet
+                      , pure CodeStylesheet
+                      ]
+
+test_index_URL = do
+    assertEqual "" (routeURL Index)
+    assertEqual (Just Index) $ parseURL "/"
+
+test_article_URL = do
+    let testArticle = ArticleView (fromGregorian 2020 01 01) "test"
+    assertEqual  "/2020-01-01/test" (routeURL testArticle)
+    assertEqual (Just testArticle) (parseURL "/2020/01/01/test")
+    assertEqual (Just testArticle) (parseURL "/2020-01-01/test/")
+
+test_meta_URL = do
+    assertEqual "/meta" (routeURL $ MetaView "meta" Nothing)
+    assertEqual (Just $ MetaView "meta" Nothing) (parseURL "/meta/")
+    assertEqual "/meta.pdf" (routeURL $ MetaView "meta" (Just Pdf))
+
+test_feed_URL = assertEqual "/feed/en" (routeURL $ Feed EN)
 
-prop_splitExt_joinExt seg =
-    let (seg', ext) = splitExt seg
-    in joinExt seg' ext == seg
+prop_routeURL_parseURL route =
+    let url = routeURL route
+    in parseURL url == Just route