diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -107,6 +107,7 @@
 $ export MB_BASE_DIR=`pwd`/myBlog
 $ mb -i
 Blog directory: .../myBlog
+Config file: .../myBlog/blog.cfg
 Setting up data directory using skeleton: /.../skel
 Configuration file changed; regenerating all content.
 Templates changed; regenerating accordingly.
@@ -168,13 +169,17 @@
 Serving the Blog
 ================
 
-To serve your blog files you can either make the "html" directory a
+By default, `mb` will use `$(MB_BASE_DIR)/html` as its output
+directory, but if you want the generated files to be installed
+somewhere else, use the `-h` option.
+
+To serve your blog files you can either make the output directory a
 document root for your web server or virtual host, or you can symlink
-the `html` directory to the location of choice.  The mathblog
+the output directory to the location of choice.  The mathblog
 directory itself is not intended to be in your web server's document
 tree because it contains many files that shouldn't be served to users.
 
-Start up a web server pointing at the `html` directory and take a
+Start up a web server pointing at the output directory and take a
 look.
 
 Now you might want to edit or create a new post, or even remove one.
@@ -234,22 +239,27 @@
 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:
+to your blog posts and other data files and regenerate them and copy
+them to the output directory when it detects a change.  Running
+mathblog in listen mode is simple:
 
 ~~~
 $ mb -l
-Blog directory: /.../blog
+Waiting for changes in .../blog ...
+Blog directory: .../blog
+Config file: .../blog/blog.cfg
 Post index changed; regenerating next/previous links.
 Done.
 
 Blog directory: /.../blog
+Config file: .../blog/blog.cfg
 Configuration file changed; regenerating all content.
 Rendering 1 post(s)...
 Rendering first-post
 Done.
 
 Blog directory: /Users/cygnus/blog3
+Config file: .../blog/blog.cfg
 Posts changed:
   first-post.txt
 Rendering 1 post(s)...
@@ -287,9 +297,6 @@
     This file makes up the structure of the post portion of the page,
     for pages which show posts (i.e., not the post index).
 
-In addition, subdirectories of the `html/` directory contain things
-you might want to customize, such as a CSS stylesheet.
-
 The templates mentioned above are StringTemplate templates and are
 processed with the HStringTemplate library.  The following template
 placeholders are supported in each template:
@@ -327,6 +334,17 @@
  - `$content$`
 
     The content of the page to be rendered.
+
+Blog Assets
+===========
+
+The default blog directory created by `mb` also includes an `assets`
+directory.  Whenever `mb` detects changes in your blog post or asset
+files (or if you run `mb -f`), everything in `assets/` will be copied
+recursively into the output directory.  This way, if you have custom
+stylesheets or other files which need to be available, they can be
+kept in your data directory and published with the rest of the
+generated content.
 
 Function Graph Embedding
 ========================
diff --git a/mathblog.cabal b/mathblog.cabal
--- a/mathblog.cabal
+++ b/mathblog.cabal
@@ -1,5 +1,5 @@
 Name:                mathblog
-Version:             0.4
+Version:             0.5
 Synopsis:            A program for creating and managing a static
                      weblog with LaTeX math and function graphs
 Description:         This package provides a program for creating and
@@ -11,8 +11,8 @@
                      of your choice, then this is the blogging
                      platform for you!  For detailed information and a
                      full feature list, please see the README.
-Author:              Jonathan Daugherty <drcygnus@gmail.com>
-Maintainer:          Jonathan Daugherty <drcygnus@gmail.com>
+Author:              Jonathan Daugherty <cygnus@foobox.com>
+Maintainer:          Jonathan Daugherty <cygnus@foobox.com>
 Build-Type:          Simple
 Cabal-version:       >= 1.6
 License:             BSD3
@@ -21,7 +21,7 @@
 
 Data-Files:
   README.md
-  skel/html/stylesheets/stylesheet.css
+  skel/assets/stylesheets/stylesheet.css
   skel/templates/postTemplate.html
   skel/templates/pageTemplate.html
   skel/templates/rssTemplate.xml
@@ -44,7 +44,7 @@
     process >= 1.0,
     time >= 1.1,
     old-locale >= 1.0,
-    SHA >= 1.4 && < 1.5,
+    SHA >= 1.4 && < 1.6,
     bytestring >= 0.9 && < 1.0,
     HStringTemplate >= 0.6 && < 0.7,
     ConfigFile >= 1.1 && < 1.2
@@ -79,12 +79,12 @@
     process >= 1.0 && < 1.1,
     time >= 1.1,
     old-locale >= 1.0 && < 1.1,
-    SHA >= 1.4 && < 1.5,
+    SHA >= 1.4 && < 1.6,
     bytestring >= 0.9 && < 1.0,
     HStringTemplate >= 0.6 && < 0.7,
     ConfigFile >= 1.1 && < 1.2,
-    HUnit >= 1.2.2 && < 1.2.3,
-    test-framework >= 0.3.3 && < 0.4,
+    HUnit >= 1.2.2 && < 1.2.5,
+    test-framework >= 0.3.3 && < 0.6,
     test-framework-hunit >= 0.2.6 && < 0.3
 
   GHC-Options: -Wall
diff --git a/skel/assets/stylesheets/stylesheet.css b/skel/assets/stylesheets/stylesheet.css
new file mode 100644
--- /dev/null
+++ b/skel/assets/stylesheets/stylesheet.css
@@ -0,0 +1,114 @@
+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;
+  margin-bottom: 1em;
+}
+div#prev-next-links {
+  margin-top: 1em;
+}
+.prev-link {
+  color: blue;
+  text-decoration: none;
+}
+.prev-link-subdued {
+  color: #aaa;
+}
+.next-link {
+  float: right;
+  color: blue;
+}
+.next-link-subdued {
+  color: #aaa;
+  float: right;
+}
+#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;
+}
+.eq-left {
+    float: left;
+    margin-right: 0.5em;
+    margin-bottom: 0.5em;
+}
+.eq-right {
+    float: right;
+}
+.eq-center {
+    display: block;
+    margin-left: auto;
+    margin-right: auto;
+}
+.displaymath {
+    display: block;
+    margin-top: 1em !important;
+    margin-bottom: 1em !important;
+    margin-left: auto !important;
+    margin-right: auto !important;
+}
diff --git a/skel/html/stylesheets/stylesheet.css b/skel/html/stylesheets/stylesheet.css
deleted file mode 100644
--- a/skel/html/stylesheets/stylesheet.css
+++ /dev/null
@@ -1,114 +0,0 @@
-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;
-  margin-bottom: 1em;
-}
-div#prev-next-links {
-  margin-top: 1em;
-}
-.prev-link {
-  color: blue;
-  text-decoration: none;
-}
-.prev-link-subdued {
-  color: #aaa;
-}
-.next-link {
-  float: right;
-  color: blue;
-}
-.next-link-subdued {
-  color: #aaa;
-  float: right;
-}
-#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;
-}
-.eq-left {
-    float: left;
-    margin-right: 0.5em;
-    margin-bottom: 0.5em;
-}
-.eq-right {
-    float: right;
-}
-.eq-center {
-    display: block;
-    margin-left: auto;
-    margin-right: auto;
-}
-.displaymath {
-    display: block;
-    margin-top: 1em !important;
-    margin-bottom: 1em !important;
-    margin-left: auto !important;
-    margin-right: auto !important;
-}
diff --git a/src/MB/Files.hs b/src/MB/Files.hs
--- a/src/MB/Files.hs
+++ b/src/MB/Files.hs
@@ -54,6 +54,3 @@
 
 postFinalHtml :: Blog -> Post -> FilePath
 postFinalHtml config p = postHtmlDir config </> postBaseName p ++ ".html"
-
-stylesheet :: Blog -> FilePath
-stylesheet c = stylesheetDir c </> "stylesheet.css"
diff --git a/src/MB/Startup.hs b/src/MB/Startup.hs
--- a/src/MB/Startup.hs
+++ b/src/MB/Startup.hs
@@ -22,12 +22,15 @@
     ( getEnvironment
     , getArgs
     )
+import System.FilePath
 import System.Console.GetOpt
 
 data StartupConfig = StartupConfig { listenMode :: Bool
                                    , dataDirectory :: FilePath
                                    , initDataDirectory :: Bool
                                    , forceRegeneration :: Bool
+                                   , htmlOutputDirectory :: FilePath
+                                   , configFilePath :: FilePath
                                    }
                      deriving (Show, Eq)
 
@@ -35,6 +38,8 @@
           | DataDir FilePath
           | InitDataDirectory
           | ForceRegenerate
+          | HtmlOutputDir FilePath
+          | ConfigFile FilePath
             deriving (Eq)
 
 getDataDirFlag :: [Flag] -> Maybe FilePath
@@ -42,6 +47,16 @@
 getDataDirFlag (DataDir p:_) = Just p
 getDataDirFlag (_:fs) = getDataDirFlag fs
 
+getHtmlOutputDirFlag :: [Flag] -> Maybe FilePath
+getHtmlOutputDirFlag [] = Nothing
+getHtmlOutputDirFlag (HtmlOutputDir p:_) = Just p
+getHtmlOutputDirFlag (_:fs) = getHtmlOutputDirFlag fs
+
+getConfigFilePath :: [Flag] -> Maybe FilePath
+getConfigFilePath [] = Nothing
+getConfigFilePath (ConfigFile p:_) = Just p
+getConfigFilePath (_:fs) = getConfigFilePath fs
+
 options :: [OptDescr Flag]
 options = [ Option ['d'] [baseDirParamName] (ReqArg DataDir "PATH")
                        $ "Path where blog files will be stored.  If this is\n" ++
@@ -57,6 +72,14 @@
                              "to view your posts as you're writing them."
           , Option ['f'] ["force"] (NoArg ForceRegenerate)
                        "Force a rebuild of all blog content."
+          , Option ['h'] ["html-dir"] (ReqArg HtmlOutputDir "PATH")
+                       $ "Write generated HTML and images to the specified\n" ++
+                         "directory.  The default is the 'html' directory in\n" ++
+                         "the blog data directory."
+          , Option ['c'] ["config-file"] (ReqArg ConfigFile "FILENAME")
+                       $ "Use the specified config file instead of 'blog.cfg'\n" ++
+                         "in the data directory.  This path must be relative\n" ++
+                         "to the data directory."
           ]
 
 -- |Inspect the program environment to create a startup configuration.
@@ -91,11 +114,15 @@
             i = InitDataDirectory `elem` flags
             f = ForceRegenerate `elem` flags
         d <- getDataDirFlag flags `mplus` (lookup baseDirEnvName env)
+        h <- getHtmlOutputDirFlag flags `mplus` (Just $ d </> "html")
+        c <- getConfigFilePath flags `mplus` (Just $ d </> "blog.cfg")
 
         return $ StartupConfig { listenMode = lm
                                , dataDirectory = d
                                , initDataDirectory = i
                                , forceRegeneration = f
+                               , htmlOutputDirectory = h
+                               , configFilePath = c
                                }
 
 baseDirEnvName :: String
diff --git a/src/MB/Types.hs b/src/MB/Types.hs
--- a/src/MB/Types.hs
+++ b/src/MB/Types.hs
@@ -21,7 +21,7 @@
 data Blog = Blog { baseDir :: FilePath
                  , postSourceDir :: FilePath
                  , htmlDir :: FilePath
-                 , stylesheetDir :: FilePath
+                 , assetDir :: FilePath
                  , postHtmlDir :: FilePath
                  , postIntermediateDir :: FilePath
                  , imageDir :: FilePath
@@ -69,5 +69,6 @@
                   , configChanged :: Bool
                   , templatesChanged :: Bool
                   , postIndexChanged :: Bool
+                  , assetsChanged :: Bool
                   }
     deriving (Show)
diff --git a/src/MB/Util.hs b/src/MB/Util.hs
--- a/src/MB/Util.hs
+++ b/src/MB/Util.hs
@@ -96,6 +96,7 @@
     exitFailure
 
   when (not dstDExists) $ createDirectory dstPath
+
   copyTree' srcPath dstPath
 
   where
@@ -114,7 +115,8 @@
             let dstDir = dst </> dirName
                 dirName = takeFileName dir
 
-            createDirectory dstDir
+            e <- doesDirectoryExist dstDir
+            when (not e) $ createDirectory dstDir
             copyTree' (src </> dirName) dstDir
 
 toUtcTime :: EpochTime -> UTCTime
@@ -150,6 +152,15 @@
          , not $ "." `isPrefixOf` f
          ]
 
+dirFilenamesRecursive :: FilePath -> IO [FilePath]
+dirFilenamesRecursive dir = do
+  entries <- filter (not . flip elem [".", ".."]) <$> getDirectoryContents dir
+  dirs <- filterM doesDirectoryExist $ map (dir </>) entries
+  files <- filterM doesFileExist $ map (dir </>) entries
+
+  rest <- concat <$> forM dirs dirFilenamesRecursive
+  return $ files ++ rest
+
 allPostFilenames :: FilePath -> IO [FilePath]
 allPostFilenames postSrcDir = do
   allFiles <- dirFilenames postSrcDir
@@ -211,6 +222,7 @@
                    , not . null . postsChanged
                    , templatesChanged
                    , postIndexChanged
+                   , assetsChanged
                    ]
 
 summarizeChanges :: Blog -> Bool -> IO ChangeSummary
@@ -243,8 +255,15 @@
                             mtime <- getModificationTime f
                             return $ mtime > baseTime
 
+  -- Determine whether any assets changed
+  assetFiles <- dirFilenamesRecursive (assetDir config)
+  assetChanges <- forM assetFiles $ \f -> do
+                            mtime <- getModificationTime f
+                            return $ mtime > baseTime
+
   return $ ChangeSummary { configChanged = configChanged' || forceAll
                          , postsChanged = map postFilename modifiedPosts
                          , templatesChanged = or (forceAll : templateChanges)
                          , postIndexChanged = postIndexChanged' || forceAll
+                         , assetsChanged = or (forceAll : assetChanges)
                          }
diff --git a/src/Main.hs b/src/Main.hs
--- a/src/Main.hs
+++ b/src/Main.hs
@@ -8,9 +8,6 @@
 import System.Exit
 import System.Directory
 import System.FilePath
-import System.Posix.Files
-    ( createSymbolicLink
-    )
 import Data.Time.LocalTime
 import qualified Text.Pandoc as Pandoc
 import qualified MB.Config as Config
@@ -34,7 +31,8 @@
     ( getDataFileName
     )
 import MB.Startup
-    ( dataDirectory
+    ( StartupConfig(..)
+    , dataDirectory
     , listenMode
     , startupConfigFromEnv
     , forceRegeneration
@@ -45,12 +43,12 @@
 import MB.Mathjax
 import MB.Gladtex
 
+defaultConfigFilename :: String
+defaultConfigFilename = "blog.cfg"
+
 skelDir :: IO FilePath
 skelDir = getDataFileName "skel"
 
-configFilename :: String
-configFilename = "blog.cfg"
-
 commonTemplateAttrs :: Blog -> [(String, String)]
 commonTemplateAttrs blog =
     [ ( "baseUrl", baseUrl blog )
@@ -167,16 +165,13 @@
         buildPost h blog p (prevPost, nextPost)
         hClose h
 
-linkIndexPage :: Blog -> IO ()
-linkIndexPage blog = do
-  let dest = Files.postFinalHtml blog post
+buildIndexPage :: Blog -> IO ()
+buildIndexPage blog = do
+  let src = Files.postFinalHtml blog post
       index = Files.indexHtml blog
       post = head $ blogPosts blog
 
-  exists <- doesFileExist index
-  when exists $ removeFile index
-
-  createSymbolicLink dest index
+  copyFile src index
 
 postModificationString :: Post -> IO String
 postModificationString p = do
@@ -239,7 +234,7 @@
 initializeDataDir :: FilePath -> IO ()
 initializeDataDir dir = do
   existsBase <- doesDirectoryExist dir
-  existsConfig <- doesFileExist $ dir </> configFilename
+  existsConfig <- doesFileExist $ dir </> defaultConfigFilename
 
   dataDir <- skelDir
 
@@ -250,13 +245,13 @@
 
   when existsConfig $
        putStrLn $ "Data directory already initialized; found " ++
-                    (dir </> configFilename)
+                    (dir </> defaultConfigFilename)
 
 ensureDirs :: Blog -> IO ()
 ensureDirs blog = do
   let dirs = [ postSourceDir
              , htmlDir
-             , stylesheetDir
+             , assetDir
              , postHtmlDir
              , postIntermediateDir
              , imageDir
@@ -270,12 +265,12 @@
         exists <- doesDirectoryExist d
         when (not exists) $ createDirectory d
 
-scanForChanges :: FilePath -> (FilePath -> IO Bool) -> IO ()
-scanForChanges dir act = do
+scanForChanges :: IO Bool -> IO ()
+scanForChanges act = do
   scan
       where
         scan = do
-          didWork <- act dir
+          didWork <- act
           when didWork $ putStrLn ""
           threadDelay $ 1 * 1000 * 1000
           scan
@@ -292,13 +287,14 @@
     , ("tikz", tikzProcessor)
     ]
 
-mkBlog :: FilePath -> IO Blog
-mkBlog base = do
-  let configFilePath = base </> configFilename
-  e <- doesFileExist configFilePath
+mkBlog :: StartupConfig -> IO Blog
+mkBlog conf = do
+  let base = dataDirectory conf
+      configFile = base </> (configFilePath conf)
+  e <- doesFileExist configFile
 
   when (not e) $ do
-                  putStrLn $ "Configuration file " ++ configFilePath ++ " not found"
+                  putStrLn $ "Configuration file " ++ configFile ++ " not found"
                   exitFailure
 
   let requiredValues = [ "baseUrl"
@@ -307,7 +303,7 @@
                        , "authorEmail"
                        ]
 
-  cfg <- Config.readConfig configFilePath requiredValues
+  cfg <- Config.readConfig configFile requiredValues
 
   let Just cfg_baseUrl = lookup "baseUrl" cfg
       Just cfg_title = lookup "title" cfg
@@ -338,13 +334,14 @@
 
       procs = [eqBackend, mathBackend]
 
-  let b = Blog { baseDir = base
+  let html = htmlOutputDirectory conf
+      b = Blog { baseDir = base
                , postSourceDir = postSrcDir
-               , htmlDir = base </> "html"
-               , stylesheetDir = base </> "html" </> "stylesheets"
-               , postHtmlDir = base </> "html" </> "posts"
+               , htmlDir = html
+               , assetDir = base </> "assets"
+               , postHtmlDir = html </> "posts"
                , postIntermediateDir = base </> "generated"
-               , imageDir = base </> "html" </> "generated-images"
+               , imageDir = html </> "generated-images"
                , templateDir = base </> "templates"
                , htmlTempDir = base </> "tmp"
                , baseUrl = cfg_baseUrl
@@ -352,7 +349,7 @@
                , authorName = cfg_authorName
                , authorEmail = cfg_authorEmail
                , eqPreamblesDir = base </> "eq-preambles"
-               , configPath = configFilePath
+               , configPath = configFile
                , blogPosts = allPosts
                , processors = procs
                }
@@ -360,15 +357,30 @@
   ensureDirs b
   return b
 
-regenerateContent :: Bool -> FilePath -> IO Bool
-regenerateContent force dir = do
-  blog <- mkBlog dir
-  summary <- summarizeChanges blog force
+installAssets :: Blog -> IO ()
+installAssets blog = do
+  let ad = assetDir blog
 
+  -- For each file and directory in assets/, copy it to the output
+  -- directory.
+  entries <- filter (not . flip elem [".", ".."]) <$> getDirectoryContents ad
+
+  dirs <- filterM doesDirectoryExist $ map (ad </>) entries
+  files <- filterM doesFileExist $ map (ad </>) entries
+
+  forM_ dirs $ \d -> copyTree d (htmlDir blog </> (takeBaseName d))
+  forM_ files $ \f -> copyFile f (htmlDir blog)
+
+regenerateContent :: StartupConfig -> IO Bool
+regenerateContent conf = do
+  blog <- mkBlog conf
+  summary <- summarizeChanges blog (forceRegeneration conf)
+
   case anyChanges summary of
     False -> return False
     True -> do
       putStrLn $ "Blog directory: " ++ baseDir blog
+      putStrLn $ "Config file: " ++ configFilePath conf
 
       when (configChanged summary) $
            putStrLn "Configuration file changed; regenerating all content."
@@ -381,9 +393,14 @@
       when (postIndexChanged summary) $
            putStrLn "Post index changed; regenerating next/previous links."
 
+      when (assetsChanged summary) $
+           do
+             putStrLn "Assets changed; reinstalling."
+             installAssets blog
+
       generatePosts blog summary
 
-      linkIndexPage blog
+      buildIndexPage blog
       generatePostList blog
       generateRssFeed blog
 
@@ -402,6 +419,9 @@
 
   case listenMode conf of
     False -> do
-         didWork <- regenerateContent (forceRegeneration conf) dir
+         didWork <- regenerateContent conf
          when (not didWork) $ putStrLn "No changes found!"
-    True -> scanForChanges dir (regenerateContent False)
+    True -> do
+         putStrLn $ "Waiting for changes in " ++ (dataDirectory conf) ++ " ..."
+         scanForChanges
+            (regenerateContent $ conf { forceRegeneration = False })
