Making a blog from scratch

Let’s see how to use achille for making a static site generator for a blog. First we decide what will be the structure of our source directory. We choose the following:

└── posts

We define the kind of metadata we want to allow in the frontmatter header of our markdown files:

{-# LANGUAGE DeriveGeneric #-}

import GHC.Generics
import Data.Aeson
import Data.Text (Text)

data Meta = Meta
  { title :: Text
  } deriving (Generic)

instance FromJSON Meta

This way we enfore correct metadata when retrieving the content of our files. Every markdown file will have to begin with the following header for our generator to proceed:

title: Something about efficiency

Then we create a generic template for displaying a page, thanks to lucid:

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE BlockArguments    #-}

import Lucid.Html5

renderPost :: Text -> Text -> Html a
renderPost title content = wrapContent do
  h1_ $ toHtml title
  toHtmlRaw content

renderIndex :: [(Text, FilePath)] -> Html a
renderIndex = wrapContent .
  ul_ . mconcat . map \(title, path) ->
    li_ $ a_ [href_ path] $ toHtml title

wrapContent :: Html a -> Html a
wrapContent content = doctypehtml_ do
    head_ do
        meta_ [charset_ "utf-8"]
        title_ "my very first blog"

    body_ do
        header_ $ h1_ "BLOG"

We define a recipe for rendering every post:

buildPosts :: Task IO [(String, FilePath)]
buildPosts =
  match "posts/*.md" do
    (Meta title, text) <- compilePandocMetadata
    saveFileAs (-<.> "html") (renderPost title text)
      <&> (title,)

We can define a simple recipe for rendering the index, given a list of posts:

buildIndex :: [(Text, FilePath)] -> Task IO FilePath
buildIndex posts =
  save (renderIndex posts) "index.html"

Then, it’s only a matter of composing the recipes and giving them to achille:

main :: IO ()
main = achille do
  posts <- buildPosts
  buildIndex posts

And that’s it, you now have a very minimalist incremental blog generator!