diff --git a/Yesod/Helpers/Static.hs b/Yesod/Helpers/Static.hs
--- a/Yesod/Helpers/Static.hs
+++ b/Yesod/Helpers/Static.hs
@@ -27,11 +27,16 @@
 module Yesod.Helpers.Static
     ( -- * Subsite
       Static (..)
+    , Public (..)
     , StaticRoute (..)
+    , PublicRoute (..)
       -- * Smart constructor
     , static
+    , publicProduction
+    , publicDevel
       -- * Template Haskell helpers
     , staticFiles
+    , publicFiles
     {-
       -- * Embed files
     , getStaticHandler
@@ -39,11 +44,12 @@
       -- * Hashing
     , base64md5
 #if TEST
-    , testSuite
+    , getFileListPieces 
 #endif
     ) where
 
 import System.Directory
+import qualified System.Time
 import Control.Monad
 
 import Yesod.Handler
@@ -53,7 +59,6 @@
 import Language.Haskell.TH
 import Language.Haskell.TH.Syntax
 
-import Data.Char
 import qualified Data.ByteString.Lazy as L
 import Data.Digest.Pure.MD5
 import qualified Data.ByteString.Base64
@@ -61,30 +66,62 @@
 import qualified Data.Serialize
 import Data.Text (Text, pack)
 import Data.Monoid (mempty)
+import qualified Data.Map as M
+import Data.IORef (readIORef, newIORef, writeIORef)
 
 import Network.Wai.Application.Static
-    ( defaultMimeTypeByExt, StaticSettings (..), staticAppPieces
-    , defaultListing
+    ( StaticSettings (..), CacheSettings (..)
+    , defaultStaticSettings, defaultPublicSettings
+    , staticAppPieces
+    , pathFromPieces
     )
 
-#if TEST
-import Test.Framework (testGroup, Test)
-import Test.Framework.Providers.HUnit
-import Test.HUnit hiding (Test)
-#endif
-
-import System.IO (withBinaryFile, IOMode (ReadMode))
-
+-- | generally static assets referenced in html files
+-- assets get a checksum query parameter appended for perfect caching
+-- * a far future expire date is set
+-- * a given asset revision will only ever be downloaded once (if the browser maintains its cache)
+-- if you don't want to see a checksum in the url- use Public
 newtype Static = Static StaticSettings
+-- | same as Static, but there is no checksum query parameter appended
+-- generally html files and the favicon, but could be any file where you don't want the checksum parameter
+-- * the file checksum is used for an ETag.
+-- * this form of caching is not as good as the static- the browser can avoid downloading the file, but it always need to send a request with the etag value to the server to see if its copy is up to date
+newtype Public = Public StaticSettings
 
 -- | Default value of 'Static' for a given file folder.
 --
 -- Does not have index files, uses default directory listings and default mime
 -- type list.
-static :: FilePath -> Static
-static fp = Static $ StaticSettings fp [] (Just defaultListing)
-                     (return . defaultMimeTypeByExt)
+static :: String -> FilePath -> IO Static
+static root fp = do
+  hashes <- mkHashMap fp
+  return $ Static $ (defaultStaticSettings (Forever $ isStaticRequest hashes)) {
+    ssFolder = fp
+  , ssMkRedirect = \_ newPath -> S8.append (S8.pack (root ++ "/")) newPath
+  }
+  where
+    isStaticRequest hashes reqf reqh = case M.lookup reqf hashes of
+                               Nothing -> False
+                               Just h  -> h == reqh
 
+-- | no directory listing
+public :: String -> FilePath -> CacheSettings -> Public
+public root fp cache = Public $ (defaultPublicSettings cache) {
+    ssFolder = fp 
+  , ssMkRedirect = \_ newPath -> S8.append (S8.pack (root ++ "/")) newPath
+  }
+
+publicProduction :: String -> FilePath -> IO Public
+publicProduction root fp = do
+  etags <- mkPublicProductionEtag fp
+  return $ public root fp etags
+
+publicDevel :: String -> FilePath -> IO Public
+publicDevel root fp = do
+  etags <- mkPublicDevelEtag fp
+  return $ public root fp etags
+
+
 -- | Manually construct a static route.
 -- The first argument is a sub-path to the file being served whereas the second argument is the key value pairs in the query string.
 -- For example,
@@ -94,45 +131,32 @@
 -- E.g. When generating image galleries.
 data StaticRoute = StaticRoute [Text] [(Text, Text)]
     deriving (Eq, Show, Read)
+data PublicRoute = PublicRoute [Text] [(Text, Text)]
+    deriving (Eq, Show, Read)
 
 type instance Route Static = StaticRoute
+type instance Route Public = PublicRoute
 
 instance RenderRoute StaticRoute where
     renderRoute (StaticRoute x y) = (x, y)
+instance RenderRoute PublicRoute where
+    renderRoute (PublicRoute x y) = (x, y)
 
 instance Yesod master => YesodDispatch Static master where
     yesodDispatch (Static set) _ pieces  _ _ =
         Just $ staticAppPieces set pieces
 
-{- FIXME
--- | Dispatch static route for a subsite
---
--- Subsites with static routes can't (yet) define Static routes the same way "master" sites can.
--- Instead of a subsite route:
--- /static StaticR Static getStatic
--- Use a normal route:
--- /static/*Strings StaticR GET
---
--- Then, define getStaticR something like:
--- getStaticR = getStaticHandler ($(mkEmbedFiles "static") typeByExt) StaticR
--- */ end CPP comment
-getStaticHandler :: Static -> (StaticRoute -> Route sub) -> [String] -> GHandler sub y ChooseRep
-getStaticHandler static toSubR pieces = do
-  toMasterR <- getRouteToMaster   
-  toMasterHandler (toMasterR . toSubR) toSub route handler
-  where route = StaticRoute pieces []
-        toSub _ = static
-        staticSite = getSubSite :: Site (Route Static) (String -> Maybe (GHandler Static y ChooseRep))
-        handler = fromMaybe notFound $ handleSite staticSite undefined route "GET"
--}
+instance Yesod master => YesodDispatch Public master where
+    yesodDispatch (Public set) _ pieces  _ _ =
+        Just $ staticAppPieces set pieces
 
 notHidden :: FilePath -> Bool
 notHidden ('.':_) = False
 notHidden "tmp" = False
 notHidden _ = True
 
-getFileList :: FilePath -> IO [[String]]
-getFileList = flip go id
+getFileListPieces :: FilePath -> IO [[String]]
+getFileListPieces = flip go id
   where
     go :: String -> ([String] -> [String]) -> IO [[String]]
     go fp front = do
@@ -150,67 +174,119 @@
 -- > style_css = StaticRoute ["style.css"] []
 -- > js_script_js = StaticRoute ["js/script.js"] []
 staticFiles :: FilePath -> Q [Dec]
-staticFiles fp = do
-    fs <- qRunIO $ getFileList fp
-    concat `fmap` mapM go fs
+staticFiles dir = mkStaticFiles dir StaticSite
+
+publicFiles :: FilePath -> Q [Dec]
+publicFiles dir = mkStaticFiles dir PublicSite
+
+mkHashMap :: FilePath -> IO (M.Map FilePath S8.ByteString)
+mkHashMap dir = do
+    fs <- getFileListPieces dir
+    hashAlist fs >>= return . M.fromList
   where
+    hashAlist :: [[String]] -> IO [(FilePath, S8.ByteString)]
+    hashAlist fs = mapM hashPair fs
+      where
+        hashPair :: [String] -> IO (FilePath, S8.ByteString)
+        hashPair pieces = do let file = pathFromPieces dir (map pack pieces)
+                             h <- base64md5File file
+                             return (file, S8.pack h)
+
+mkPublicDevelEtag :: FilePath -> IO CacheSettings
+mkPublicDevelEtag dir = do
+    etags <- mkHashMap dir
+    mtimeVar <- newIORef (M.empty :: M.Map FilePath System.Time.ClockTime)
+    return $ ETag $ \f ->
+      case M.lookup f etags of
+        Nothing -> return Nothing
+        Just checksum -> do
+          newt <- getModificationTime f
+          mtimes <- readIORef mtimeVar
+          oldt <- case M.lookup f mtimes of
+            Nothing -> writeIORef mtimeVar (M.insert f newt mtimes) >> return newt
+            Just ot -> return ot
+          return $ if newt /= oldt then Nothing else Just checksum
+
+
+mkPublicProductionEtag :: FilePath -> IO CacheSettings
+mkPublicProductionEtag dir = do
+    etags <- mkHashMap dir
+    return $ ETag $ \f -> return . M.lookup f $ etags
+
+data StaticSite = StaticSite | PublicSite
+mkStaticFiles :: FilePath -> StaticSite -> Q [Dec]
+mkStaticFiles fp StaticSite = mkStaticFiles' fp "StaticRoute" True
+mkStaticFiles fp PublicSite = mkStaticFiles' fp "PublicRoute" False
+
+mkStaticFiles' :: FilePath -> -- ^ static directory
+                  String   -> -- ^ route constructor "StaticRoute"
+                  Bool     -> -- ^ append checksum query parameter
+                  Q [Dec]
+mkStaticFiles' fp routeConName makeHash = do
+    fs <- qRunIO $ getFileListPieces fp
+    concat `fmap` mapM mkRoute fs
+  where
     replace' c
         | 'A' <= c && c <= 'Z' = c
         | 'a' <= c && c <= 'z' = c
         | '0' <= c && c <= '9' = c
         | otherwise = '_'
-    go f = do
-        let adjust [] = ""
-            adjust str@(x:xs) | isDigit x = '_' : x : xs
-                              | isUpper x = toLower x : xs
-                              | otherwise = str
-        let name = mkName $ intercalate "_" $ map (adjust . map replace') f
+    mkRoute f = do
+        let name = mkName $ intercalate "_" $ map (map replace') f
         f' <- [|map pack $(lift f)|]
-        let sr = ConE $ mkName "StaticRoute"
-        hash <- qRunIO . calcHash $ fp ++ '/' : intercalate "/" f
-
+        let route = mkName routeConName
         pack' <- [|pack|]
-        qs <- [|[(pack $(lift hash), mempty)]|]
+        qs <- if makeHash
+                    then do hash <- qRunIO $ base64md5File $ pathFromPieces fp (map pack f)
+                            [|[(pack $(lift hash), mempty)]|]
+                    else return $ ListE []
         return
-            [ SigD name $ ConT ''Route `AppT` ConT ''Static
+            [ SigD name $ ConT route
             , FunD name
-                [ Clause [] (NormalB $ sr `AppE` f' `AppE` qs) []
+                [ Clause [] (NormalB $ (ConE route) `AppE` f' `AppE` qs) []
                 ]
             ]
 
-#if TEST
-
-testSuite :: Test
-testSuite = testGroup "Yesod.Helpers.Static"
-    [ testCase "get file list" caseGetFileList
-    ]
-
-caseGetFileList :: Assertion
-caseGetFileList = do
-    x <- getFileList "test"
-    x @?= [["foo"], ["bar", "baz"]]
-
-#endif
+base64md5File :: FilePath -> IO String
+base64md5File file = do
+  contents <- L.readFile file
+  return $ base64md5 contents
 
 -- | md5-hashes the given lazy bytestring and returns the hash as
 -- base64url-encoded string.
 --
 -- This function returns the first 8 characters of the hash.
 base64md5 :: L.ByteString -> String
-base64md5 = map go
+base64md5 = map tr
           . take 8
           . S8.unpack
           . Data.ByteString.Base64.encode
           . Data.Serialize.encode
           . md5
   where
-    go '+' = '-'
-    go '/' = '_'
-    go c   = c
+    tr '+' = '-'
+    tr '/' = '_'
+    tr c   = c
 
-calcHash :: FilePath -> IO String
-calcHash fname =
-    withBinaryFile fname ReadMode hashHandle
-  where
-    hashHandle h = do s <- L.hGetContents h
-                      return $! base64md5 s
+{- FIXME
+-- | Dispatch static route for a subsite
+--
+-- Subsites with static routes can't (yet) define Static routes the same way "master" sites can.
+-- Instead of a subsite route:
+-- /static StaticR Static getStatic
+-- Use a normal route:
+-- /static/*Strings StaticR GET
+--
+-- Then, define getStaticR something like:
+-- getStaticR = getStaticHandler ($(mkEmbedFiles "static") typeByExt) StaticR
+-- */ end CPP comment
+getStaticHandler :: Static -> (StaticRoute -> Route sub) -> [String] -> GHandler sub y ChooseRep
+getStaticHandler static toSubR pieces = do
+  toMasterR <- getRouteToMaster   
+  toMasterHandler (toMasterR . toSubR) toSub route handler
+  where route = StaticRoute pieces []
+        toSub _ = static
+        staticSite = getSubSite :: Site (Route Static) (String -> Maybe (GHandler Static y ChooseRep))
+        handler = fromMaybe notFound $ handleSite staticSite undefined route "GET"
+-}
+
diff --git a/runtests.hs b/runtests.hs
deleted file mode 100644
--- a/runtests.hs
+++ /dev/null
@@ -1,6 +0,0 @@
-import Test.Framework (defaultMain)
-
-import Yesod.Helpers.Static
-
-main :: IO ()
-main = defaultMain [testSuite]
diff --git a/yesod-static.cabal b/yesod-static.cabal
--- a/yesod-static.cabal
+++ b/yesod-static.cabal
@@ -1,5 +1,5 @@
 name:            yesod-static
-version:         0.1.0.1
+version:         0.2.0
 license:         BSD3
 license-file:    LICENSE
 author:          Michael Snoyman <michael@snoyman.com>
@@ -7,16 +7,14 @@
 synopsis:        Static file serving subsite for Yesod Web Framework.
 category:        Web, Yesod
 stability:       Stable
-cabal-version:   >= 1.6
+cabal-version:   >= 1.8
 build-type:      Simple
 homepage:        http://www.yesodweb.com/
 
-flag test
-  description: Build the executable to run unit tests
-  default: False
-
 library
     build-depends:   base                      >= 4        && < 5
+                   , containers                >= 0.4
+                   , old-time                  >= 1.0
                    , yesod-core                >= 0.8      && < 0.9
                    , base64-bytestring         >= 0.1.0.1  && < 0.2
                    , pureMD5                   >= 2.1.0.3  && < 2.2
@@ -25,24 +23,10 @@
                    , template-haskell
                    , directory                 >= 1.0      && < 1.2
                    , transformers              >= 0.2      && < 0.3
-                   , wai-app-static            >= 0.1      && < 0.2
+                   , wai-app-static            >= 0.2      && < 0.3
                    , text                      >= 0.5      && < 1.0
     exposed-modules: Yesod.Helpers.Static
     ghc-options:     -Wall
-
-executable             runtests
-    if flag(test)
-        Buildable: True
-        cpp-options:   -DTEST
-        build-depends: test-framework,
-                       test-framework-quickcheck2,
-                       test-framework-hunit,
-                       HUnit,
-                       QuickCheck >= 2 && < 3
-    else
-        Buildable: False
-    ghc-options:     -Wall
-    main-is:         runtests.hs
 
 source-repository head
   type:     git
