DEV Community

Kazuki Okamoto
Kazuki Okamoto

Posted on

Static site generator powered by Shake, Lucid, and Hint

The Japanese version is at はてなブログ.


I have a website for self-published books (Doujin-shi; 同人誌), which was generated by Jekyll. I replaced it with a generator powered by Shake, Lucid, and Hint.

The source codes are in this repository. And I made a template repository.

Why

I chose Jekyll naturally because I chose GitHub Pages as a host. When I followed the rail, Jekyll was useful. However, when I wanted to do out of the rail, Jekyll got to be hard to be used.

I will explain what the website is before an explanation of what I wanted to do. This website introduces my self-published books and consists of pages of each book and a page of a list of books. Every book has information on sales and exhibitions (Sokubaikai; 即売会) because the books are distributed in ones. And I wanted to show which books are distributed on a sale and exhibition. In other words, I wanted pages of each sale and exhibition and a page of a list of ones. This was difficult with Jekyll (at least with a version of that time).

I made the site on about January 2018, and soon I intended to change its framework. I tried Hakyll but I didn't like its API design and I didn't change it for a time.

A day I found Shake, which is used by the GHC project as a build system. I thought it may be useful for my site too. And I found two static website generators using Shake which are Rib and Slick.

I preferred Slick's API and tried to use it. After I removed unnecessary parts of Slick for me, I found no need for Slick and used Shake directly.

Project structure

Following is a figure of its project structure.

Project structure

First I didn't want compilations with GHC and links each time contents are changed because they take a long time. And so I decided to embed a Haskell interpreter with a Hint package.

This is a flow of a generation: writing a rule of a build with Shake, embedding an interpreter with Hint, and building executable gen. By executing gen, reading Haskell source codes of contents and images, etc., converting them, and outputting them as HTML, etc.

Data.hs is read by both because it is used for generating of gen and by the interpreter executed by gen.

There are two styles of Shake's rule. One is “backward” style like Make from products to sources, the other is “forward” one from sources to products. This time I used forward style. The forward style needs Filesystem Access Tracer (fsatrace).

Implementation

A directory structure is like this.

  • app
    • gen.hs — a generator itself
  • content
    • book/xxx.hs — a page of a book
    • image
    • lib/Layout.hs — layouts wrapping contents
    • style
    • xxx.hs — other pages
  • lib
    • Data.hs — data types for both gen.hs and book/xxx.hs
  • doujin-site.cabal

A point of gen is about this.

lucid :: forall p r. (Show p, Typeable r) => FilePath -> FilePath -> p -> Shake.Action r
lucid source destination param = do
  libs <- Shake.getDirectoryFiles "content/lib" ["*.hs"]
  result <- liftIO $ Hint.runInterpreter $ do
    Hint.set [Hint.languageExtensions := [Hint.DuplicateRecordFields, Hint.OverloadedStrings]]
    Hint.loadModules $ ("content" </> source) : (("content/lib" </>) <$> libs)
    Hint.setTopLevelModules ["Main"]
    Hint.setImports ["Data.Functor.Identity", "Lucid", "Data.Text"]
    Hint.interpret ("render (" ++ show param ++ ")") (Hint.as :: Lucid.Html r)
  case result of
    Left e  -> do
      liftIO $ hPutStrLn stderr $ displayException e
      fail "interpret"
    Right html -> do
      Shake.writeFile' ("out" </> destination) $ show html
      pure $ runIdentity $ Lucid.evalHtmlT html
Enter fullscreen mode Exit fullscreen mode

I think this is not very difficult. There is one constraint which is that show param should output a valid Haskell code. When it is derived automatically it must be no problem but manual implementations of show may not work.

Haskell source codes of contents must expose render :: Typeable r => p -> Html r. book/xxx.hs is like bellow.

{-# LANGUAGE OverloadedStrings #-}

import           Data
import qualified Layout as L

import Lucid

render path = do
  L.top (L.ogp ogp) $ L.book book (Just content)
  pure book
  where
    ogp = 
    book = 
    content = 
Enter fullscreen mode Exit fullscreen mode

The Book return values of each page are gathered and passed to index.hs. index.hs uses it for making a list of books.

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns        #-}
{-# LANGUAGE OverloadedStrings     #-}

import           Data
import qualified Layout as L

import Data.Foldable as F
import Lucid

render (path, books) =
  L.top
    (L.ogp ogp)
    $ div_ [class_ "home"] $ do
        ul_ [class_ "book-list"] $ do
          F.for_ books $ \(Book { title, bookImage, events }, path) -> do
            li_ $ do
              h3_ $
                a_ [class_ "post-link", href_ path] $ toHtml title
              div_ [class_ "justify-bottom"] $ do
                ul_ [class_ "event-badges"] $ do
                  F.for_ events $ \Event { title } -> do
                    li_ [class_ "event-badge"] $ toHtml title
                    " "
                a_ [href_ path] $ img_ [src_ bookImage, alt_ "book image", class_ "home-book-front"]
  where
    ogp = 
Enter fullscreen mode Exit fullscreen mode

I chose Lucid as an HTML notation. A unified syntax is easy because Lucid is an embedded DSL. You can change this part if you want.

After these preparations, I think it is easy to make pages of sales and exhibitions and a page of a list of them.

Summary

  • Shake as a dependencies resolver
  • Hint for embedding an interpreter
    • Compilation of contents is unnecessary
  • Lucid as an HTML notation
    • EDSL produces a unified syntax

In my opinion, I have noticed again that I prefer combining libraries as parts to making content for frameworks because the former is more flexible.

Top comments (0)