diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
 # Change Log for rib
 
+## 0.4.1.0
+
+- `Rib.Pandoc`: 
+  - Export `render'` and `renderInlines'` (the non-Lucid versions)
+  - Re-export `Text.Pandoc.Readers` so the library user does not have to directly depend on `pandoc` only to render its documents.
+- `Rib.App`: The `run` funtion now takes two more arguments, specifying the input and output directory, which are no longer hardcoded.
+- `Rib.Simple`: add LaTeX to default list of readers
+- `Rib.Server`: Remove ".html" detection magic from URLs
+
 ## 0.3.0.0
 
 - Rename `Rib.App.Watch` to `Rib.App.WatchAndGenerate`
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -42,15 +42,15 @@
 ...
 $ cd mysite
 $ ls -F
-a/  b/  default.nix  Main.hs  README.md  rib-sample.cabal
+a/  default.nix  Main.hs  README.md  rib-sample.cabal
 ```
 
 The three key items here are:
 
 1. `Main.hs`: Haskell source containing the DSL of the HTML/CSS of your site.
 1. `a/`: The source content (eg: Markdown sources and static files)
-1. `b/`: The target directory, initially empty, will contain _generated_ content
-   (i.e., the HTML files, and copied over static content)
+1. `b/`: The target directory, excluded from the git repository, will contain
+   _generated_ content (i.e., the HTML files, and copied over static content)
    
 The template repository comes with a few sample posts under `a/`, and a basic
 HTML layout and CSS style defined in `Main.hs`. 
@@ -141,4 +141,4 @@
 * [rib-sample](https://github.com/srid/rib-sample): Use this to get started with
   your own site.
 
-* Author's own website. Live at https://notes.srid.ca/ 
+* Author's own website. Live at https://www.srid.ca/ 
diff --git a/rib.cabal b/rib.cabal
--- a/rib.cabal
+++ b/rib.cabal
@@ -1,6 +1,6 @@
 cabal-version: 2.2
 name: rib
-version: 0.3.0.0
+version: 0.4.1.0
 license: BSD-3-Clause
 copyright: 2019 Sridhar Ratnakumar
 maintainer: srid@srid.ca
@@ -39,25 +39,25 @@
         binary >=0.8.6 && <0.9,
         text >=1.2.3 && <1.3,
         time >=1.8.0 && <1.9,
-        async >=2.2.2 && <2.3,
+        async,
         clay >=0.13.1 && <0.14,
         mtl >=2.2.2 && <2.3,
         cmdargs >=0.10.20 && <0.11,
         data-default >=0.7.1 && <0.8,
         fsnotify >=0.3.0 && <0.4,
         http-types >=0.12.3 && <0.13,
-        lens >=4.17.1 && <4.18,
+        lens,
         lens-aeson >=1.0.2 && <1.1,
         lucid >=2.9.11 && <2.10,
-        pandoc >=2.7.3 && <2.8,
         pandoc-types >=1.17.5 && <1.18,
         safe >=0.3.17 && <0.4,
-        skylighting >=0.8.1 && <0.9,
+        skylighting,
         pandoc-include-code >=1.4.0 && <1.5,
-        shake >=0.18.3 && <0.19,
+        shake,
         wai >=3.2.2 && <3.3,
         wai-app-static >=3.1.6 && <3.2,
-        wai-extra >=3.0.26 && <3.1,
-        warp >=3.2.28 && <3.3,
+        wai-extra,
+        warp,
         base >=4.7 && <5,
-        pandoc >=2.7 && <3
+        pandoc >=2.7 && <3,
+        directory >= 1.0 && <2.0
diff --git a/src/Rib/App.hs b/src/Rib/App.hs
--- a/src/Rib/App.hs
+++ b/src/Rib/App.hs
@@ -9,8 +9,6 @@
   ( App(..)
   , run
   , runWith
-  , ribOutputDir
-  , ribInputDir
   ) where
 
 import Control.Concurrent (threadDelay)
@@ -24,6 +22,7 @@
 import System.FSNotify (watchTree, withManager)
 
 import qualified Rib.Server as Server
+import Rib.Shake (Dirs (..))
 
 -- | Application modes
 --
@@ -35,34 +34,31 @@
     }
   -- ^ Generate static files once.
   | WatchAndGenerate
-  -- ^ Watch for changes in `ribInputDir` and run `Generate`
+  -- ^ Watch for changes in the input directory and run `Generate`
   | Serve
     { port :: Int
       -- ^ Port to bind the server
     , dontWatch :: Bool
       -- ^ Unless set run `WatchAndGenerate` automatically
     }
-  -- ^ Run a HTTP server serving `ribOutputDir`
+  -- ^ Run a HTTP server serving content from the output directory
   deriving (Data,Typeable,Show,Eq)
 
--- | The path where static files will be generated.
---
--- Rib's server uses this directory when serving files.
-ribOutputDir :: FilePath
-ribOutputDir = "b"
 
--- | Directory from which source content will be read.
-ribInputDir :: FilePath
-ribInputDir = "a"
--- NOTE: ^ This should ideally *not* be `"."` as our use of watchTree (of
--- `runWith`) can interfere with Shake's file scaning.
-
 -- | Run Rib using arguments passed in the command line.
 run
-  :: Action ()
+  :: FilePath
+  -- ^ Directory from which source content will be read.
+  --
+  -- NOTE: This should ideally *not* be `"."` as our use of watchTree (of
+  -- `runWith`) can interfere with Shake's file scaning.
+  -> FilePath
+  -- ^ The path where static files will be generated.  Rib's server uses this
+  -- directory when serving files.
+  -> Action ()
   -- ^ Shake build rules for building the static site
   -> IO ()
-run buildAction = runWith buildAction =<< cmdArgs ribCli
+run src dst buildAction = runWith src dst buildAction =<< cmdArgs ribCli
   where
     ribCli = modes
       [ Serve
@@ -78,25 +74,26 @@
       ]
 
 -- | Like `run` but with an explicitly passed `App` mode
-runWith :: Action () -> App -> IO ()
-runWith buildAction = \case
+runWith :: FilePath -> FilePath -> Action () -> App -> IO ()
+runWith src dst buildAction = \case
   WatchAndGenerate -> withManager $ \mgr -> do
     -- Begin with a *full* generation as the HTML layout may have been changed.
-    runWith buildAction $ Generate True
+    runWith src dst buildAction $ Generate True
     -- And then every time a file changes under the current directory
-    putStrLn $ "[Rib] Watching " <> ribInputDir
-    void $ watchTree mgr ribInputDir (const True) $ const $
-      runWith buildAction $ Generate False
+    putStrLn $ "[Rib] Watching " <> src
+    void $ watchTree mgr src (const True) $ const $
+      runWith src dst buildAction $ Generate False
     -- Wait forever, effectively.
     forever $ threadDelay maxBound
 
   Serve p dw -> concurrently_
-    (unless dw $ runWith buildAction WatchAndGenerate)
-    (Server.serve p ribOutputDir)
+    (unless dw $ runWith src dst buildAction WatchAndGenerate)
+    (Server.serve p dst)
 
   Generate forceGen ->
     let opts = shakeOptions
           { shakeVerbosity = Chatty
           , shakeRebuild = bool [] [(RebuildNow, "**")] forceGen
+          , shakeExtra = addShakeExtra (Dirs (src, dst)) (shakeExtra shakeOptions)
           }
     in shakeForward opts buildAction
diff --git a/src/Rib/Pandoc.hs b/src/Rib/Pandoc.hs
--- a/src/Rib/Pandoc.hs
+++ b/src/Rib/Pandoc.hs
@@ -5,13 +5,15 @@
 
 -- | Helpers for working with Pandoc documents
 module Rib.Pandoc
-  (
+  ( module Text.Pandoc.Readers
   -- * Parsing
-    parse
+  , parse
   , parsePure
   -- * Converting to HTML
   , render
   , renderInlines
+  , render'
+  , renderInlines'
   -- * Metadata
   , getMeta
   , setMeta
@@ -31,9 +33,10 @@
 import Lucid (Html, toHtmlRaw)
 import Text.Pandoc
 import Text.Pandoc.Filter.IncludeCode (includeCode)
+import Text.Pandoc.Readers
 import Text.Pandoc.Readers.Markdown (yamlToMeta)
 import Text.Pandoc.Shared (stringify)
-import Text.Pandoc.Walk (walkM, query)
+import Text.Pandoc.Walk (query, walkM)
 
 
 class IsMetaValue a where
@@ -109,6 +112,7 @@
   where
     settings = def { readerExtensions = exts }
 
+-- | Like `render` but returns the raw HTML string, or the rendering error.
 render' :: Pandoc -> Either PandocError Text
 render' = runPure . writeHtml5String settings
   where
@@ -118,6 +122,7 @@
 render :: Pandoc -> Html ()
 render = either (error . show) toHtmlRaw . render'
 
+-- | Like `renderInlines` but returns the raw HTML string, or the rendering error.
 renderInlines' :: [Inline] -> Either PandocError Text
 renderInlines' = render' . Pandoc mempty . pure . Plain
 
diff --git a/src/Rib/Server.hs b/src/Rib/Server.hs
--- a/src/Rib/Server.hs
+++ b/src/Rib/Server.hs
@@ -11,44 +11,23 @@
 
 import Prelude hiding (init, last)
 
-import Control.Monad (guard)
-import Data.List (isSuffixOf)
-import Data.Maybe (fromMaybe)
 import Data.Text (Text)
 import qualified Data.Text as T
-import Safe (initMay, lastMay)
 
-import Development.Shake.FilePath
+import Development.Shake.FilePath ((-<.>))
 import Network.Wai.Application.Static (defaultFileServerSettings, ssListing, ssLookupFile, staticApp)
 import qualified Network.Wai.Handler.Warp as Warp
-import WaiAppStatic.Types (LookupResult (..), Pieces, StaticSettings, fromPiece, unsafeToPiece)
+import WaiAppStatic.Types (StaticSettings)
 
 -- | WAI Settings suited for serving statically generated websites.
 staticSiteServerSettings :: FilePath -> StaticSettings
 staticSiteServerSettings root = settings
-  { ssLookupFile = lookupFileForgivingHtmlExt
+  { ssLookupFile = ssLookupFile settings
   , ssListing = Nothing  -- Disable directory listings
   }
   where
     settings = defaultFileServerSettings root
 
-    -- | Like upstream's `ssLookupFile` but ignores the ".html" suffix in the
-    -- URL when looking up the corresponding file in the filesystem.
-    --
-    -- This allows "clean urls" so to speak.
-    lookupFileForgivingHtmlExt :: Pieces -> IO LookupResult
-    lookupFileForgivingHtmlExt pieces = ssLookupFile settings pieces >>= \case
-      LRNotFound -> ssLookupFile settings (addHtmlExt pieces)
-      x -> pure x
-
-    -- | Add the ".html" suffix to the URL unless it already exists
-    addHtmlExt :: Pieces -> Pieces
-    addHtmlExt xs = fromMaybe xs $ do
-      init <- fmap fromPiece <$> initMay xs
-      last <- fromPiece <$> lastMay xs
-      guard $ not $ ".html" `isSuffixOf` T.unpack last
-      pure $ fmap unsafeToPiece $ init <> [last <> ".html"]
-
 -- | Return the URL for the given @.html@ file under serve directory
 --
 -- File path must be relative to the serve directory.
@@ -59,7 +38,7 @@
   :: FilePath
   -- ^ Relative path to a page (extension is ignored)
   -> Text
-getHTMLFileUrl = T.pack . ("/" ++) . dropExtension
+getHTMLFileUrl path = T.pack $ "/" ++  (path -<.> ".html")
 
 -- | Run a HTTP server to serve a directory of static files
 --
diff --git a/src/Rib/Shake.hs b/src/Rib/Shake.hs
--- a/src/Rib/Shake.hs
+++ b/src/Rib/Shake.hs
@@ -6,8 +6,6 @@
 
 -- | Combinators for working with Shake.
 --
--- The functions in this module work with `ribInputDir` and `ribOutputDir`.
---
 -- See the source of `Rib.Simple.buildAction` for example usage.
 module Rib.Shake
   (
@@ -19,6 +17,7 @@
   , readPandocMulti
   -- * Misc
   , buildStaticFiles
+  , Dirs(..)
   )
 where
 
@@ -39,20 +38,35 @@
 import Development.Shake.Forward (cacheAction)
 import Lucid (Html)
 import qualified Lucid
+import System.Directory (createDirectoryIfMissing)
 import Text.Pandoc (Pandoc (Pandoc), PandocIO, ReaderOptions)
 
-import Rib.App (ribInputDir, ribOutputDir)
 import qualified Rib.Pandoc
 
--- FIXME: Auto create ./b directory
+newtype Dirs = Dirs (FilePath, FilePath)
 
+getDirs :: Action (FilePath, FilePath)
+getDirs = getShakeExtra >>= \case
+  Just (Dirs d) -> return d
+  Nothing -> fail "Input output directories are not initialized"
 
+ribInputDir :: Action FilePath
+ribInputDir = fst <$> getDirs
+
+ribOutputDir :: Action FilePath
+ribOutputDir = do
+  output <- snd <$> getDirs
+  liftIO $ createDirectoryIfMissing True output
+  return output
+
 -- | Shake action to copy static files as is
 buildStaticFiles :: [FilePattern] -> Action [FilePath]
 buildStaticFiles staticFilePatterns = do
-  files <- getDirectoryFiles ribInputDir staticFilePatterns
+  input <- ribInputDir
+  output <- ribOutputDir
+  files <- getDirectoryFiles input staticFilePatterns
   void $ forP files $ \f ->
-    copyFileChanged (ribInputDir </> f) (ribOutputDir </> f)
+    copyFileChanged (input </> f) (output </> f)
   pure files
 
 -- | Convert the given pattern of source files into their HTML.
@@ -77,7 +91,8 @@
      -- ^ Tuple of pattern of files to work on and document format.
   -> Action [(FilePath, Pandoc)]
 readPandocMulti (pat, r) = do
-  fs <- getDirectoryFiles ribInputDir [pat]
+  input <- ribInputDir
+  fs <- getDirectoryFiles input [pat]
   forP fs $ \f ->
     jsonCacheAction f $ (f, ) <$> readPandoc r f
 
@@ -91,7 +106,8 @@
   -> FilePath
   -> Action Pandoc
 readPandoc r f = do
-  let inp = ribInputDir </> f
+  input <- ribInputDir
+  let inp = input </> f
   need [inp]
   content <- T.decodeUtf8 <$> liftIO (BS.readFile inp)
   doc <- liftIO $ Rib.Pandoc.parse r content
@@ -113,7 +129,8 @@
 -- | Build a single HTML file with the given value
 buildHtml :: FilePath -> Html () -> Action ()
 buildHtml f html = do
-  let out = ribOutputDir </> f
+  output <- ribOutputDir
+  let out = output </> f
   writeHtml out html
 
 writeHtml :: MonadIO m => FilePath -> Html () -> m ()
diff --git a/src/Rib/Simple.hs b/src/Rib/Simple.hs
--- a/src/Rib/Simple.hs
+++ b/src/Rib/Simple.hs
@@ -14,7 +14,7 @@
 
 import Development.Shake (Action)
 import Lucid (Html)
-import Text.Pandoc (Pandoc, readMarkdown, readRST, readOrg)
+import Text.Pandoc (Pandoc, readLaTeX, readMarkdown, readOrg, readRST)
 
 import Rib.Pandoc (getMeta)
 import Rib.Shake
@@ -46,4 +46,5 @@
       [ ("*.md", readMarkdown)
       , ("*.rst", readRST)
       , ("*.org", readOrg)
+      , ("*.tex", readLaTeX)
       ]
