Hakyll From Scratch Pt 2

Posted on June 4, 2026 by Jarvis Cochrane · Tagged ,

Previously in Hakyll From Scratch, I shared a minimal Hakyll program that copies a single HTML file to the _site directory.

But copying a single file to a directory does not a static site generator make.

(I’m mangling Aristotle apparently: μία γὰρ χελιδὼν ἔαρ οὐ ποιεῖ – One swallow does not a Spring make.)

Well, perhaps technically it does, but most practical sites have more extensive requirements. For example, copying multiple files from a source directory, and wrapping the content of each page in a standard page template.

Extended Minimal Site Generator

(Download the example)

The site generator, extended to support these new requirements, is now:

-- Minimal Hakyll site with
--  * Pages stored in `pages` subdirectory
--  * Default page template applied to page content

import Hakyll

main :: IO ()
main = hakyll $ do
  -- Compile templates & cache
  match (fromGlob "templates/*") $ compile templateBodyCompiler

  -- Read page content, apply template, and copy to destination
  -- For each page found in `pages`:
  match (fromGlob "pages/*.html") $ do
    -- Set the destination path to the basename
    route (gsubRoute "pages/" (const ""))
    -- Apply template to file contents
    compile $ getResourceBody
      >>= loadAndApplyTemplate
            (fromFilePath "templates/default.html")
            defaultContext

Whew! That’s a lot, isn’t it?

But let’s work through the changes little by little.

Do Notation

In the minimal example from Part 1, I replaced the idiomatic do notation by chaining expressions together with >>. This was to confirm that I mostly understood what do was, ah, doing under the hood.

I’m still wrestling with the complexities of do. It looks like sequential statements in, say, Python, but isn’t.

-- This:

match (fromGlob "index.html") $ do
  route idRoute
  compile copyFileCompiler

-- Is a shorthand for:

main = hakyll $
  match (fromGlob "index.html") (
    route idRoute >>
    compile copyFileCompiler)

Compiling Templates

Before they can be used in pages, Hakyll templates need to be compiled.

The loadAndApplyTemplate function is a little confusing because it doesn’t load the template from the filesystem by path, but from the cache of compiled templates using an identifier, which happens to be the path of the original template file. Not confusing at all!

match (fromGlob "templates/*") $ compile templateBodyCompiler

This expression matches all the files in the templates directory and specifies that the templateBodyCompiler should be used to compile them.

Note that templates don’t have a route specified, since the templates won’t be included in the site except as part of other pages.

Matching Pages

To find multiple source pages in a subdirectory, we can provide an extended glob pattern to fromGlob:

match (fromGlob "pages/*.html") $ do

This expression matches all the html pages in the pages directory and applies the processing that will be specified in the do clause.

Rewriting The Path

In the minimal example we used idRoute to set the relative destination path to be the same as the relative source path. index.html became _site/index.html.

For this example, the relative destination path should be the relative source path, but with the prefix pages/ removed. pages/index.html should become _site/index.html.

route (gsubRoute "pages/" (const ""))

This expression processes the source path of each page with a function that substitutes pages/ with an empty string.

Applying A Template

Each page matching pages/*.html should be loaded and then wrapped in a standard page template.

compile $ getResourceBody
  >>= loadAndApplyTemplate
        (fromFilePath "templates/default.html")
        defaultContext

While the function names make it pretty obvious what’s going on here, there’s a trap worth being aware of.

loadAndApplyTemplate doesn’t load the template from the filesystem using the supplied path. It uses the path as a key to look up the compiled template from the cache.

The defaultContext contains fields that can be used in templates such as the page title and body content.

The Template

The next-to-last piece of the puzzle is the page template. It’s written in a fairly typical HTML templating language using $...$ for interpolated content.

$title$ is the page title, and $body$ is the content of the source file.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>$title$</title>
  </head>
  <body>
    <nav>
      <a href="/index.html">Index</a>
      <a href="/about.html">About</a>
    </nav>
    <main>
      $body$
    </main>
  </body>
</html>

The Pages

The final pieces of the puzzle are the pages of content, which look like this:

<h1>Index</h1>
<p>Templated index page.</p>
<h1>About</h1>
<p>Templated about page.</p>

P.S.

The .cabal file is unchanged!

Download the example and stay tuned for part 3!