packages feed

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 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)+      ]