rib (empty) → 0.2.0.0
raw patch · 9 files changed
+700/−0 lines, 9 filesdep +aesondep +asyncdep +basesetup-changed
Dependencies added: aeson, async, base, binary, bytestring, clay, cmdargs, containers, data-default, fsnotify, http-types, lens, lens-aeson, lucid, mtl, pandoc, pandoc-include-code, pandoc-types, safe, shake, skylighting, text, time, wai, wai-app-static, wai-extra, warp
Files
- CHANGELOG.md +6/−0
- README.md +139/−0
- Setup.hs +2/−0
- rib.cabal +60/−0
- src/Rib/App.hs +88/−0
- src/Rib/Pandoc.hs +154/−0
- src/Rib/Server.hs +75/−0
- src/Rib/Shake.hs +127/−0
- src/Rib/Simple.hs +49/−0
+ CHANGELOG.md view
@@ -0,0 +1,6 @@+# Change Log for rib++## 0.2.0.0++- Initial release.+
+ README.md view
@@ -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/
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ rib.cabal view
@@ -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
+ src/Rib/App.hs view
@@ -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
+ src/Rib/Pandoc.hs view
@@ -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+ ]
+ src/Rib/Server.hs view
@@ -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
+ src/Rib/Shake.hs view
@@ -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
+ src/Rib/Simple.hs view
@@ -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)+ ]