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
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")
defaultContextWhew! 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 templateBodyCompilerThis 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") $ doThis 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")
defaultContextWhile 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!