diff --git a/corebot-bliki.cabal b/corebot-bliki.cabal
--- a/corebot-bliki.cabal
+++ b/corebot-bliki.cabal
@@ -1,5 +1,5 @@
 name:           corebot-bliki
-version:        0.1
+version:        0.2
 synopsis:       A bliki written using yesod. Uses pandoc to process files stored in git.
 description:    Also provides a sample use of the library that uses $HOME/bliki for data and serves
                 to port 8080.
@@ -10,14 +10,61 @@
 license:        BSD3
 license-file:   LICENSE
 homepage:       http://github.com/coreyoconnor/corebot-bliki
+category:       yesod
+maintainer:     Corey O'Connor<coreyoconnor@gmail.com>
 
 source-repository head
     type:       git
     location:   git://github.com/coreyoconnor/corebot-bliki.git
     
+library 
+    hs-source-dirs: src
+    exposed-modules:    Yesod.CoreBot.Bliki
+                        Yesod.CoreBot.Bliki.Widgets
+                        Yesod.CoreBot.Bliki.Config
+    other-modules:      Yesod.CoreBot.Bliki.Store
+                        Yesod.CoreBot.Bliki.DB
+                        Yesod.CoreBot.Bliki.Resources.Blog
+                        Yesod.CoreBot.Bliki.Resources.Data
+                        Yesod.CoreBot.Bliki.Resources.Wiki
+                        Yesod.CoreBot.Bliki.Resources.Static
+                        Yesod.CoreBot.Bliki.Resources.Base
+                        Yesod.CoreBot.Bliki.Base
+                        Yesod.CoreBot.Bliki.Prelude
+                        Yesod.CoreBot.Bliki.Cache.UpdateHTML
+    extensions:     TemplateHaskell
+                    QuasiQuotes
+                    TypeFamilies
+                    MultiParamTypeClasses
+                    FlexibleInstances
+                    NoMonomorphismRestriction
+                    OverloadedStrings
+                    RankNTypes
+                    ScopedTypeVariables
+                    UndecidableInstances
+                    EmptyDataDecls
+                    GADTs
+                    FlexibleContexts
+    build-depends:  base             >= 4.4.0 && < 5,
+                    aeson            >= 0.3.2.11,
+                    blaze-builder    >= 0.3.0.1,
+                    bytestring       >= 0.9.2.0,
+                    containers       >= 0.4.1.0,
+                    directory        >= 1.1.0.1,
+                    filepath         >= 1.2.0.1,
+                    filestore        >= 0.4.0.5,
+                    http-types       >= 0.6.7,
+                    monads-tf        >= 0.1.0.0,
+                    pandoc           >= 1.9,
+                    text             >= 0.11.1.5,
+                    template-haskell >= 2.6.0.0,
+                    time             >= 1.2.0.5,
+                    yesod            >= 0.9.4.1
+
 executable corebot-bliki
     main-is: DefaultMain.hs
     hs-source-dirs: src
+    other-modules:  Paths_corebot_bliki
     extensions:     TemplateHaskell
                     QuasiQuotes
                     TypeFamilies
diff --git a/src/Yesod/CoreBot/Bliki.hs b/src/Yesod/CoreBot/Bliki.hs
new file mode 100644
--- /dev/null
+++ b/src/Yesod/CoreBot/Bliki.hs
@@ -0,0 +1,90 @@
+module Yesod.CoreBot.Bliki ( mk_bliki
+                           , default_blog_entry
+                           , indirect_load
+                           , Bliki_(..)
+                           , module Yesod.CoreBot.Bliki.Config
+                           , module Yesod.CoreBot.Bliki.Resources.Base
+                           ) where
+
+import Yesod.CoreBot.Bliki.Prelude
+
+import Yesod.CoreBot.Bliki.Base
+import Yesod.CoreBot.Bliki.Config
+
+import Yesod.CoreBot.Bliki.Resources.Base
+import qualified Yesod.CoreBot.Bliki.Resources.Blog as Blog
+import qualified Yesod.CoreBot.Bliki.Resources.Data as Data
+import qualified Yesod.CoreBot.Bliki.Resources.Static as Static
+import qualified Yesod.CoreBot.Bliki.Resources.Wiki as Wiki
+
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding
+
+-- | Constructs a Bliki component given the config.
+--
+-- This is parameterised by the Yesod app. Represented by the "master" type variable. With this type
+-- and the routes in Config the Bliki can coordinate between other subsites of the app. The other
+-- subsites support the resources required by the bliki. 
+--
+-- This would not need the routes in Config and the master type if a Bliki was entirely represented
+-- by a single Yesod subsite. I did not find that this provided the flexibility I wanted to
+-- supported both the the REST URIs for corebotllc.com and the URIs other people might want.
+--
+-- For an example of how a Bliki is constructed and integrated into a Yesod app see DefaultMain.hs
+mk_bliki :: Yesod master
+         => Config master
+         -> IO ( Bliki_ master )
+mk_bliki config = do
+    src_data <- Data.mk_data config
+    blog <- Blog.mk_blog src_data
+    wiki <- Wiki.mk_wiki src_data
+    return Bliki { data_res      = src_data
+                 , blog_res      = blog
+                 , wiki_res      = wiki
+                 }
+
+getMainR = do
+    bliki <- getYesodSub
+    defaultLayout $ do
+        default_blog_entry bliki
+
+indirect_load bliki data_R = do
+    let cfg = config $ data_res bliki
+    base_URL <- approot <$> ( lift getYesod )
+    let wiki_node_URL = render_absolute_URL base_URL $ data_routes cfg $ EntryLatestR []
+    addScript $ static_routes cfg $ FileR "jquery.min.js"
+    addScript $ static_routes cfg $ FileR "indirect_load.js"
+    -- XXX: the $(.blog_content) is not specific enough. Needs to be exactly the element tied to
+    -- this data_R
+    addHamletHead [hamlet|
+<script>
+    \$(document).ready( function() 
+    {
+        \$.get ( "@{data_R}"
+              , function( data ) 
+                {
+                    \$(".blog_content").html(data);
+                    process_HTML_for_wiki(data, $(".blog_content"), "#{wiki_node_URL}");
+                }
+              , 'html'
+              );
+    } );
+
+|]
+
+default_blog_entry bliki = do
+    let cfg = config $ data_res bliki
+        data_R = data_routes cfg $ LatestR
+    indirect_load bliki data_R
+    [whamlet|
+<div .blog_content>
+    Loading 
+    <a href=@{data_R}>
+        latest blog entry 
+    \ HTML content.
+|]
+
+mkYesodSubDispatch "Bliki_ master" [] [parseRoutes|
+/           MainR     GET
+|]
+
diff --git a/src/Yesod/CoreBot/Bliki/Base.hs b/src/Yesod/CoreBot/Bliki/Base.hs
new file mode 100644
--- /dev/null
+++ b/src/Yesod/CoreBot/Bliki/Base.hs
@@ -0,0 +1,19 @@
+module Yesod.CoreBot.Bliki.Base where
+
+import Yesod.CoreBot.Bliki.Prelude
+
+import Yesod.CoreBot.Bliki.Config
+import Yesod.CoreBot.Bliki.Resources.Base
+
+data Bliki_ master = Bliki
+    { data_res    :: Data_   master
+    , blog_res    :: Blog_   master
+    , wiki_res    :: Wiki_   master
+    }
+
+-- | The bliki can also be used as a subsite. If so then these are the routes supported by the
+-- Bliki_ subsite.
+mkYesodSubData "Bliki_ master" [] [parseRoutes|
+/           MainR     GET
+|]
+
diff --git a/src/Yesod/CoreBot/Bliki/Cache/UpdateHTML.hs b/src/Yesod/CoreBot/Bliki/Cache/UpdateHTML.hs
new file mode 100644
--- /dev/null
+++ b/src/Yesod/CoreBot/Bliki/Cache/UpdateHTML.hs
@@ -0,0 +1,135 @@
+module Yesod.CoreBot.Bliki.Cache.UpdateHTML where
+
+import Yesod.CoreBot.Bliki.Prelude
+
+import Yesod.CoreBot.Bliki.Config
+import Yesod.CoreBot.Bliki.Resources.Base
+import Yesod.CoreBot.Bliki.Store
+import Yesod.CoreBot.Bliki.DB
+
+import HereDoc
+
+import Control.Monad.Reader
+
+import qualified Data.ByteString.Lazy as B
+
+import Data.FileStore ( Revision(..) 
+                      , TimeRange(..)
+                      , Resource
+                      , RevisionId
+                      )
+import qualified Data.FileStore as FileStore
+
+import qualified Data.Text as Text
+
+import qualified Data.Text.Lazy          as TL
+import qualified Data.Text.Lazy.Encoding as TL
+
+import Text.Pandoc 
+import Text.Pandoc.Shared
+
+import System.Directory
+
+pandoc_write_options :: ConfigM master m
+                     => Maybe Bloggable 
+                     -> m WriterOptions
+pandoc_write_options mprev_update = do
+    extra_vars <- case mprev_update of
+                    Nothing -> return []
+                    Just (UpdateBloggable _ prev_rev_ID _) -> do
+                        link_URL <- revision_blog_URL prev_rev_ID
+                        return $ ( "prev-blog-update"
+                                 , prev_rev_ID 
+                                 ) :
+                                 ( "prev-blog-update-URL"
+                                 , link_URL
+                                 ) : []
+                    Just (WikiBloggable prev_entry_path prev_rev_ID _) -> do
+                        link_URL <- entry_at_rev_URL prev_entry_path prev_rev_ID
+                        return $ ( "prev-node-update"
+                                 , prev_entry_path
+                                 ) : 
+                                 ( "prev-node-update-rev"
+                                 , prev_rev_ID
+                                 ) :
+                                 ( "prev-node-update-URL"
+                                 , link_URL
+                                 ) : []
+    return $ defaultWriterOptions
+        { writerHtml5 = True
+        , writerHTMLMathMethod = MathJax "http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"
+        , writerEmailObfuscation = JavascriptObfuscation
+        , writerStandalone = True
+        , writerVariables = extra_vars ++ writerVariables defaultWriterOptions
+        , writerTemplate = [heredoc|
+$for(include-before)$
+$include-before$
+$endfor$
+$if(title)$
+<header>
+<h1 class="title">$title$</h1>
+$for(author)$
+<h2 class="author">$author$</h2>
+$endfor$
+$if(date)$
+<h3 class="date">$date$</h3>
+$endif$
+</header>
+$endif$
+$if(toc)$
+<nav id="$idprefix$TOC">
+$toc$
+</nav>
+$endif$
+$body$
+$for(include-after)$
+$include-after$
+$endfor$
+<div class="previous_update_summary">
+$if(prev-node-update)$
+Previously, node $prev-node-update$ was modified to revision <a href="$prev-node-update-URL$">$prev-node-update-rev$</a>
+$endif$
+$if(prev-blog-update)$
+Previously, blog for update revision <a href="$prev-blog-update-URL$">$prev-blog-update$</a>
+$endif$
+</div>
+|]
+    }
+
+build_blog_HTML :: ( MonadIO m, ConfigM master m )
+                => Maybe Bloggable 
+                -> RevisionId 
+                -> String 
+                -> m ()
+build_blog_HTML mprev_update rev_ID txt = do
+    out_path <- asks blog_HTML_path <*> pure rev_ID
+    out_exists <- liftIO $ doesFileExist out_path 
+    case out_exists of
+        True  -> return ()
+        False -> do
+            liftIO $ putStrLn $ "build HTML for " ++ rev_ID ++ " log"
+            let pandoc = readMarkdown defaultParserState txt
+            write_opts <- pandoc_write_options mprev_update
+            let html_string = writeHtmlString write_opts pandoc
+            liftIO $ cache_str out_path html_string
+    
+build_node_HTML :: ( MonadIO m, StoreM m, ConfigM master m )
+                => Maybe Bloggable 
+                -> RevisionId 
+                -> FilePath 
+                -> m ()
+build_node_HTML mprev_update rev_ID node_path = do
+    out_path <- asks node_HTML_path <*> pure rev_ID <*> pure node_path
+    out_exists <- liftIO $ doesFileExist out_path
+    case out_exists of
+        True  -> return ()
+        False -> do
+            liftIO $ putStrLn $ "build HTML for " ++ node_path ++ " " ++ show rev_ID
+            store_data <- data_for_node_rev node_path rev_ID
+            let markdown_data = FileStore.toByteString store_data
+                markdown_text = TL.unpack $ TL.decodeUtf8 markdown_data
+                pandoc        = readMarkdown    defaultParserState  markdown_text
+            write_opts <- pandoc_write_options mprev_update
+            let html_string   = writeHtmlString write_opts pandoc
+            liftIO $ cache_str out_path html_string
+
diff --git a/src/Yesod/CoreBot/Bliki/Config.hs b/src/Yesod/CoreBot/Bliki/Config.hs
new file mode 100644
--- /dev/null
+++ b/src/Yesod/CoreBot/Bliki/Config.hs
@@ -0,0 +1,57 @@
+module Yesod.CoreBot.Bliki.Config where
+
+import Yesod.CoreBot.Bliki.Prelude
+
+import Yesod.CoreBot.Bliki.DB
+import Yesod.CoreBot.Bliki.Store
+
+import Control.Monad.Reader.Class
+
+import Data.FileStore ( RevisionId )
+
+import qualified Data.Text as Text
+
+data Data_ master = Data 
+    { config             :: Config master
+    , store              :: Store
+    , update_thread_ID   :: ThreadId
+    , db_ref             :: IORef DB
+    }
+
+data Blog_ master = Blog ( Data_ master )
+
+data Wiki_ master = Wiki ( Data_ master )
+
+data Static 
+    = UseServer String
+    | UseDir FilePath
+
+-- I don't think there is a way to do subsites of subsites in Yesod? 
+-- Widgets that need to build links need the master routes?
+data Config master where
+    Config :: Yesod master => 
+        { store_dir :: FilePath
+        , cache_dir :: FilePath
+        , data_routes   :: Route ( Data_ master ) -> Route master
+        , blog_routes   :: Route ( Blog_ master ) -> Route master
+        , wiki_routes   :: Route ( Wiki_ master ) -> Route master
+        , static_routes :: Route Static -> Route master
+        , static_config :: Static
+        , site :: master
+        } -> Config master
+
+class ( Applicative m, MonadReader m, Yesod master, EnvType m ~ Config master ) => ConfigM master m
+instance ( Applicative m, MonadReader m, Yesod master, EnvType m ~ Config master ) => ConfigM master m
+
+node_markdown_path :: Yesod master => Config master -> FilePath -> FilePath
+node_markdown_path config node_path =
+    store_dir config </> node_path
+
+blog_HTML_path :: Yesod master => Config master -> RevisionId -> FilePath
+blog_HTML_path config rev_ID = 
+    cache_dir config </> rev_ID </> "_log"
+
+node_HTML_path :: Yesod master => Config master-> RevisionId -> FilePath -> FilePath
+node_HTML_path config rev_ID node_path = 
+    cache_dir config </> rev_ID </> node_path
+
diff --git a/src/Yesod/CoreBot/Bliki/DB.hs b/src/Yesod/CoreBot/Bliki/DB.hs
new file mode 100644
--- /dev/null
+++ b/src/Yesod/CoreBot/Bliki/DB.hs
@@ -0,0 +1,89 @@
+module Yesod.CoreBot.Bliki.DB where
+
+import Yesod.CoreBot.Bliki.Prelude
+
+import Control.Monad.Reader
+import Control.Monad.State.Strict 
+
+import Data.FileStore ( Revision(..) 
+                      , TimeRange(..)
+                      , Resource
+                      , RevisionId
+                      )
+import qualified Data.FileStore as FileStore
+
+import Data.Map ( Map )
+import qualified Data.Map as Map
+
+import Data.Time.Clock
+
+data DataUpdate 
+    = Tweet
+        { update_rev_ID :: RevisionId 
+        , tweet_text    :: String
+        }
+    | BlogAdded
+        { update_rev_ID :: RevisionId 
+        , blog_text     :: String
+        }
+    | EntryAdded
+        { update_rev_ID     :: RevisionId
+        , update_entry_path :: FilePath
+        }
+    | EntryChanged 
+        { update_rev_ID     :: RevisionId 
+        , update_entry_path :: FilePath
+        }
+    | Wibble
+        { update_rev_ID :: RevisionId
+        }
+    deriving ( Show, Eq )
+
+data Bloggable = WikiBloggable
+    { blog_entry  :: String
+    , view_rev    :: RevisionId
+    , prev_bloggable   :: Maybe Bloggable
+    } | UpdateBloggable
+    { blog_str    :: String
+    , source_rev  :: RevisionId
+    , prev_bloggable :: Maybe Bloggable
+    }
+
+-- XXX The order is always assumed to be from newest to oldest in the lists. This should be
+-- checked/enforced.
+-- XXX Use a database store like SQLite
+data DB = DB
+    { raw_history       :: [ Revision ]
+    , update_log        :: [ DataUpdate ]
+    , latest_revisions  :: Map FilePath RevisionId
+    , bloggables        :: [ Bloggable ]
+    }
+
+history_to_updates :: [ Revision ] -> [ DataUpdate ]
+history_to_updates (r : rs) = revision_to_updates r ++ history_to_updates rs
+
+revision_to_updates :: Revision -> [ DataUpdate ]
+revision_to_updates r = 
+    let txt = FileStore.revDescription r
+        rev_ID = FileStore.revId r
+    in map ( classify_change rev_ID ) ( FileStore.revChanges r )
+       ++ case length txt of
+            txt_len | txt_len > 200 -> [ BlogAdded rev_ID txt ]
+                    | txt_len > 8   -> [ Tweet     rev_ID txt ]
+                    | otherwise     -> [ Wibble    rev_ID     ]
+
+classify_change rev_ID  (FileStore.Added   page_path) = 
+    case head page_path of 
+        '.' -> Wibble rev_ID
+        _   -> EntryAdded rev_ID page_path
+classify_change rev_ID (FileStore.Deleted  page_path) = Wibble rev_ID
+classify_change rev_ID (FileStore.Modified page_path) = 
+    case head page_path of
+        '.' -> Wibble rev_ID
+        _   -> EntryChanged rev_ID page_path
+
+head_time :: IORef DB -> IO UTCTime
+head_time db_ref = do
+    db <- readIORef db_ref
+    return $ FileStore.revDateTime $ head $ raw_history db
+
diff --git a/src/Yesod/CoreBot/Bliki/Prelude.hs b/src/Yesod/CoreBot/Bliki/Prelude.hs
new file mode 100644
--- /dev/null
+++ b/src/Yesod/CoreBot/Bliki/Prelude.hs
@@ -0,0 +1,60 @@
+module Yesod.CoreBot.Bliki.Prelude ( module Yesod.CoreBot.Bliki.Prelude
+                                   , module Control.Applicative
+                                   , module Control.Concurrent
+                                   , module Control.Monad
+                                   , module Control.Monad.Fix
+                                   , module Data.Aeson
+                                   , module Data.FileStore
+                                   , module Data.IORef
+                                   , module Data.List
+                                   , module Data.Maybe
+                                   , module Data.Monoid
+                                   , module Data.Text
+                                   , module System.FilePath
+                                   , module Yesod
+                                   )
+where
+
+import Yesod hiding ( delete
+                    , deleteBy
+                    , insert 
+                    , insertBy
+                    , get
+                    , object
+                    )
+
+import Blaze.ByteString.Builder ( toByteString )
+
+import Control.Applicative
+import Control.Concurrent
+import Control.Monad
+import Control.Monad.Fix
+
+import Data.Aeson
+
+import Data.FileStore ( FileStore
+                      , Revision(..) 
+                      , TimeRange(..)
+                      , Resource
+                      , RevisionId
+                      )
+
+import Data.IORef
+import Data.List
+import Data.Maybe
+import Data.Monoid
+
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as TextEnc
+import Data.Text (Text)
+
+import Network.HTTP.Types 
+
+import System.FilePath hiding ( joinPath )
+
+render_absolute_URL :: RenderRoute r => Text -> Route r -> Text
+render_absolute_URL base_URL r = 
+    let route_subpath = fst $ renderRoute r
+        route_sub_URL = TextEnc.decodeUtf8 $ toByteString $ encodePathSegments route_subpath
+    in base_URL `Text.append` route_sub_URL
+
diff --git a/src/Yesod/CoreBot/Bliki/Resources/Base.hs b/src/Yesod/CoreBot/Bliki/Resources/Base.hs
new file mode 100644
--- /dev/null
+++ b/src/Yesod/CoreBot/Bliki/Resources/Base.hs
@@ -0,0 +1,54 @@
+module Yesod.CoreBot.Bliki.Resources.Base ( module Yesod.CoreBot.Bliki.Resources.Base
+                                          ) where
+
+import Yesod.CoreBot.Bliki.Prelude
+
+import Yesod.CoreBot.Bliki.Config
+import Yesod.CoreBot.Bliki.DB
+import Yesod.CoreBot.Bliki.Store
+
+import Control.Concurrent
+import Control.Monad.Reader.Class
+
+import qualified Data.Text as Text
+
+mkYesodSubData "Data_ master" [] [parseRoutes|
+/latest                     LatestR      GET
+/                           UpdateLogR   GET
+/entry/*Texts               EntryLatestR GET
+/blog/#RevisionId           BlogR        GET 
+/rev/#RevisionId/*Texts     EntryRevR    GET
+|]
+
+mkYesodSubData "Blog_ master" [] [parseRoutes|
+/         BlogIndexR       GET
+|]
+
+mkYesodSubData "Wiki_ master" [] [parseRoutes|
+/*Texts  WikiIndexR GET
+|]
+
+mkYesodSubData "Static" [] [parseRoutes|
+/#String  FileR GET
+|]
+
+revision_blog_URL :: ConfigM master m
+                  => RevisionId 
+                  -> m String
+revision_blog_URL rev_ID = do
+    config :: Config master <- ask
+    let base_URL = approot $ site config
+        rev_blog_R = data_routes config $ BlogR rev_ID
+    return $ Text.unpack $ render_absolute_URL base_URL rev_blog_R
+        
+    
+entry_at_rev_URL :: ConfigM master m
+                 => String 
+                 -> RevisionId 
+                 -> m String
+entry_at_rev_URL entry_path rev_ID = do
+    config :: Config master <- ask
+    let base_URL = approot $ site config
+        entry_rev_R = data_routes config $ EntryRevR rev_ID [ Text.pack entry_path ]
+    return $ Text.unpack $ render_absolute_URL base_URL entry_rev_R
+
diff --git a/src/Yesod/CoreBot/Bliki/Resources/Blog.hs b/src/Yesod/CoreBot/Bliki/Resources/Blog.hs
new file mode 100644
--- /dev/null
+++ b/src/Yesod/CoreBot/Bliki/Resources/Blog.hs
@@ -0,0 +1,59 @@
+module Yesod.CoreBot.Bliki.Resources.Blog where
+
+import Yesod.CoreBot.Bliki.Prelude
+
+import Yesod.CoreBot.Bliki.Resources.Base
+
+import Yesod.CoreBot.Bliki.Config 
+import Yesod.CoreBot.Bliki.DB 
+import Yesod.CoreBot.Bliki.Store 
+
+import qualified Data.Text as Text
+
+mk_blog :: Data_ master -> IO ( Blog_ master )
+mk_blog src_data = return $ Blog src_data 
+
+-- XXX: needs json representation
+getBlogIndexR :: Yesod master => GHandler ( Blog_ master ) master RepHtml
+getBlogIndexR = do
+    blog@(Blog src_data) <- getYesodSub
+    db <- liftIO $ readIORef $ db_ref src_data
+    let updates = take 50 $ update_log db
+        update_summaries = build_summaries updates
+        build_summaries [] = []
+        build_summaries ( Tweet     _ txt : us ) 
+            = [whamlet|
+                <p .tweet> #{txt}
+            |] : build_summaries us
+        build_summaries ( BlogAdded _ txt : us )
+            = [whamlet|
+                <p .blog_summary> #{take 200 txt}
+            |] : build_summaries us
+        build_summaries ( EntryAdded _ node_path : us ) = 
+            let node_R = data_routes (config src_data) $ EntryLatestR []
+            in [whamlet|
+                <p .node_update>
+                    Added 
+                    <a href=@{node_R}/#{node_path}>#{node_path}
+            |] : build_summaries us
+        build_summaries ( EntryChanged _ node_path : us ) = 
+            let node_R = data_routes (config src_data) $ EntryLatestR []
+            in [whamlet|
+                <p .node_update>
+                    Changed 
+                    <a href=@{node_R}/#{node_path}>#{node_path}
+            |] : build_summaries us
+        build_summaries ( Wibble _ : us )
+            = build_summaries us
+    defaultLayout $ do
+        [whamlet|
+<div .update_log>
+    <ol .summary_listing>
+        $forall summary <- update_summaries
+            <li> ^{summary}     
+|]
+
+mkYesodSubDispatch "Blog_ master" [] [parseRoutes|
+/         BlogIndexR       GET
+|]
+
diff --git a/src/Yesod/CoreBot/Bliki/Resources/Data.hs b/src/Yesod/CoreBot/Bliki/Resources/Data.hs
new file mode 100644
--- /dev/null
+++ b/src/Yesod/CoreBot/Bliki/Resources/Data.hs
@@ -0,0 +1,227 @@
+{-# LANGUAGE NamedFieldPuns #-}
+module Yesod.CoreBot.Bliki.Resources.Data where
+
+import Yesod.CoreBot.Bliki.Prelude 
+
+import Yesod.CoreBot.Bliki.Resources.Base 
+
+import Yesod.CoreBot.Bliki.Cache.UpdateHTML
+import Yesod.CoreBot.Bliki.Config
+import Yesod.CoreBot.Bliki.DB
+import Yesod.CoreBot.Bliki.Store
+
+import Control.Monad.Reader hiding ( lift )
+import Control.Monad.State.Strict hiding ( lift )
+
+import qualified Data.FileStore as FileStore
+
+import Data.Map ( Map )
+import qualified Data.Map as Map
+
+import qualified Data.Text               as T
+
+import Data.Time.Clock
+import Data.Time.Clock.POSIX
+
+import System.Directory ( createDirectory
+                        , doesDirectoryExist
+                        , removeDirectoryRecursive 
+                        )
+
+-- XXX: should be a RWST
+type DataM master a = StateT DB ( StateT Store (ReaderT ( Config master ) IO) ) a
+
+process_revisions :: Yesod master => ( Config master, Store ) -> IORef DB -> [ Revision ] -> IO ()
+process_revisions ( config, store ) db_ref rs = do
+    db  <- readIORef db_ref
+    let db_mod = execStateT (apply_revisions rs) db
+    db' <- runReaderT ( evalStateT db_mod store ) config 
+    writeIORef db_ref db'
+
+apply_revisions :: Yesod master => [ Revision ] -> DataM master ()
+apply_revisions [] = do
+    return ()
+apply_revisions [ r ] = do
+    let new_updates = revision_to_updates r
+    apply_updates new_updates
+    modify $ \db -> db { raw_history = r : raw_history db }
+    return ()
+apply_revisions (r : r_prev : rs) = do
+    apply_revisions ( r_prev : rs )
+    let new_updates = revision_to_updates r
+    apply_updates new_updates
+    modify $ \db -> db { raw_history = r : raw_history db }
+    return ()
+
+apply_updates :: Yesod master => [ DataUpdate ] -> DataM master ()
+apply_updates [] = do
+    return ()
+apply_updates ( u : us ) = do
+    apply_updates us
+    modify $ \db -> db { update_log = u : update_log db }
+    f u 
+    where 
+        f ( Wibble _ ) = do
+            return ()
+
+        f ( Tweet _ _ ) = do
+            return ()
+
+        f ( BlogAdded update_rev_ID blog_str ) = do
+            b <- add_bloggable $ UpdateBloggable blog_str update_rev_ID
+            build_blog_HTML ( prev_bloggable b ) update_rev_ID blog_str
+
+        f ( EntryAdded update_rev_ID update_entry_path ) = do
+            revs <- gets latest_revisions
+            let revs' = Map.insert update_entry_path update_rev_ID revs
+            modify $ \db -> db { latest_revisions = revs' }
+            b <- add_bloggable $ WikiBloggable update_entry_path update_rev_ID
+            lift $ build_node_HTML (prev_bloggable b) update_rev_ID update_entry_path
+
+        f ( EntryChanged update_rev_ID update_entry_path ) = do 
+            revs <- gets latest_revisions
+            let revs' = Map.insert update_entry_path update_rev_ID revs
+            modify $ \db -> db { latest_revisions = revs' }
+            bs <- gets bloggables
+            b <- case bs of
+                ( WikiBloggable blog_entry view_rev prev_bloggable : rest_bs )
+                    | blog_entry == update_entry_path -> do
+                        let b' = WikiBloggable blog_entry update_rev_ID prev_bloggable
+                        modify $ \db -> db { bloggables = b' : rest_bs }
+                        return b'
+                _ -> do
+                    add_bloggable $ WikiBloggable update_entry_path update_rev_ID 
+            lift $ build_node_HTML ( prev_bloggable b ) update_rev_ID update_entry_path
+
+add_bloggable :: ( Maybe Bloggable -> Bloggable ) -> DataM master Bloggable
+add_bloggable fb = do
+    bs <- gets bloggables
+    let b = case bs of
+                []         -> fb Nothing
+                b_prev : _ -> fb $ Just b_prev
+    modify $ \db -> db { bloggables = b : bs }
+    return b
+
+-- XXX: Should be event driven but that'd be harder
+update_thread :: Yesod master => ( Config master, Store ) -> IORef DB  -> IO ()
+update_thread ( config, store ) db_ref = do
+    -- XXX: Lower bound to FileStore.history is not exclusive
+    let inc_a_bit = addUTCTime (fromInteger 1)
+    prev_time_ref <- newIORef =<< return . inc_a_bit =<< head_time db_ref
+    let update_thread_ = do
+            prev_time <- readIORef prev_time_ref
+            putStrLn $ "probing for changes since " ++ show prev_time
+            rs <- FileStore.history ( filestore store ) 
+                                    [] 
+                                    ( TimeRange (Just prev_time) 
+                                                Nothing
+                                    )
+            case null rs of
+                True -> return ()
+                False -> do
+                    putStrLn "found updates"
+                    process_revisions ( config, store ) db_ref rs
+                    writeIORef prev_time_ref =<< return . inc_a_bit 
+                                             =<< head_time db_ref
+            -- delay before probing for updates again
+            threadDelay 10000000
+    forever update_thread_
+
+mk_data :: Yesod master => Config master -> IO ( Data_ master )
+mk_data config = do
+    -- clear memoization store
+    should_clear_memo_store <- doesDirectoryExist $ cache_dir config
+    when should_clear_memo_store $ removeDirectoryRecursive $ cache_dir config
+    createDirectory $ cache_dir config
+    -- build internal DB state
+    let filestore = FileStore.gitFileStore $ store_dir config
+        store = Store { filestore = filestore }
+        empty_db = DB [] [] Map.empty []
+    initial_history <- FileStore.history filestore [] (TimeRange Nothing Nothing) 
+    let db_0_build = execStateT (apply_revisions initial_history) empty_db
+    db_ref <- newIORef =<< runReaderT (evalStateT db_0_build store) config
+    -- XXX: Only because store is not pure value but a reference
+    the_ID <- forkIO $ update_thread ( config, store ) db_ref
+    return Data { config            = config
+                , store             = store
+                , update_thread_ID  = the_ID
+                , db_ref            = db_ref
+                }
+
+node_HTML_content :: Yesod master => Data_ master -> DB -> FilePath -> Content
+node_HTML_content src_data db node_path = 
+    let Just rev_ID = Map.lookup node_path ( latest_revisions db )
+        out_path = node_HTML_path (config src_data) rev_ID node_path
+    in ContentFile out_path Nothing
+
+blog_HTML_content :: Yesod master => Data_ master -> RevisionId -> Content
+blog_HTML_content src_data rev_ID = 
+    let out_path = blog_HTML_path (config src_data) rev_ID
+    in ContentFile out_path Nothing
+
+getBlogR  :: Yesod master => RevisionId -> GHandler ( Data_ master ) master [(ContentType, Content)]
+getBlogR rev_ID = do
+    src_data <- getYesodSub
+    let out_HTML_content = blog_HTML_content src_data rev_ID
+    return [ ( typeHtml, out_HTML_content )
+           ]
+    
+getLatestR :: Yesod master => GHandler ( Data_ master ) master [(ContentType, Content)]
+getLatestR = do
+    src_data <- getYesodSub 
+    db <- liftIO $ readIORef $ db_ref src_data
+    let latest = head $ bloggables db
+    case latest of
+        UpdateBloggable blog_str source_rev _ -> do
+            let out_HTML = blog_HTML_content src_data source_rev
+            return [ ( typeHtml, out_HTML )
+                   , ( typePlain, toContent blog_str )
+                   ]
+        WikiBloggable blog_entry _ _  -> do
+            let markdown_path = node_markdown_path (config src_data) blog_entry
+            let out_HTML  = node_HTML_content src_data db blog_entry
+            return [ ( typeHtml , out_HTML )
+                   , ( typePlain, ContentFile markdown_path Nothing ) 
+                   ]
+
+getUpdateLogR :: Yesod master => GHandler ( Data_ master ) master RepJson
+getUpdateLogR = do
+    jsonToRepJson $ toJSON ()
+
+getEntryRevR :: Yesod master 
+             => RevisionId 
+             -> [ Text ] 
+             -> GHandler ( Data_ master ) master [(ContentType, Content)]
+getEntryRevR rev_ID entry_path_texts = do
+    src_data <- getYesodSub
+    let ( first_path : rest_paths ) = map T.unpack entry_path_texts
+        node_path = foldl (</>) first_path rest_paths
+    let p = node_HTML_path (config src_data) rev_ID node_path
+    let markdown_path = node_markdown_path (config src_data) node_path
+    return [ ( typeHtml , ContentFile p Nothing  )
+           , ( typePlain, ContentFile markdown_path Nothing ) 
+           ]
+    
+getEntryLatestR :: Yesod master => [ Text ] -> GHandler ( Data_ master ) master [(ContentType, Content)]
+getEntryLatestR entry_path_texts = do
+    let ( first_path : rest_paths ) = map T.unpack entry_path_texts
+        node_path = foldl (</>) first_path rest_paths
+    src_data <- getYesodSub
+    db <- liftIO $ readIORef $ db_ref src_data
+    let x = Map.lookup node_path ( latest_revisions db )
+    case x of
+        Nothing -> do
+            liftIO $ putStrLn $ "no node at path " ++ show node_path
+            fail $ "no node at path " ++ show node_path
+        Just rev_ID -> do
+            liftIO $ putStrLn $ "latest rev of " ++ show node_path ++ " is " ++ show rev_ID
+            getEntryRevR rev_ID entry_path_texts
+
+mkYesodSubDispatch "Data_ master" [] [parseRoutes|
+/latest                     LatestR      GET
+/                           UpdateLogR   GET
+/entry/*Texts               EntryLatestR GET
+/blog/#RevisionId           BlogR        GET 
+/rev/#RevisionId/*Texts     EntryRevR    GET
+|]
+
diff --git a/src/Yesod/CoreBot/Bliki/Resources/Static.hs b/src/Yesod/CoreBot/Bliki/Resources/Static.hs
new file mode 100644
--- /dev/null
+++ b/src/Yesod/CoreBot/Bliki/Resources/Static.hs
@@ -0,0 +1,29 @@
+module Yesod.CoreBot.Bliki.Resources.Static where
+
+import Yesod.CoreBot.Bliki.Prelude
+
+import Yesod.CoreBot.Bliki.Base
+import Yesod.CoreBot.Bliki.Config
+import Yesod.CoreBot.Bliki.Resources.Base
+
+import System.Directory ( doesFileExist )
+
+getFileR :: Yesod m => String -> GHandler Static m ()
+getFileR file_name = do
+    mode <- getYesodSub
+    case mode of
+        UseServer server -> fail "XXX: UseServer server should not be hit"
+        UseDir dir -> do
+            let file_path = dir </> file_name
+                file_type = case takeExtension file_path of
+                                ".js" -> typeJavascript
+                                _     -> typePlain
+            exists <- liftIO $ doesFileExist file_path
+            case exists of
+                True  -> sendFile file_type file_path
+                False -> notFound
+    
+mkYesodSubDispatch "Static" [] [parseRoutes|
+/#String  FileR GET
+|]
+
diff --git a/src/Yesod/CoreBot/Bliki/Resources/Wiki.hs b/src/Yesod/CoreBot/Bliki/Resources/Wiki.hs
new file mode 100644
--- /dev/null
+++ b/src/Yesod/CoreBot/Bliki/Resources/Wiki.hs
@@ -0,0 +1,43 @@
+module Yesod.CoreBot.Bliki.Resources.Wiki where
+
+import Yesod.CoreBot.Bliki.Prelude
+
+import Yesod.CoreBot.Bliki.Base
+import Yesod.CoreBot.Bliki.Config
+import Yesod.CoreBot.Bliki.Store 
+import Yesod.CoreBot.Bliki.Resources.Base
+
+import Control.Monad.State.Strict
+
+import Data.FileStore
+import qualified Data.Text as Text
+import Data.Time.Clock.POSIX
+
+mk_wiki :: Data_ master -> IO ( Wiki_ master )
+mk_wiki src_data = return $ Wiki src_data 
+
+-- XXX: needs json representation
+getWikiIndexR :: Yesod master => [ Text ] -> GHandler ( Wiki_ master ) master RepHtml
+getWikiIndexR node_path = do
+    liftIO $ putStrLn $ "index for node " ++ show node_path
+    wiki@(Wiki src_data) <- getYesodSub
+    defaultLayout $ do
+        let store_path = foldl (</>) "" $ map Text.unpack node_path
+        listing <- evalStateT (directory_listing store_path) ( store src_data )
+        let entry_names = [ Text.pack name | FSFile name      <- listing ]
+            node_names  = [ Text.pack name | FSDirectory name <- listing ]
+        let data_URL = data_routes (config src_data) $ EntryLatestR []
+            index_URL = wiki_routes (config src_data) $ WikiIndexR []
+        [whamlet|
+        <div .wiki_index>
+            <ul>/#{store_path}
+                $forall node_name <- node_names
+                    <li .node_name><a href=@{index_URL}/#{store_path}/#{node_name}>#{node_name}
+                $forall entry_name <- entry_names
+                    <li .entry_name><a href=@{data_URL}/#{store_path}/#{entry_name}>#{entry_name}
+|]
+
+mkYesodSubDispatch "Wiki_ master" [] [parseRoutes|
+/*Texts  WikiIndexR GET
+|]
+
diff --git a/src/Yesod/CoreBot/Bliki/Store.hs b/src/Yesod/CoreBot/Bliki/Store.hs
new file mode 100644
--- /dev/null
+++ b/src/Yesod/CoreBot/Bliki/Store.hs
@@ -0,0 +1,47 @@
+module Yesod.CoreBot.Bliki.Store where
+
+import Yesod.CoreBot.Bliki.Prelude
+
+import Control.Monad.State.Class
+import Control.Monad.Trans
+
+import qualified Data.ByteString.Lazy as B
+
+import qualified Data.FileStore as FileStore
+
+import qualified Data.Text.Lazy          as TL
+import qualified Data.Text.Lazy.Encoding as TL
+
+import System.Directory
+
+-- XXX: The inner FileStore is not actually pure.
+data Store = Store
+    { filestore  :: FileStore
+    }
+
+class ( MonadIO m, Applicative m, MonadState m, StateType m ~ Store ) => StoreM m
+instance ( MonadIO m, Applicative m, MonadState m, StateType m ~ Store ) => StoreM m
+
+directory_listing :: StoreM m => FilePath -> m [ Resource ]
+directory_listing node_path = do
+    store <- get
+    liftIO $ FileStore.directory (filestore store) node_path
+
+cache_str :: FilePath -> String -> IO ()
+cache_str out_path str = do
+    let text = TL.pack str
+        bs = TL.encodeUtf8 text
+        tmp_out   = out_path ++ ".tmp"
+    createDirectoryIfMissing True (takeDirectory tmp_out)
+    B.writeFile tmp_out bs
+    renameFile tmp_out out_path
+    return ()
+
+data_for_node_rev :: StoreM m
+                  => FilePath 
+                  -> RevisionId 
+                  -> m B.ByteString
+data_for_node_rev node_path rev_ID = do
+    fs <- gets filestore
+    liftIO $ FileStore.smartRetrieve fs True node_path (Just rev_ID) 
+
diff --git a/src/Yesod/CoreBot/Bliki/Widgets.hs b/src/Yesod/CoreBot/Bliki/Widgets.hs
new file mode 100644
--- /dev/null
+++ b/src/Yesod/CoreBot/Bliki/Widgets.hs
@@ -0,0 +1,17 @@
+{- Widgets useful for building the wiki site. 
+ - See DefaultMain.hs for an example of all of these in use at once.
+ -}
+module Yesod.CoreBot.Bliki.Widgets where
+
+import Yesod.CoreBot.Bliki.Prelude
+
+import Yesod.CoreBot.Bliki.Base
+import Yesod.CoreBot.Bliki.Config
+import Yesod.CoreBot.Bliki.Resources.Base
+
+data NavWidget = NavWidget
+
+instance ToWidget sub master NavWidget where
+    toWidget _ = do
+        return ()
+
