diff --git a/hakyll.cabal b/hakyll.cabal
--- a/hakyll.cabal
+++ b/hakyll.cabal
@@ -1,5 +1,5 @@
 Name:               hakyll
-Version:            3.0.0.3
+Version:            3.0.0.4
 
 Synopsis:           A simple static site generator library.
 Description:        A simple static site generator library, mainly aimed at
@@ -65,7 +65,7 @@
                       strict-concurrency >= 0.2
   exposed-modules:    Hakyll
                       Hakyll.Main
-                      Hakyll.Web.Util.String
+                      Hakyll.Web.Util.Url
                       Hakyll.Web.Preview.Server
                       Hakyll.Web.Preview.Poll
                       Hakyll.Web.CompressCss
@@ -85,6 +85,7 @@
                       Hakyll.Core.UnixFilter
                       Hakyll.Core.Util.Arrow
                       Hakyll.Core.Util.File
+                      Hakyll.Core.Util.String
                       Hakyll.Core.ResourceProvider
                       Hakyll.Core.CompiledItem
                       Hakyll.Core.Compiler
diff --git a/src/Hakyll.hs b/src/Hakyll.hs
--- a/src/Hakyll.hs
+++ b/src/Hakyll.hs
@@ -12,6 +12,7 @@
     , module Hakyll.Core.UnixFilter
     , module Hakyll.Core.Util.Arrow
     , module Hakyll.Core.Util.File
+    , module Hakyll.Core.Util.String
     , module Hakyll.Core.Writable
     , module Hakyll.Main
     , module Hakyll.Web.CompressCss
@@ -24,7 +25,7 @@
     , module Hakyll.Web.RelativizeUrls
     , module Hakyll.Web.Tags
     , module Hakyll.Web.Template
-    , module Hakyll.Web.Util.String
+    , module Hakyll.Web.Util.Url
     ) where
 
 import Hakyll.Core.Compiler
@@ -38,6 +39,7 @@
 import Hakyll.Core.UnixFilter
 import Hakyll.Core.Util.Arrow
 import Hakyll.Core.Util.File
+import Hakyll.Core.Util.String
 import Hakyll.Core.Writable
 import Hakyll.Main
 import Hakyll.Web.CompressCss
@@ -50,4 +52,4 @@
 import Hakyll.Web.RelativizeUrls
 import Hakyll.Web.Tags
 import Hakyll.Web.Template
-import Hakyll.Web.Util.String
+import Hakyll.Web.Util.Url
diff --git a/src/Hakyll/Core/Routes.hs b/src/Hakyll/Core/Routes.hs
--- a/src/Hakyll/Core/Routes.hs
+++ b/src/Hakyll/Core/Routes.hs
@@ -32,6 +32,8 @@
     , setExtension
     , ifMatch
     , customRoute
+    , gsubRoute
+    , composeRoutes
     ) where
 
 import Data.Monoid (Monoid, mempty, mappend)
@@ -40,6 +42,7 @@
 
 import Hakyll.Core.Identifier
 import Hakyll.Core.Identifier.Pattern
+import Hakyll.Core.Util.String
 
 -- | Type used for a route
 --
@@ -94,3 +97,40 @@
 --
 customRoute :: (Identifier -> FilePath) -> Routes
 customRoute f = Routes $ Just . f
+
+-- | Create a gsub route
+--
+-- Example:
+--
+-- > runRoutes (gsubRoute "rss/" (const "")) "tags/rss/bar.xml"
+--
+-- Result:
+--
+-- > Just "tags/bar.xml"
+--
+gsubRoute :: String              -- ^ Pattern
+          -> (String -> String)  -- ^ Replacement
+          -> Routes              -- ^ Resulting route
+gsubRoute pattern replacement = customRoute $
+    replaceAll pattern replacement . toFilePath
+
+-- | Compose routes so that @f `composeRoutes` g@ is more or less equivalent
+-- with @f >>> g@.
+--
+-- Example:
+--
+-- > let routes = gsubRoute "rss/" (const "") `composeRoutes` setExtension "xml"
+-- > in runRoutes routes "tags/rss/bar"
+--
+-- Result:
+--
+-- > Just "tags/bar.xml"
+--
+-- If the first route given fails, Hakyll will not apply the second route.
+--
+composeRoutes :: Routes  -- ^ First route to apply
+              -> Routes  -- ^ Second route to apply
+              -> Routes  -- ^ Resulting route
+composeRoutes (Routes f) (Routes g) = Routes $ \i -> do
+    p <- f i
+    g $ parseIdentifier p
diff --git a/src/Hakyll/Core/Util/String.hs b/src/Hakyll/Core/Util/String.hs
new file mode 100644
--- /dev/null
+++ b/src/Hakyll/Core/Util/String.hs
@@ -0,0 +1,48 @@
+-- | Miscellaneous string manipulation functions.
+--
+module Hakyll.Core.Util.String
+    ( trim
+    , replaceAll
+    , splitAll
+    ) where
+
+import Data.Char (isSpace)
+import Data.Maybe (listToMaybe)
+
+import Text.Regex.PCRE ((=~~))
+
+-- | Trim a string (drop spaces, tabs and newlines at both sides).
+--
+trim :: String -> String
+trim = reverse . trim' . reverse . trim'
+  where
+    trim' = dropWhile isSpace
+
+-- | A simple (but inefficient) regex replace funcion
+--
+replaceAll :: String              -- ^ Pattern
+           -> (String -> String)  -- ^ Replacement (called on capture)
+           -> String              -- ^ Source string
+           -> String              -- ^ Result
+replaceAll pattern f source = replaceAll' source
+  where
+    replaceAll' src = case listToMaybe (src =~~ pattern) of
+        Nothing     -> src
+        Just (o, l) ->
+            let (before, tmp) = splitAt o src
+                (capture, after) = splitAt l tmp
+            in before ++ f capture ++ replaceAll' after
+
+-- | A simple regex split function. The resulting list will contain no empty
+-- strings.
+--
+splitAll :: String    -- ^ Pattern
+         -> String    -- ^ String to split
+         -> [String]  -- ^ Result
+splitAll pattern = filter (not . null) . splitAll'
+  where
+    splitAll' src = case listToMaybe (src =~~ pattern) of
+        Nothing     -> [src]
+        Just (o, l) ->
+            let (before, tmp) = splitAt o src
+            in before : splitAll' (drop l tmp)
diff --git a/src/Hakyll/Web/CompressCss.hs b/src/Hakyll/Web/CompressCss.hs
--- a/src/Hakyll/Web/CompressCss.hs
+++ b/src/Hakyll/Web/CompressCss.hs
@@ -12,7 +12,7 @@
 
 import Hakyll.Core.Compiler
 import Hakyll.Core.ResourceProvider
-import Hakyll.Web.Util.String
+import Hakyll.Core.Util.String
 
 -- | Compiler form of 'compressCss'
 --
diff --git a/src/Hakyll/Web/Feed.hs b/src/Hakyll/Web/Feed.hs
--- a/src/Hakyll/Web/Feed.hs
+++ b/src/Hakyll/Web/Feed.hs
@@ -33,7 +33,7 @@
 import Hakyll.Web.Page.Metadata
 import Hakyll.Web.Template
 import Hakyll.Web.Template.Read.Hakyll (readTemplate)
-import Hakyll.Web.Util.String
+import Hakyll.Web.Util.Url
 
 import Paths_hakyll
 
@@ -60,17 +60,17 @@
            -> String             -- ^ Resulting feed
 createFeed feedTemplate itemTemplate url configuration items =
     pageBody $ applyTemplate feedTemplate
-             $ setField "timestamp"   timestamp
-             $ setField "title"       (feedTitle configuration)
-             $ setField "description" (feedDescription configuration)
-             $ setField "authorName"  (feedDescription configuration)
-             $ setField "root"        (feedRoot configuration)
-             $ setField "url"         url
+             $ trySetField "timestamp"   timestamp
+             $ trySetField "title"       (feedTitle configuration)
+             $ trySetField "description" (feedDescription configuration)
+             $ trySetField "authorName"  (feedDescription configuration)
+             $ trySetField "root"        (feedRoot configuration)
+             $ trySetField "url"         url
              $ fromBody body
   where
     -- Preprocess items
     items' = flip map items $ applyTemplate itemTemplate
-                            . setField "root" (feedRoot configuration)
+                            . trySetField "root" (feedRoot configuration)
 
     -- Body: concatenated items
     body = concat $ map pageBody items'
diff --git a/src/Hakyll/Web/Page.hs b/src/Hakyll/Web/Page.hs
--- a/src/Hakyll/Web/Page.hs
+++ b/src/Hakyll/Web/Page.hs
@@ -73,7 +73,7 @@
 import Hakyll.Web.Page.Metadata
 import Hakyll.Web.Pandoc
 import Hakyll.Web.Template
-import Hakyll.Web.Util.String
+import Hakyll.Web.Util.Url
 
 -- | Create a page from a body, without metadata
 --
@@ -107,12 +107,12 @@
   where
     -- Add root and url, based on route
     addRoute Nothing  = id
-    addRoute (Just r) = setField "url" (toUrl r)
+    addRoute (Just r) = trySetField "url" (toUrl r)
 
     -- Add title and category, based on identifier
-    addIdentifier i = setField "title" (takeBaseName p)
-                    . setField "category" (takeBaseName $ takeDirectory p)
-                    . setField "path" p
+    addIdentifier i = trySetField "title" (takeBaseName p)
+                    . trySetField "category" (takeBaseName $ takeDirectory p)
+                    . trySetField "path" p
       where
         p = toFilePath i
 
diff --git a/src/Hakyll/Web/Page/Metadata.hs b/src/Hakyll/Web/Page/Metadata.hs
--- a/src/Hakyll/Web/Page/Metadata.hs
+++ b/src/Hakyll/Web/Page/Metadata.hs
@@ -4,6 +4,7 @@
     ( getField
     , getFieldMaybe
     , setField
+    , trySetField
     , setFieldA
     , renderField
     , changeField
@@ -24,7 +25,7 @@
 import System.Locale (TimeLocale, defaultTimeLocale)
 
 import Hakyll.Web.Page.Internal
-import Hakyll.Web.Util.String
+import Hakyll.Core.Util.String
 
 -- | Get a metadata field. If the field does not exist, the empty string is
 -- returned.
@@ -41,13 +42,21 @@
               -> Maybe String  -- ^ Value, if found
 getFieldMaybe key = M.lookup key . pageMetadata
 
--- | Add a metadata field. If the field already exists, it is not overwritten.
+-- | Version of 'trySetField' which overrides any previous value
 --
 setField :: String  -- ^ Key
          -> String  -- ^ Value
          -> Page a  -- ^ Page to add it to
          -> Page a  -- ^ Resulting page
-setField k v (Page m b) = Page (M.insertWith (flip const) k v m) b
+setField k v (Page m b) = Page (M.insert k v m) b
+
+-- | Add a metadata field. If the field already exists, it is not overwritten.
+--
+trySetField :: String  -- ^ Key
+            -> String  -- ^ Value
+            -> Page a  -- ^ Page to add it to
+            -> Page a  -- ^ Resulting page
+trySetField k v (Page m b) = Page (M.insertWith (flip const) k v m) b
 
 -- | Arrow-based variant of 'setField'. Because of it's type, this function is
 -- very usable together with the different 'require' functions.
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
@@ -12,7 +12,7 @@
 import qualified Data.Map as M
 
 import Hakyll.Web.Page.Internal
-import Hakyll.Web.Util.String
+import Hakyll.Core.Util.String
 
 -- | We're using a simple state monad as parser
 --
diff --git a/src/Hakyll/Web/Preview/Server.hs b/src/Hakyll/Web/Preview/Server.hs
--- a/src/Hakyll/Web/Preview/Server.hs
+++ b/src/Hakyll/Web/Preview/Server.hs
@@ -18,7 +18,7 @@
                         , ConfigListen (..), emptyConfig
                         )
 
-import Hakyll.Web.Util.String (replaceAll)
+import Hakyll.Core.Util.String (replaceAll)
 
 -- | The first file in the list that actually exists is returned
 --
diff --git a/src/Hakyll/Web/RelativizeUrls.hs b/src/Hakyll/Web/RelativizeUrls.hs
--- a/src/Hakyll/Web/RelativizeUrls.hs
+++ b/src/Hakyll/Web/RelativizeUrls.hs
@@ -29,7 +29,7 @@
 
 import Hakyll.Core.Compiler
 import Hakyll.Web.Page
-import Hakyll.Web.Util.String
+import Hakyll.Web.Util.Url
 
 -- | Compiler form of 'compressCss' which automatically picks the right root
 -- path
diff --git a/src/Hakyll/Web/Tags.hs b/src/Hakyll/Web/Tags.hs
--- a/src/Hakyll/Web/Tags.hs
+++ b/src/Hakyll/Web/Tags.hs
@@ -55,10 +55,11 @@
 
 import Hakyll.Web.Page
 import Hakyll.Web.Page.Metadata
-import Hakyll.Web.Util.String
+import Hakyll.Web.Util.Url
 import Hakyll.Core.Writable
 import Hakyll.Core.Identifier
 import Hakyll.Core.Compiler
+import Hakyll.Core.Util.String
 
 -- | Data about tags
 --
diff --git a/src/Hakyll/Web/Util/String.hs b/src/Hakyll/Web/Util/String.hs
deleted file mode 100644
--- a/src/Hakyll/Web/Util/String.hs
+++ /dev/null
@@ -1,73 +0,0 @@
--- | Miscellaneous string manipulation functions.
---
-module Hakyll.Web.Util.String
-    ( trim
-    , replaceAll
-    , splitAll
-    , toUrl
-    , toSiteRoot
-    ) where
-
-import Data.Char (isSpace)
-import Data.Maybe (listToMaybe)
-
-import System.FilePath (splitPath, takeDirectory, joinPath)
-import Text.Regex.PCRE ((=~~))
-
--- | Trim a string (drop spaces, tabs and newlines at both sides).
---
-trim :: String -> String
-trim = reverse . trim' . reverse . trim'
-  where
-    trim' = dropWhile isSpace
-
--- | A simple (but inefficient) regex replace funcion
---
-replaceAll :: String              -- ^ Pattern
-           -> (String -> String)  -- ^ Replacement (called on capture)
-           -> String              -- ^ Source string
-           -> String              -- ^ Result
-replaceAll pattern f source = replaceAll' source
-  where
-    replaceAll' src = case listToMaybe (src =~~ pattern) of
-        Nothing     -> src
-        Just (o, l) ->
-            let (before, tmp) = splitAt o src
-                (capture, after) = splitAt l tmp
-            in before ++ f capture ++ replaceAll' after
-
--- | A simple regex split function. The resulting list will contain no empty
--- strings.
---
-splitAll :: String    -- ^ Pattern
-         -> String    -- ^ String to split
-         -> [String]  -- ^ Result
-splitAll pattern = filter (not . null) . splitAll'
-  where
-    splitAll' src = case listToMaybe (src =~~ pattern) of
-        Nothing     -> [src]
-        Just (o, l) ->
-            let (before, tmp) = splitAt o src
-            in before : splitAll' (drop l tmp)
-
--- | Convert a filepath to an URL starting from the site root
---
--- Example:
---
--- > toUrl "foo/bar.html"
---
--- Result:
---
--- > "/foo/bar.html"
---
-toUrl :: FilePath -> String
-toUrl = ('/' :)
-
--- | Get the relative url to the site root, for a given (absolute) url
---
-toSiteRoot :: String -> String
-toSiteRoot = emptyException . joinPath . map parent . splitPath . takeDirectory
-  where
-    parent = const ".."
-    emptyException [] = "."
-    emptyException x  = x
diff --git a/src/Hakyll/Web/Util/Url.hs b/src/Hakyll/Web/Util/Url.hs
new file mode 100644
--- /dev/null
+++ b/src/Hakyll/Web/Util/Url.hs
@@ -0,0 +1,30 @@
+-- | Miscellaneous URL manipulation functions.
+--
+module Hakyll.Web.Util.Url
+    ( toUrl
+    , toSiteRoot
+    ) where
+
+import System.FilePath (splitPath, takeDirectory, joinPath)
+
+-- | Convert a filepath to an URL starting from the site root
+--
+-- Example:
+--
+-- > toUrl "foo/bar.html"
+--
+-- Result:
+--
+-- > "/foo/bar.html"
+--
+toUrl :: FilePath -> String
+toUrl = ('/' :)
+
+-- | Get the relative url to the site root, for a given (absolute) url
+--
+toSiteRoot :: String -> String
+toSiteRoot = emptyException . joinPath . map parent . splitPath . takeDirectory
+  where
+    parent = const ".."
+    emptyException [] = "."
+    emptyException x  = x
