diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2010, Jonathan Daugherty.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * The names of the contributors may not be used to endorse or
+      promote products derived from this software without specific
+      prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README b/README
new file mode 100644
--- /dev/null
+++ b/README
@@ -0,0 +1,209 @@
+
+mathblog
+--------
+
+mathblog is a Haskell program targeted at people who want to write
+statically-generated, mathematically-themed weblogs.
+
+Project vision
+--------------
+
+I wrote mathblog with a very specific set of requirements in mind,
+motivated by the following principles:
+
+ - a blog should be easy to create, host, and update.
+
+ - a blog should be easy to maintain.
+
+ - I should be able to edit posts in my editor of choice and write
+   them in an intelligent textual markup language.
+
+ - It should be easy to embed high-quality mathematical symbols and
+   equations in the blog posts.
+
+As a result, mathblog has the following properties:
+
+ - the software is composed of a single executable which will
+   automatically take care of creating your blog and regenerating
+   pages when your post markup changes.
+
+ - all content is stored in plain text files and is generated
+   statically.  No database, web framework, or templating system is
+   used.
+
+ - a mathblog can be hosted with a simple static fileserver such as
+   thttpd, lighttpd, or apache.
+
+ - blog posts are written in the Markdown format with extensions, as
+   supported by the Pandoc document converter.
+
+ - Math is embedded with $...$ and $$...$$.
+
+These properties have some nice advantages; your blog content is
+cacheable and can be subjected to revision control.  Posts are easy to
+edit and editing doesn't require a web browser.  The static file
+representation model means you can compose a blog post on your laptop
+and get it just right using a local installation of mathblog, then
+push it up to your server to post it to your public blog.
+
+Dependencies
+------------
+
+mathblog takes advantage of three primary software components:
+
+ - pandoc, which will be installed for you automatically if you use
+   "cabal" to install mathblog.  For best results, use the revision
+   control version of Pandoc.
+
+ - gladTeX, a package of tools for rendering LaTeX equations.  At
+   present you MUST get gladTeX from revision control, since mathblog
+   requires features not yet released.
+
+ - a TeX distribution, such as TeX-live
+
+How to use
+----------
+
+mathblog doesn't have any persistent configuration file; the way you
+configure it is by setting two environment variables:
+
+  MB_BASE_DIR
+
+    The location of your blog files on the filesystem.  Must be an
+    absolute path.
+
+  MB_BASE_URL
+
+    The base URL of your blog as it will be hosted.  This is used to
+    generate fully-qualified URLs.  A trailing slash is not expected.
+    E.g., "http://localhost:8080".
+
+Once you've set those environment variables, just run "mb".  It will
+take care of setting up a new blog data directory for you, complete
+with some default pages and a default first blog post:
+
+  $ export MB_BASE_URL="http://localhost:8080"
+  $ export MB_BASE_DIR=`pwd`/myBlog
+  $ mb
+  mb: using base directory "/home/.../myBlog"
+  mb: using base url "http://localhost:8080"
+  Setting up data directory using skeleton: /home/.../.cabal/share/mathblog-0.1/skel
+  Processing: first-post
+  Generating all-posts list.
+  Done.
+
+Now you have a blog; fire up a web server pointing at myBlog/html and
+you'll see a demo post.
+
+Next, you might want to edit or create a new post, or even remove one.
+Just edit the appropriate file in myBlog/posts/ accordingly and re-run
+mb.  Here's an example of adding a new post:
+
+  $ mb
+  mb: using base directory "/home/.../blog/myBlog"
+  mb: using base url "http://localhost:8080"
+  Processing: second-post
+  Generating all-posts list.
+  Done.
+
+Post format
+-----------
+
+Posts are formatted in Markdown and support the extended Markdown
+syntax as implemented by Pandoc.  The only important convention to
+note is that the post title goes on the first line of the file
+following a percent sign, as follows:
+
+  %My First Post
+
+  First paragraph starts here.
+
+Running mb in "listen" mode
+---------------------------
+
+Ordinarily, you'll just run "mb" once in a while to update your
+content.  But if you're in the middle of authoring a new post and you
+want to see what it looks like as you edit it rather than run "mb" by
+hand periodically, you can run "mb" in "listen" mode; this means that
+mb will run forever, peroidically scanning the filesystem for changes
+to your blog posts and other data files and regenerate them when it
+detects a change.  Running mathblog in listen mode is simple:
+
+  $ mb -l
+  mb: using base directory "/home/.../myBlog"
+  mb: using base url "http://localhost:8080"
+
+  Changes detected:
+    /home/.../myBlog/posts/second-post.txt
+  Processing: second-post
+  Generating all-posts list.
+  Done.
+
+Above I ran "mb" in listen mode and then modified one of the posts,
+second-post.txt, and "mb" detected the change and ran its typical
+regeneration routine.
+
+Customizing your blog
+---------------------
+
+It's likely that you'll want to customize the look and feel of your
+blog.  To this end, mathblog generates the pages of your blog by
+assembling various pieces of the page to create the final result.  The
+biggest piece of a generated page is the blog post itself, but the
+surrounding elements are read from various files that are created by
+mathblog when it creates your blog data directory.  These files are
+stored in the "templates/" subdirectory of your blog data directory
+and are as follows:
+
+  templates/rssPreamble.xml
+  templates/rssPostamble.xml
+
+    These files make up the leading and trailing RSS XML that will be
+    used to wrap the individual elements of your blog's RSS feed.
+    You'll probably want to change the rssPreamble.xml file, since it
+    contains information about the blog author.
+
+  templates/pagePreamble.html
+  templates/pagePostamble.html
+
+    These files make up the outermost HTML content of the blog post
+    pages, including the index page and the post list page.
+
+  templates/postPreamble.html
+  templates/postPostamble.html
+
+    These files make up the HTML content surrounding the post body
+    itself.
+
+In addition, subdirectories of the "html/" directory contain things
+you might want to customize, such as a CSS stylesheet.
+
+Other features
+--------------
+
+Since mathblog doesn't provide many moving parts, it's up to you to
+outsource various web site features, such as comments.  I've
+successfully integrated mathblog with the Disqus comments service.  To
+do this, some javascript needs to be embedded in the blog pages.
+Disqus works best when you supply it with a page identifier so it can
+guarantee that comments are post-specific rather than URL-specific.
+The way mathblog makes this possible is by exposing a JavaScript
+variable to other scripts in your page:
+
+  Blog.pageName = "foobar-baz";
+
+This variable name can be used by scripts you embed, such as with
+Disqus comment forms:
+
+  var disqus_identifier = Blog.pageName;
+
+Afterword
+---------
+
+I personally use this software package but I'll be pleased if others
+find it useful.  In addition, I'm open to accepting contributions on
+the project if they're consistent with what I've outlined above.
+Happy blogging!
+
+Jonathan Daugherty
+drcygnus@gmail.com
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/mathblog.cabal b/mathblog.cabal
new file mode 100644
--- /dev/null
+++ b/mathblog.cabal
@@ -0,0 +1,54 @@
+Name:                mathblog
+Version:             0.1
+Synopsis:            A program for creating and managing a
+                     static, mathematically-inclined weblog
+Description:         This package provides a program for creating and
+                     managing a statically-generated, VCS-friendly,
+                     mathematically-inclined weblog.  If you're
+                     interested in managing a blog with few moving
+                     parts and support for embedded LaTeX math and the
+                     UNIX editor of your choice, then this is the
+                     blogging platform for you!  For detailed
+                     information, please see the README.
+Author:              Jonathan Daugherty <drcygnus@gmail.com>
+Maintainer:          Jonathan Daugherty <drcygnus@gmail.com>
+Build-Type:          Simple
+Cabal-version:       >= 1.2
+License:             BSD3
+License-file:        LICENSE
+Category:            Web
+Homepage:            http://repos.codevine.org/mathblog/
+
+Data-Files:
+  README
+  skel/html/images/older.png
+  skel/html/images/newer-subdued.png
+  skel/html/images/newer.png
+  skel/html/images/older-subdued.png
+  skel/html/stylesheets/stylesheet.css
+  skel/templates/postPostamble.html
+  skel/templates/postPreamble.html
+  skel/templates/pagePreamble.html
+  skel/templates/pagePostamble.html
+  skel/templates/rssPreamble.xml
+  skel/templates/rssPostamble.xml
+  skel/posts/first-post.txt
+
+Executable mb
+  Build-depends:
+    base >= 3 && < 5,
+    directory >= 1.0 && < 1.1,
+    filepath >= 1.1 && < 1.2,
+    pandoc >= 1.6 && < 1.7,
+    unix >= 2.3 && < 2.5,
+    process >= 1.0 && < 1.1,
+    time >= 1.1 && < 1.2,
+    old-locale >= 1.0 && < 1.1
+
+  Other-modules:
+    Paths_mathblog
+
+  GHC-Options: -Wall
+
+  Hs-Source-Dirs:  src
+  Main-is:         Main.hs
diff --git a/skel/html/images/newer-subdued.png b/skel/html/images/newer-subdued.png
new file mode 100644
Binary files /dev/null and b/skel/html/images/newer-subdued.png differ
diff --git a/skel/html/images/newer.png b/skel/html/images/newer.png
new file mode 100644
Binary files /dev/null and b/skel/html/images/newer.png differ
diff --git a/skel/html/images/older-subdued.png b/skel/html/images/older-subdued.png
new file mode 100644
Binary files /dev/null and b/skel/html/images/older-subdued.png differ
diff --git a/skel/html/images/older.png b/skel/html/images/older.png
new file mode 100644
Binary files /dev/null and b/skel/html/images/older.png differ
diff --git a/skel/html/stylesheets/stylesheet.css b/skel/html/stylesheets/stylesheet.css
new file mode 100644
--- /dev/null
+++ b/skel/html/stylesheets/stylesheet.css
@@ -0,0 +1,110 @@
+html {
+  background: #aaa;
+}
+img {
+  border: none;
+}
+body {
+  font-size: 16px;
+  margin-left: 3em;
+  margin-right: 3em;
+  background: #fff;
+  margin-top: 0px;
+  min-height: 900px;
+}
+#page {
+  padding-top: 1em;
+  padding-left: 1em;
+  padding-right: 1em;
+  margin-left: auto;
+  margin-right: auto;
+  max-width: 700px;
+}
+#header {
+  font-size: 26px;
+}
+h1 {
+  font-size: 32px;
+  font-weight: normal;
+}
+h3 {
+  font-weight: normal;
+}
+#header a {
+  color: #00f;
+  text-decoration: none;
+}
+a#listing {
+  float: right;
+  font-size: 14px;
+  margin-top: 0.25em;
+}
+div#all-posts {
+  margin-top: 1.5em;
+}
+div.listing-entry {
+  margin-top: 0.5em;
+  padding-bottom: 0.5em;
+}
+span.post-title {
+  font-size: 20px;
+  display: block;
+}
+span.post-title a {
+  text-decoration: none;
+  color: blue;
+}
+span.post-created {
+  font-size: 15px;
+  display: block;
+  margin-left: 1em;
+  color: #555;
+}
+div#prev-next-links {
+  margin-top: 1em;
+}
+.prev-link {
+  color: blue;
+  text-decoration: none;
+  background-image: url('/images/newer.png');
+  background-repeat: no-repeat;
+  background-position: 0px 5px;
+  padding-left: 30px;
+}
+.prev-link-subdued {
+  color: #aaa;
+  background-image: url('/images/newer-subdued.png');
+  background-repeat: no-repeat;
+  background-position: 0px 5px;
+  padding-left: 30px;
+}
+.next-link {
+  float: right;
+  color: blue;
+  text-decoration: none;
+  background-image: url('/images/older.png');
+  background-repeat: no-repeat;
+  background-position: right 5px;
+  padding-right: 25px;
+}
+.next-link-subdued {
+  color: #aaa;
+  float: right;
+  background-image: url('/images/older-subdued.png');
+  background-repeat: no-repeat;
+  background-position: right 5px;
+  padding-right: 25px;
+}
+#footer {
+  border-top: 1px solid #ddd;
+  color: #555;
+  margin-top: 1em;
+  padding-top: 0.5em;
+  font-size: smaller;
+}
+#post h1 {
+  margin-bottom: 0px;
+}
+#post span.post-created {
+  color: #999;
+}
diff --git a/skel/posts/first-post.txt b/skel/posts/first-post.txt
new file mode 100644
--- /dev/null
+++ b/skel/posts/first-post.txt
@@ -0,0 +1,44 @@
+%A first post $p = 0$
+
+You've successfully set up your shiny new math blog!
+
+This initial post is here to show how the software works.  Posts are
+written in the Markdown format using <a
+href="http://johnmacfarlane.net/pandoc/">Pandoc</a>.  They employ an
+extended Markdown syntax as described in the <a
+href="http://johnmacfarlane.net/pandoc/README.html#pandocs-markdown-vs.standard-markdown">Pandoc
+User Guide</a>.  You can embed LaTeX math in your posts; it will be
+processed with <a
+href="http://ans.hsh.no/home/mgg/gladtex/">gladTeX</a>.
+
+Inline math is introduced with `$...$` and looks like $x \in Y$.  Math
+can be embedded in post titles, as well!  To insert a block-level
+equation, use `$$...$$` as a block-level Markdown element.  This
+causes the math to be typeset in the `displaymath` environment.  The
+results look like this:
+
+$$\int^n_0{\ldots} = \sum^n_{i=1}{b_0 G(u^s)}
+\Longrightarrow \frac{\tan x}{\lim_{h \to \infty}{\Phi}}$$
+
+as opposed to this:
+
+$\int^n_0{\ldots} = \sum^n_{i=1}{b_0 G(u^s)}
+\Longrightarrow \frac{\tan x}{\lim_{h \to \infty}{\Phi}}$
+
+The content of this post can be found in the `posts` subdirectory of
+your blog data directory; the rest of the content of this page can be
+found in "preamble" and "postamble" files in the `templates`
+subdirectory.  You can modify those files and re-run `mb` to
+regenerate all of your blog content.  Note that some defaults, such as
+the blog title and footer text, are probably bogus and need to be
+changed.
+
+Note that `mb` will not bother to regenerate the actual content of
+your blog posts unless the corresponding source file in `posts` has
+changed.
+
+Once you have the hang of creating posts, you'll probably want to
+delete this one; just delete it from the `posts` subdirectory and run
+`mb`.
+
+Enjoy!
diff --git a/skel/templates/pagePostamble.html b/skel/templates/pagePostamble.html
new file mode 100644
--- /dev/null
+++ b/skel/templates/pagePostamble.html
@@ -0,0 +1,6 @@
+      <div id="footer">
+        Copyright &copy; 2010 Your Name Here
+      </div>
+    </div>
+  </body>
+</html>
diff --git a/skel/templates/pagePreamble.html b/skel/templates/pagePreamble.html
new file mode 100644
--- /dev/null
+++ b/skel/templates/pagePreamble.html
@@ -0,0 +1,12 @@
+<html>
+  <head>
+    <title>A Blog</title>
+    <link rel="stylesheet" type="text/css" href="/stylesheets/stylesheet.css"/>
+    <link rel="alternate" type="application/rss+xml" href="/feed.xml"/>
+  </head>
+  <body>
+    <div id="page">
+      <div id="header">
+        <a id="listing" href="/posts/">all posts</a>
+        <a href="/">A Blog</a>
+      </div>
diff --git a/skel/templates/postPostamble.html b/skel/templates/postPostamble.html
new file mode 100644
--- /dev/null
+++ b/skel/templates/postPostamble.html
@@ -0,0 +1,1 @@
+</div>
diff --git a/skel/templates/postPreamble.html b/skel/templates/postPreamble.html
new file mode 100644
--- /dev/null
+++ b/skel/templates/postPreamble.html
@@ -0,0 +1,1 @@
+<div id="post">
diff --git a/skel/templates/rssPostamble.xml b/skel/templates/rssPostamble.xml
new file mode 100644
--- /dev/null
+++ b/skel/templates/rssPostamble.xml
@@ -0,0 +1,2 @@
+</channel>
+</rss>
diff --git a/skel/templates/rssPreamble.xml b/skel/templates/rssPreamble.xml
new file mode 100644
--- /dev/null
+++ b/skel/templates/rssPreamble.xml
@@ -0,0 +1,8 @@
+<rss version="2.0">
+  <channel>
+    <title>A Blog</title>
+    <link>http://localhost:8080/</link>
+    <description>A Blog.</description>
+    <language>en-us</language>
+    <managingEditor>your@email.com</managingEditor>
+    <webMaster>your@email.com</webMaster>
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -0,0 +1,414 @@
+module Main where
+
+import Control.Applicative
+    ( (<*>)
+    , pure
+    )
+import Control.Monad
+    ( when
+    , forM_
+    )
+import Control.Concurrent
+    ( threadDelay
+    )
+import System.IO
+    ( IOMode(WriteMode)
+    , Handle
+    , openFile
+    , hPutStr
+    , hClose
+    )
+import System.Exit
+    ( exitFailure
+    )
+import System.Environment
+    ( getEnvironment
+    , getArgs
+    )
+import System.Directory
+    ( doesDirectoryExist
+    , doesFileExist
+    , getDirectoryContents
+    , removeFile
+    , copyFile
+    , createDirectory
+    )
+import System.FilePath
+    ( (</>)
+    )
+import System.Posix.Files
+    ( getFileStatus
+    , modificationTime
+    , createSymbolicLink
+    )
+import Data.List
+    ( sortBy
+    , isSuffixOf
+    )
+import Data.Maybe
+    ( isNothing
+    )
+import Data.Time.LocalTime
+    ( TimeZone(timeZoneName)
+    , getCurrentTimeZone
+    )
+import Data.Time.Clock
+    ( UTCTime
+    , getCurrentTime
+    )
+import qualified Text.Pandoc as Pandoc
+import MB.Util
+    ( copyTree
+    , toUtcTime
+    , toLocalTime
+    , pandocTitle
+    , pandocTitleRaw
+    , rssModificationTime
+    )
+import MB.Gladtex
+    ( gladTex
+    , checkForGladtex
+    )
+import MB.Types
+import qualified MB.Files as Files
+import Paths_mathblog
+    ( getDataFileName
+    )
+
+skelDir :: IO FilePath
+skelDir = getDataFileName "skel"
+
+baseDirEnvName :: String
+baseDirEnvName = "MB_BASE_DIR"
+
+baseUrlEnvName :: String
+baseUrlEnvName = "MB_BASE_URL"
+
+allPostFilenames :: Config -> IO [FilePath]
+allPostFilenames config = do
+  allFiles <- getDirectoryContents $ postSourceDir config
+  return [ postSourceDir config </> f | f <- allFiles
+         , ".txt" `isSuffixOf` f
+         ]
+
+getModificationTime :: FilePath -> IO UTCTime
+getModificationTime fullPath = do
+  info <- getFileStatus fullPath
+  return $ toUtcTime $ modificationTime info
+
+loadPost :: FilePath -> IO Post
+loadPost fullPath = do
+  fileContent <- readFile fullPath
+  t <- getModificationTime fullPath
+  let doc = Pandoc.readMarkdown Pandoc.defaultParserState fileContent
+
+  return $ Post { postTitle = pandocTitle doc
+                , postTitleRaw = pandocTitleRaw doc
+                , postFilename = fullPath
+                , postModificationTime = t
+                , postAst = doc
+                }
+
+allPosts :: Config -> IO [Post]
+allPosts config = do
+  postFiles <- allPostFilenames config
+
+  -- For each file, construct a Post from it.
+  posts <- mapM loadPost postFiles
+
+  -- Return posts sorted by modification time, descending
+  return $ sortBy (\a b -> postModificationTime b `compare`
+                           postModificationTime a) posts
+
+pandocWriterOptions :: Pandoc.WriterOptions
+pandocWriterOptions =
+    Pandoc.defaultWriterOptions { Pandoc.writerHTMLMathMethod = Pandoc.GladTeX
+                                }
+
+writePost :: Handle -> Post -> IO ()
+writePost h post = do
+  created <- postModificationString post
+  hPutStr h $ "<h1>" ++ postTitle post 175 ++ "</h1>"
+  hPutStr h $ "<span class=\"post-created\">Posted " ++ created ++ "</span>"
+  hPutStr h $ Pandoc.writeHtmlString pandocWriterOptions (postAst post)
+
+buildLinks :: Maybe Post -> Maybe Post -> String
+buildLinks prev next =
+    "<div id=\"prev-next-links\">"
+      ++ link "next-link" "older" next
+      ++ link "prev-link" "newer" prev
+      ++ "</div>"
+        where
+          link cls name Nothing =
+              "<span class=\"" ++ cls ++ "-subdued\">" ++ name ++ "</span>"
+          link cls name (Just p) =
+              "<a class=\"" ++ cls ++ "\" href=\"" ++ Files.postUrl p ++
+                                "\">" ++ name ++ "</a>"
+
+jsInfo :: Post -> String
+jsInfo post =
+    "<script type=\"text/javascript\">\n" ++
+    "Blog = {\n" ++
+    "  pageName: " ++ show (Files.postBaseName post) ++
+    "\n" ++
+    "};\n" ++
+    "</script>\n"
+
+buildPost :: Handle -> Config -> Post -> (Maybe Post, Maybe Post) -> IO ()
+buildPost h config post prevNext = do
+  hPutStr h =<< (readFile $ Files.pagePreamble config)
+  hPutStr h $ jsInfo post
+  hPutStr h $ uncurry buildLinks prevNext
+  hPutStr h =<< (readFile $ Files.postPreamble config)
+  hPutStr h =<< (readFile $ Files.postIntermediateHtml config post)
+  hPutStr h =<< (readFile $ Files.postPostamble config)
+  hPutStr h =<< (readFile $ Files.pagePostamble config)
+
+generatePost :: Config -> Post -> IO ()
+generatePost config post = do
+  let tempHtml = htmlTempDir config </> Files.postBaseName post ++ ".html"
+      finalHtml = Files.postIntermediateHtml config post
+
+  htmlExists <- doesFileExist finalHtml
+  skip <- case htmlExists of
+            False -> return False
+            True -> do
+              info <- getFileStatus finalHtml
+              return $ (toUtcTime $ modificationTime info) > postModificationTime post
+
+  when (not skip) $ do
+    putStrLn $ "Processing: " ++ Files.postBaseName post
+
+    h <- openFile (Files.postHtex config post) WriteMode
+    writePost h post
+    hClose h
+
+    -- Run gladtex on the temp file to generate the final file.
+    gladTex config (Files.postHtex config post) "000000"
+
+    -- Gladtex generates the HTML in the same directory as the source
+    -- file, so we need to copy that to the final location.
+    copyFile tempHtml finalHtml
+
+    -- Remove the temporary file.
+    removeFile $ Files.postHtex config post
+    removeFile tempHtml
+
+generatePosts :: Config -> [Post] -> IO ()
+generatePosts config posts = do
+  let n = length posts
+  forM_ (zip posts [0..]) $ \(p, i) ->
+      do
+        let prevPost = if i == 0 then Nothing else Just (posts !! (i - 1))
+            nextPost = if i == n - 1 then Nothing else Just (posts !! (i + 1))
+
+        generatePost config p
+        h <- openFile (Files.postFinalHtml config p) WriteMode
+        buildPost h config p (prevPost, nextPost)
+        hClose h
+
+generateIndex :: Config -> Post -> IO ()
+generateIndex config post = do
+  let dest = Files.postFinalHtml config post
+      index = Files.indexHtml config
+
+  exists <- doesFileExist index
+  when exists $ removeFile index
+
+  createSymbolicLink dest index
+
+postModificationString :: Post -> IO String
+postModificationString p = do
+  tz <- getCurrentTimeZone
+  localTime <- toLocalTime $ postModificationTime p
+  return $ show localTime ++ "  " ++ timeZoneName tz
+
+generateList :: Config -> [Post] -> IO ()
+generateList config posts = do
+  putStrLn "Generating all-posts list."
+
+  h <- openFile (Files.listHtex config) WriteMode
+
+  hPutStr h =<< (readFile $ Files.pagePreamble config)
+  hPutStr h "<div id=\"all-posts\">"
+
+  -- For each post in the order they were given, extract the
+  -- unrendered title and construct an htex document.  Then render it
+  -- to the listing location.
+  forM_ posts $ \p -> do
+    created <- postModificationString p
+    hPutStr h $ concat [ "<div class=\"listing-entry\"><span class=\"post-title\">"
+                       , "<a href=\"" ++ Files.postUrl p ++ "\">"
+                       , postTitle p 110
+                       , "</a></span><span class=\"post-created\">Posted "
+                       , created
+                       , "</span></div>\n"
+                       ]
+
+  hPutStr h "</div>"
+  hPutStr h =<< (readFile $ Files.pagePostamble config)
+  hClose h
+
+  gladTex config (Files.listHtex config) "0000FF"
+
+  -- Gladtex generates the HTML in the same directory as the source
+  -- file, so we need to copy that to the final location.
+  copyFile (Files.listTmpHtml config) (Files.listHtml config)
+
+  -- Remove the temporary file.
+  removeFile $ Files.listHtex config
+  removeFile $ Files.listTmpHtml config
+
+fullPostUrl :: Config -> Post -> String
+fullPostUrl config p = baseUrl config ++ Files.postUrl p
+
+rssItem :: Config -> Post -> String
+rssItem config p =
+    concat [ "<item>"
+           , "<title>" ++ postTitleRaw p ++ "</title>\n"
+           , "<link>" ++ fullPostUrl config p ++ "</link>\n"
+           , "<pubDate>" ++ rssModificationTime p ++ "</pubDate>\n"
+           , "<guid>" ++ fullPostUrl config p ++ "</guid>\n"
+           , "</item>\n"
+           ]
+
+generateRssFeed :: Config -> [Post] -> IO ()
+generateRssFeed config posts = do
+  h <- openFile (Files.rssXml config) WriteMode
+  hPutStr h =<< (readFile $ Files.rssPreamble config)
+
+  forM_ posts (hPutStr h . rssItem config)
+
+  hPutStr h =<< (readFile $ Files.rssPostamble config)
+  hClose h
+
+setup :: Config -> IO ()
+setup config = do
+  exists <- doesDirectoryExist $ baseDir config
+  dataDir <- skelDir
+
+  when (not exists) $ do
+          putStrLn $ "Setting up data directory using skeleton: " ++ dataDir
+          copyTree dataDir $ baseDir config
+
+  ensureDirs config
+
+ensureDirs :: Config -> IO ()
+ensureDirs config = do
+  let dirs = [ postSourceDir
+             , htmlDir
+             , stylesheetDir
+             , postHtmlDir
+             , postIntermediateDir
+             , imageDir
+             , templateDir
+             , htmlTempDir
+             ]
+
+  forM_ (dirs <*> pure config) $ \d ->
+      do
+        exists <- doesDirectoryExist d
+        when (not exists) $ createDirectory d
+
+-- The files we look at to decide whether to regenerate the blog.
+-- We'll always look at the post input files, but we also want to look
+-- at other files to trigger a regeneration.
+changedFiles :: Config -> [FilePath]
+changedFiles config = [ Files.rssPreamble
+                      , Files.rssPostamble
+                      , Files.pagePreamble
+                      , Files.pagePostamble
+                      , Files.postPreamble
+                      , Files.postPostamble
+                      ] <*> pure config
+
+preserveM :: (Monad m) => (a -> m b) -> a -> m (a, b)
+preserveM act val = act val >>= \r -> return (val, r)
+
+scanForChanges :: Config -> IO () -> IO ()
+scanForChanges config act = do
+  t <- getCurrentTime
+  scan t
+      where
+        scan t = do
+          posts <- allPostFilenames config
+          let filesToInspect = posts ++ changedFiles config
+          allTimes <- mapM (preserveM getModificationTime) filesToInspect
+
+          let modified = filter ((> t) . snd) allTimes
+              nextTime = if null modified
+                         then t
+                         else maximum $ map snd modified
+
+          when (not $ null modified) $
+               do
+                 putStrLn ""
+                 putStrLn "Changes detected:"
+                 forM_ modified $ \(fp, _) -> do
+                              putStrLn $ "  " ++ fp
+                 act
+          threadDelay $ 1 * 1000 * 1000
+          scan nextTime
+
+mkConfig :: FilePath -> String -> Config
+mkConfig base url = Config { baseDir = base
+                           , postSourceDir = base </> "posts"
+                           , htmlDir = base </> "html"
+                           , stylesheetDir = base </> "html" </> "stylesheets"
+                           , postHtmlDir = base </> "html" </> "posts"
+                           , postIntermediateDir = base </> "generated"
+                           , imageDir = base </> "html" </> "generated-images"
+                           , templateDir = base </> "templates"
+                           , htmlTempDir = base </> "tmp"
+                           , baseUrl = url
+                           }
+
+usage :: IO ()
+usage = do
+  putStrLn "Usage: mb [-l]\n"
+  putStrLn "mb is a tool for creating and managing a mathematically-inclined"
+  putStrLn "weblog.  To use mb, you must set a few environment variables:"
+  putStrLn ""
+  putStrLn $ "  " ++ baseDirEnvName ++ ": path where blog files will be stored"
+  putStrLn $ "  " ++ baseUrlEnvName ++ ": base URL where blog will be hosted"
+  putStrLn ""
+  putStrLn " -l: make mb poll periodically and regenerate your blog content"
+  putStrLn "     when something changes.  This is useful if you want to run a"
+  putStrLn "     local web server to view your posts as you're writing them."
+
+main :: IO ()
+main = do
+  env <- getEnvironment
+  args <- getArgs
+
+  checkForGladtex
+
+  let mBase = lookup baseDirEnvName env
+      mBaseUrl = lookup baseUrlEnvName env
+
+  when (isNothing (mBase >> mBaseUrl)) $ usage >> exitFailure
+
+  let Just dir = mBase
+      Just url = mBaseUrl
+
+  when (head dir /= '/') $ do
+         putStrLn $ baseDirEnvName ++ " must contain an absolute path"
+         exitFailure
+
+  putStrLn $ "mb: using base directory " ++ (show dir)
+  putStrLn $ "mb: using base url " ++ (show url)
+  let config = mkConfig dir url
+  setup config
+
+  let work = do
+         posts <- allPosts config
+         generatePosts config posts
+         generateIndex config $ head posts
+         generateList config posts
+         generateRssFeed config posts
+         putStrLn "Done."
+
+  case args of
+    [] -> work
+    ["-l"] -> scanForChanges config work
+    _ -> usage >> exitFailure
