diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,6 @@
+# Change Log for rib
+
+## 0.2.0.0
+
+- Initial release.
+
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,139 @@
+<!--
+Credit for this image: https://www.svgrepo.com/svg/24439/ribs
+-->
+<img align="right" width="50" src="https://raw.githubusercontent.com/srid/rib/master/assets/rib.svg?sanitize=true" />
+
+# rib
+
+Rib is a Haskell library for writing your own **static site generator**.
+
+How does it compare to Hakyll?
+
+- Use the [Shake](https://shakebuild.com/) build system
+- Builtin support for using Haskell DSL to define the HTML
+  ([Lucid](https://chrisdone.com/posts/lucid2/)) & CSS
+  ([Clay](http://fvisser.nl/clay/)) of your site 
+  - Like Hakyll, Rib uses [Pandoc](https://pandoc.org/) for parsing the source
+    documents
+- Remain as simple as possible to use (see screenshot below)
+- Optional Nix based workflow for easily reproducible environment
+
+Rib prioritizes the use of *existing* tools over reinventing them, and enables
+the user to compose them as they wish instead of having to write code to fit a
+custom framework.
+
+Here is how your code may look like if you were to generate your static site
+using Rib:
+
+<img src="https://raw.githubusercontent.com/srid/rib/master/assets/rib-sample-main.png" />
+
+## Getting Started
+
+The easiest way to get started with [Rib](/) is to [use the
+template](https://help.github.com/en/articles/creating-a-repository-from-a-template)
+repository, [**rib-sample**](https://github.com/srid/rib-sample), from Github.
+
+### Directory structure
+
+Let's look at what's in the template repository:
+
+```shell
+$ git clone https://github.com/srid/rib-sample.git mysite
+...
+$ cd mysite
+$ ls -F
+a/  b/  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)
+   
+The template repository comes with a few sample posts under `a/`, and a basic
+HTML layout and CSS style defined in `Main.hs`. 
+
+### Run the site
+
+Now let's run them all. 
+
+Clone the sample repository locally, install [Nix](https://nixos.org/nix/) and
+run your site as follows:
+
+```shell
+nix-shell --run 'ghcid -T main'
+```
+
+Running this command gives you a local HTTP server at http://localhost:8080/
+(serving the generated files) that automatically reloads when either the content
+(`a/`) or the HTML/CSS/build-actions (`Main.hs`) changes. Hot reload, in other
+words.
+
+### How Rib works
+
+How does the aforementioned nix-shell command work?
+
+1. `nix-shell` will run the given command in a shell environment with all of our
+dependencies (notably the Haskell ones including the `rib` library itself)
+installed. 
+
+1. [`ghcid`](https://github.com/ndmitchell/ghcid) will compile your `Main.hs`
+   and run its `main` function.
+
+1. `Main.hs:main` in turn calls the Shake build action (via `Rib.App.run`)
+   defined in `Rib.Simple.buildAction` passing it your function `renderPage`.
+
+There is quite a bit going on in that step 3! Let's break it down:
+
+1. `Rib.App.run`: this parses the CLI arguments and runs the rib CLI "app" which
+   can be run in one of a few modes --- generating static files, watching the
+   `a/` directory for changes, starting HTTP server for the `b/` directory. By
+   default---without any explicit arguments---this will run the Shake build
+   action passed as argument on every file change and spin up a HTTP server.
+   
+1. `Rib.Simple.buildAction`: The `run` function takes a Shake build action to
+   run on file change. `Rib.Simple` provides a very simple build action for
+   generating the most simple static site --- a list of posts with static assets
+   --- which the sample repository uses.
+   
+Run that command, and visit http://localhost:8080 to view your site.
+
+### Editing workflow
+
+Now try making some changes to the content, say `a/first-post.md`. You should
+see it reflected when you refresh the page. Or change the HTML or CSS of your
+site in `Main.hs`; this will trigger `ghcid` to rebuild the Haskell source and
+restart the server.
+
+### What's next?
+
+Great, by now you should have your static site generator ready and running! What
+more can you do? Surely you may have specific needs; and this usually translates
+to running custom Shake actions during the build.
+
+Rib provides helper functions in `Rib.Shake` and `Rib.Pandoc` to make this
+easier. Indeed the `Rib.Simple.buildAction` function which the sample project
+readily uses makes use of these functions.
+
+In order to customize your site's build actions,
+
+1. Copy the source for `buildAction` from the
+[`Rib.Simple`](https://github.com/srid/rib/blob/master/src/Rib/Simple.hs) module
+to your `Main.hs`
+
+1. Make any customizations you want in *your* `buildAction` function.
+
+1. Use that as the argument to the `Rib.App.run` function in your `main`
+
+Notice how Rib's builtin `buildAction` is 
+[forward-defined](http://hackage.haskell.org/package/shake-0.18.3/docs/Development-Shake-Forward.html)
+which adds to the simplicity of the entire thing.
+
+## Examples
+
+* [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/ 
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/rib.cabal b/rib.cabal
new file mode 100644
--- /dev/null
+++ b/rib.cabal
@@ -0,0 +1,60 @@
+cabal-version: 2.2
+name: rib
+version: 0.2.0.0
+license: BSD-3-Clause
+copyright: 2019 Sridhar Ratnakumar
+maintainer: srid@srid.ca
+author: Sridhar Ratnakumar
+homepage: https://github.com/srid/rib#readme
+bug-reports: https://github.com/srid/rib/issues
+description:
+    Haskell library for writing your own static site generator
+build-type: Simple
+extra-source-files:
+    README.md
+    CHANGELOG.md
+
+source-repository head
+    type: git
+    location: https://github.com/srid/rib
+
+library
+    exposed-modules:
+        Rib.App
+        Rib.Pandoc
+        Rib.Server
+        Rib.Shake
+        Rib.Simple
+    hs-source-dirs: src
+    default-language: Haskell2010
+    ghc-options: -Wall -Wincomplete-uni-patterns
+                 -Wincomplete-record-updates
+    build-depends:
+        aeson >=1.4.2 && <1.5,
+        bytestring >=0.10.8 && <0.11,
+        containers >=0.6.0 && <0.7,
+        binary >=0.8.6 && <0.9,
+        text >=1.2.3 && <1.3,
+        time >=1.8.0 && <1.9,
+        async >=2.2.2 && <2.3,
+        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-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,
+        pandoc-include-code >=1.4.0 && <1.5,
+        shake >=0.18.3 && <0.19,
+        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,
+        base >=4.7 && <5,
+        pandoc >=2.7 && <3
diff --git a/src/Rib/App.hs b/src/Rib/App.hs
new file mode 100644
--- /dev/null
+++ b/src/Rib/App.hs
@@ -0,0 +1,88 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | CLI interface for Rib.
+--
+-- Typically you would call `Rib.App.run` passing your Shake build action.
+module Rib.App
+  ( App(..)
+  , run
+  , runWith
+  , ribOutputDir
+  , ribInputDir
+  ) where
+
+import Control.Concurrent (threadDelay)
+import Control.Concurrent.Async (concurrently_)
+import Control.Monad
+import Data.Bool (bool)
+
+import Development.Shake
+import Development.Shake.Forward (shakeForward)
+import System.Console.CmdArgs
+import System.FSNotify (watchTree, withManager)
+
+import qualified Rib.Server as Server
+
+data App
+  = Watch
+  | Serve { port :: Int, dontWatch :: Bool }
+  | Generate { force :: Bool }
+  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 ()
+  -- ^ Shake build rules for building the static site
+  -> IO ()
+run buildAction = runWith buildAction =<< cmdArgs ribCli
+  where
+    ribCli = modes
+      [ Serve
+          { port = 8080 &= help "Port to bind to"
+          , dontWatch = False &= help "Do not watch in addition to serving generated files"
+          } &= help "Serve the generated site"
+            &= auto
+      , Watch
+          &= help "Watch for changes and generate"
+      , Generate
+          { force = False &= help "Force generation of all files"
+          } &= help "Generate the site"
+      ]
+
+-- | Like `run` but with an explicitly passed `App` mode
+runWith :: Action () -> App -> IO ()
+runWith buildAction = \case
+  Watch -> withManager $ \mgr -> do
+    -- Begin with a *full* generation as the HTML layout may have been changed.
+    runWith 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
+    -- Wait forever, effectively.
+    forever $ threadDelay maxBound
+
+  Serve p dw -> concurrently_
+    (unless dw $ runWith buildAction Watch)
+    (Server.serve p ribOutputDir)
+
+  Generate forceGen ->
+    let opts = shakeOptions
+          { shakeVerbosity = Chatty
+          , shakeRebuild = bool [] [(RebuildNow, "**")] forceGen
+          }
+    in shakeForward opts buildAction
diff --git a/src/Rib/Pandoc.hs b/src/Rib/Pandoc.hs
new file mode 100644
--- /dev/null
+++ b/src/Rib/Pandoc.hs
@@ -0,0 +1,154 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+-- | Helpers for working with Pandoc documents
+module Rib.Pandoc
+  (
+  -- * Parsing
+    parse
+  , parsePure
+  -- * Converting to HTML
+  , render
+  , renderInlines
+  -- * Metadata
+  , getMeta
+  , setMeta
+  , parseMeta
+  -- * Extracting information
+  , getH1
+  , getFirstImg
+  )
+where
+
+import Control.Monad
+import Data.ByteString.Lazy (ByteString)
+import qualified Data.Map as Map
+import Data.Text (Text)
+import qualified Data.Text as T
+
+import Lucid (Html, toHtmlRaw)
+import Text.Pandoc
+import Text.Pandoc.Filter.IncludeCode (includeCode)
+import Text.Pandoc.Readers.Markdown (yamlToMeta)
+import Text.Pandoc.Shared (stringify)
+import Text.Pandoc.Walk (walkM, query)
+
+
+class IsMetaValue a where
+  parseMetaValue :: MetaValue -> a
+
+instance IsMetaValue [Inline] where
+  parseMetaValue = \case
+    MetaInlines inlines -> inlines
+    _ -> error "Not a MetaInline"
+
+instance IsMetaValue (Html ()) where
+  parseMetaValue = renderInlines . parseMetaValue @[Inline]
+
+instance IsMetaValue Text where
+  parseMetaValue = T.pack . stringify . parseMetaValue @[Inline]
+
+instance {-# Overlappable #-} IsMetaValue a => IsMetaValue [a] where
+  parseMetaValue = \case
+    MetaList vals -> parseMetaValue <$> vals
+    _ -> error "Not a MetaList"
+
+-- NOTE: This requires UndecidableInstances, but is there a better way?
+instance {-# Overlappable #-} Read a => IsMetaValue a where
+  parseMetaValue = read . T.unpack . parseMetaValue @Text
+
+-- | Get the metadata value for the given key in a Pandoc document.
+--
+-- It is recommended to call this function with type application specifying the
+-- type of `a`.
+--
+-- `MetaValue` is parsed in accordance with the `IsMetaValue` class constraint.
+-- Available instances:
+--
+-- - `Html`: parse value as a Pandoc document and convert to Lucid Html
+-- - `Text`: parse a raw value (Inline with one Str value)
+-- - @[a]@: parse a list of values
+-- - @Read a => a@: parse a raw value and then read it.
+getMeta :: IsMetaValue a => String -> Pandoc -> Maybe a
+getMeta k (Pandoc meta _) = parseMetaValue <$> lookupMeta k meta
+
+-- | Add, or set, a metadata data key to the given Haskell value
+setMeta :: Show a => String -> a -> Pandoc -> Pandoc
+setMeta k v (Pandoc (Meta meta) bs) = Pandoc (Meta meta') bs
+  where
+    meta' = Map.insert k v' meta
+    v' = MetaInlines [Str $ show v]
+
+-- | Pure version of `parse`
+parsePure :: (ReaderOptions -> Text -> PandocPure Pandoc) -> Text -> Pandoc
+parsePure r =
+  either (error . show) id . runPure . r settings
+  where
+    settings = def { readerExtensions = exts }
+
+-- | Parse the source text as a Pandoc document
+--
+-- Supports the [includeCode](https://github.com/owickstrom/pandoc-include-code) extension.
+parse
+  :: (ReaderOptions -> Text -> PandocIO Pandoc)
+  -- ^ Document format. Example: `Text.Pandoc.Readers.readMarkdown`
+  -> Text
+  -- ^ Source text to parse
+  -> IO Pandoc
+parse r =
+  either (error . show) (walkM includeSources) <=< runIO . r settings
+  where
+    settings = def { readerExtensions = exts }
+    includeSources = includeCode $ Just $ Format "html5"
+
+-- | Parse the metadata source as a Pandoc Meta value
+parseMeta :: ByteString -> IO Meta
+parseMeta = either (error . show) pure <=< runIO . yamlToMeta settings
+  where
+    settings = def { readerExtensions = exts }
+
+render' :: Pandoc -> Either PandocError Text
+render' = runPure . writeHtml5String settings
+  where
+    settings = def { writerExtensions = exts }
+
+-- | Render a Pandoc document as Lucid HTML
+render :: Pandoc -> Html ()
+render = either (error . show) toHtmlRaw . render'
+
+renderInlines' :: [Inline] -> Either PandocError Text
+renderInlines' = render' . Pandoc mempty . pure . Plain
+
+-- | Render a list of Pandoc `Text.Pandoc.Inline` values as Lucid HTML
+--
+-- Useful when working with `Text.Pandoc.Meta` values from the document metadata.
+renderInlines :: [Inline] -> Html ()
+renderInlines = either (error . show) toHtmlRaw . renderInlines'
+
+-- | Get the top-level heading as Lucid HTML
+getH1 :: Pandoc -> Maybe (Html ())
+getH1 (Pandoc _ bs) = fmap renderInlines $ flip query bs $ \case
+  Header 1 _ xs -> Just xs
+  _ -> Nothing
+
+-- | Get the first image in the document if one exists
+getFirstImg
+  :: Pandoc
+  -> Maybe Text
+  -- ^ Relative URL path to the image
+getFirstImg (Pandoc _ bs) = flip query bs $ \case
+  Image _ _ (url, _) -> Just $ T.pack url
+  _ -> Nothing
+
+exts :: Extensions
+exts = mconcat
+  [ extensionsFromList
+    [ Ext_yaml_metadata_block
+    , Ext_fenced_code_attributes
+    , Ext_auto_identifiers
+    , Ext_smart
+    ]
+  , githubMarkdownExtensions
+  ]
diff --git a/src/Rib/Server.hs b/src/Rib/Server.hs
new file mode 100644
--- /dev/null
+++ b/src/Rib/Server.hs
@@ -0,0 +1,75 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | Serve generated static files with HTTP
+module Rib.Server
+  (
+    serve
+  , getHTMLFileUrl
+  )
+where
+
+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 Network.Wai.Application.Static (defaultFileServerSettings, ssListing, ssLookupFile, staticApp)
+import qualified Network.Wai.Handler.Warp as Warp
+import WaiAppStatic.Types (LookupResult (..), Pieces, StaticSettings, fromPiece, unsafeToPiece)
+
+-- | WAI Settings suited for serving statically generated websites.
+staticSiteServerSettings :: FilePath -> StaticSettings
+staticSiteServerSettings root = settings
+  { ssLookupFile = lookupFileForgivingHtmlExt
+  , 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.
+--
+-- You may also pass source paths as long as they map directly to destination
+-- path except for file extension.
+getHTMLFileUrl
+  :: FilePath
+  -- ^ Relative path to a page (extension is ignored)
+  -> Text
+getHTMLFileUrl = T.pack . ("/" ++) . dropExtension
+
+-- | Run a HTTP server to serve a directory of static files
+--
+-- Allow URLs of the form @//foo//bar@ to serve @${path}//foo//bar.html@
+serve
+  :: Int
+  -- ^ Port number to bind to
+  -> FilePath
+  -- ^ Directory to serve.
+  -> IO ()
+serve port path = do
+  putStrLn $ "[Rib] Serving at http://localhost:" <> show port
+  Warp.run port $ staticApp $ staticSiteServerSettings path
diff --git a/src/Rib/Shake.hs b/src/Rib/Shake.hs
new file mode 100644
--- /dev/null
+++ b/src/Rib/Shake.hs
@@ -0,0 +1,127 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TupleSections #-}
+
+-- | 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
+  (
+  -- * Basic helpers
+    buildHtmlMulti
+  , buildHtml
+  -- * Read helpers
+  , readPandoc
+  , readPandocMulti
+  -- * Misc
+  , buildStaticFiles
+  )
+where
+
+import Control.Monad
+import Control.Monad.IO.Class
+import Data.Aeson (FromJSON, ToJSON)
+import qualified Data.Aeson as Aeson
+import Data.Binary
+import Data.Bool
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as BSL
+import Data.Text (Text)
+import qualified Data.Text.Encoding as T
+import Data.Typeable
+
+import Development.Shake
+import Development.Shake.FilePath
+import Development.Shake.Forward (cacheAction)
+import Lucid (Html)
+import qualified Lucid
+import Text.Pandoc (Pandoc (Pandoc), PandocIO, ReaderOptions)
+
+import Rib.App (ribInputDir, ribOutputDir)
+import qualified Rib.Pandoc
+
+-- FIXME: Auto create ./b directory
+
+
+-- | Shake action to copy static files as is
+buildStaticFiles :: [FilePattern] -> Action [FilePath]
+buildStaticFiles staticFilePatterns = do
+  files <- getDirectoryFiles ribInputDir staticFilePatterns
+  void $ forP files $ \f ->
+    copyFileChanged (ribInputDir </> f) (ribOutputDir </> f)
+  pure files
+
+-- | Convert the given pattern of source files into their HTML.
+buildHtmlMulti
+  :: (FilePattern, ReaderOptions -> Text -> PandocIO Pandoc)
+  -- ^ Source file patterns & their associated Pandoc readers
+  -> ((FilePath, Pandoc) -> Html ())
+  -- ^ How to render the given Pandoc document to HTML
+  -> Action [(FilePath, Pandoc)]
+  -- ^ List of relative path to generated HTML and the associated Pandoc document
+buildHtmlMulti spec r = do
+  xs <- readPandocMulti spec
+  void $ forP xs $ \x ->
+    buildHtml (fst x -<.> "html") (r x)
+  pure xs
+
+-- | Like `readPandoc` but operates on multiple files
+readPandocMulti
+  :: ( FilePattern
+     , ReaderOptions -> Text -> PandocIO Pandoc
+     )
+     -- ^ Tuple of pattern of files to work on and document format.
+  -> Action [(FilePath, Pandoc)]
+readPandocMulti (pat, r) = do
+  fs <- getDirectoryFiles ribInputDir [pat]
+  forP fs $ \f ->
+    jsonCacheAction f $ (f, ) <$> readPandoc r f
+
+-- | Read and parse a Pandoc source document
+--
+-- If an associated metadata file exists (same filename, with @.yaml@ as
+-- extension), use it to specify the metadata of the document.
+readPandoc
+  :: (ReaderOptions -> Text -> PandocIO Pandoc)
+  -- ^ Document format. Example: `Text.Pandoc.Readers.readMarkdown`
+  -> FilePath
+  -> Action Pandoc
+readPandoc r f = do
+  let inp = ribInputDir </> f
+  need [inp]
+  content <- T.decodeUtf8 <$> liftIO (BS.readFile inp)
+  doc <- liftIO $ Rib.Pandoc.parse r content
+  -- FIXME: When _creating_ the yaml file for first time, Shake doesn't know to
+  -- rebuild this.
+  boolFileExists (inp -<.> "yaml") (pure doc) $
+    fmap (overrideMeta doc) . readMeta
+  where
+    overrideMeta (Pandoc _ bs) meta = Pandoc meta bs
+    readMeta mf = do
+      need [mf]
+      liftIO $ Rib.Pandoc.parseMeta =<< BSL.readFile mf
+    -- | Like `bool` but works on file existence value
+    --
+    -- The second function takes the filepath as value.
+    boolFileExists fp missingF existsF =
+      doesFileExist fp >>= bool missingF (existsF fp)
+
+-- | Build a single HTML file with the given value
+buildHtml :: FilePath -> Html () -> Action ()
+buildHtml f html = do
+  let out = ribOutputDir </> f
+  writeHtml out html
+
+writeHtml :: MonadIO m => FilePath -> Html () -> m ()
+writeHtml f = liftIO . BSL.writeFile f . Lucid.renderBS
+
+-- | Like `Development.Shake.cacheAction` but uses JSON instance instead of Typeable / Binary on `b`.
+jsonCacheAction :: (FromJSON b, Typeable k, Binary k, Show k, ToJSON a) => k -> Action a -> Action b
+jsonCacheAction k =
+    fmap (either error id . Aeson.eitherDecode)
+  . cacheAction k
+  . fmap Aeson.encode
diff --git a/src/Rib/Simple.hs b/src/Rib/Simple.hs
new file mode 100644
--- /dev/null
+++ b/src/Rib/Simple.hs
@@ -0,0 +1,49 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- | Sensible defaults for writing the most simple static site
+module Rib.Simple where
+
+import Control.Monad
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Maybe (fromMaybe)
+import GHC.Generics (Generic)
+
+import Development.Shake (Action)
+import Lucid (Html)
+import Text.Pandoc (Pandoc, readMarkdown, readRST, readOrg)
+
+import Rib.Pandoc (getMeta)
+import Rib.Shake
+
+-- | Type of page to be generated
+data Page
+  = Page_Index [(FilePath, Pandoc)]
+  -- ^ Index page linking to a list of posts
+  | Page_Post (FilePath, Pandoc)
+  -- ^ Individual post page
+  deriving (Generic, Show, FromJSON, ToJSON)
+
+-- | Shake build action for the most simple static site
+--
+-- - Copies @static/@ as is.
+-- - Builds @*.md@, @*.rst@ and @*.org@ as HTML
+-- - Builds an @index.html@ of all pages unless `draft` metadata is set to `True`.
+buildAction :: (Page -> Html ()) -> Action ()
+buildAction renderPage = do
+  void $ buildStaticFiles ["static/**"]
+  posts <- concat <$> forM pats
+    (flip buildHtmlMulti $ renderPage . Page_Post)
+  let publicPosts = filter (not . isDraft . snd) posts
+  buildHtml "index.html" $
+    renderPage $ Page_Index publicPosts
+  where
+    isDraft = fromMaybe False . getMeta @Bool "draft"
+    pats =
+      [ ("*.md", readMarkdown)
+      , ("*.rst", readRST)
+      , ("*.org", readOrg)
+      ]
