Skip to main content

hakyll.hs

Hakyll-based static site generator entry point orchestrating Pandoc transforms and template rendering

Pathbuild/app/hakyll.hs
LanguageHaskell
Lines602
Sourcebuild/app/hakyll.hs
at 406d3e423

Read this when Use this page when tracing the Haskell build pipeline, generators, metadata code, or backend utility behavior around hakyll.

Overview​

hakyll.hs is the main entry point for building gwern.net. It uses Hakyll (a Haskell static site generator) to compile Markdown files into HTML through a multi-stage pipeline: Hakyll parses the Markdown, Pandoc converts it to an AST, a chain of custom transforms enriches the AST, then Pandoc renders HTML which is wrapped in templates.

The file handles three main concerns: (1) pre-build setup including loading metadata databases and writing annotation fragments, (2) defining routing rules that control URL structure and file handling, and (3) the pandocTransform pipeline that applies 15+ AST transformations for typography, link annotations, archives, and more.

A key design decision is the SLOW environment variable toggle—expensive operations like running the full test suite only happen when SLOW=true. The build also supports targeted compilation via CLI arguments (e.g., ./hakyll build note/foo.md to rebuild a single file).


Public API​

main :: IO ()​

Entry point. Loads databases, writes blog entries and annotations, then runs Hakyll's build system.

main = do
arg <- lookupEnv "SLOW"
let slow = "true" == fromMaybe "" arg
am <- readArchiveMetadataAndCheck -- Archive link database
meta <- readLinkMetadataSlow -- Annotation database
sizes <- annotationSizeDB meta am -- Size metadata
writeOutBlogEntries meta
withArgs [head args] $ hakyll $ do ...

Called by: sync.sh (via the hakyll executable) Calls: readArchiveMetadataAndCheck, readLinkMetadataSlow, writeAnnotationFragments, pandocTransform


pandocTransform :: Metadata -> ArchiveMetadata -> String -> Pandoc -> IO Pandoc​

The core AST transformation pipeline. Applies all content transforms to a parsed Markdown document.

SizeDB is computed earlier for annotation fragment generation, but it is not passed into pandocTransform.

Called by: Hakyll's pandocCompilerWithTransformM Calls: (in order)

  1. convertInterwikiLinks - Expand interwiki syntax.
  2. footnoteAnchorChecker - Fail the build on suspicious short spaceless footnotes (non-index pages only).
  3. createAnnotations - Trigger annotation generation (non-index pages only).
  4. addPageLinkWalk - Mark local links before annotation/archive rewrites.
  5. nominalToRealInflationAdjuster - Adjust dollar amounts for inflation (non-index pages only).
  6. hasAnnotation - Add annotation classes from metadata.
  7. localizeLink - Rewrite to archived versions.
  8. typographyTransformTemporary - Typography fixes.
  9. headerSelflinkAndSanitize - Make headers self-linking and sanitize IDs.
  10. addPageLinkWalk - Re-mark local links after header/archive rewrites.
  11. wrapInParagraphs - Convert Plain blocks to paragraphs.
  12. imageLinkHeightWidthSet - Add image dimensions.
  13. addCanPrefetch - Mark prefetchable links.

postCtx :: Metadata -> ArchiveMetadata -> String -> String -> Context String​

Hakyll context providing template variables. This is where YAML frontmatter is processed and exposed to templates. (The Metadata/ArchiveMetadata arguments are needed for tag and author-HTML rendering, the first String is the index flag, the second the /ref/ timestamp.)

postCtx md am indexp rts =
fieldsTagPlain md <>
fieldsTagHTML md <>
authorPlainField "author" <>
titlePlainField "title-plain" <>
descField True "title" "title-escaped" <>
...
defaultContext

Template Variables Provided:

VariableDescription
$title$Title with HTML formatting
$title-plain$Plain text title for <title>
$title-escaped$HTML-escaped for <meta>
$description$Description with HTML
$description-escaped$Escaped description rendered through Utext for Unicode-rich plain text
$created$Creation date (YYYY-MM-DD)
$modified$Last modified date
$status$Writing status
$confidence$Certainty level
$importance$Topic importance (0-10 or N/A)
$tagsHTML$Rendered tag links
$tags-plain$Comma-separated tags
$thumbnail$OG image path
$safe-url$CSS-safe page identifier
$escaped-url$URL-encoded path
$backlinks-yes$Boolean: has backlinks?
$similars-yes$Boolean: has similar links?
$linkbib-yes$Boolean: has link bibliography?
$page-created-recently$CSS class if within C.isNewWithinNDays days; currently 62 days
$refMapTimestamp$Cache-busting timestamp for /ref/
$date-range-HTML$Formatted date range with duration

woptions :: WriterOptions​

Pandoc writer configuration.

woptions = defaultHakyllWriterOptions {
writerSectionDivs = True, -- Wrap sections in <section>
writerTableOfContents = True, -- Generate TOC
writerTOCDepth = 4, -- Include h1-h4
writerHTMLMathMethod = MathJax defaultMathJaxURL,
writerEmailObfuscation = NoObfuscation
}

The TOC is injected via a custom template that wraps content in #TOC and #markdownBody divs.


Internal Architecture​

Build Flow​

main
├── Load databases (archive, metadata, sizes)
├── Write blog entries
└── hakyll $ do
├── preprocess: writeAnnotationFragments
├── preprocess: testAll (if SLOW=true)
├── preprocess: writeOutID2URLdb
│
├── match "**.md" (excluding doc/www/**)
│ ├── Route: strip .md, remove commas/apostrophes, spaces→hyphens
│ ├── Compile: pandocCompilerWithTransformM + pandocTransform
│ ├── Apply template: default.html
│ └── Post-process: imgUrls (add dimensions)
│
├── match static files (metadata/**)
│ └── copyFileCompiler
│
├── match static files (doc/**, **.hs, **.css, etc.)
│ └── symlinkFileCompiler (custom forked Hakyll)
│
└── match "template/*.html"
└── templateCompiler

Note: The symlinkFileCompiler is a custom optimization requiring a forked Hakyll installation. Instead of copying files, it creates symlinks to save disk space and I/O. See hakyll#786.

Template System​

Templates live in template/ and use Hakyll's $variable$ syntax with conditionals:

$if(author)$
<meta name="author" content="$author$">
$else$
<meta name="author" content="Gwern">
$endif$

The main template default.html (186 lines) handles:

  • HTML <head> with SEO metadata
  • Body class assignment (page-$safe-url$ $css-extension$)
  • Article wrapper with title and metadata block
  • Backlinks/similar links sections (conditional)
  • Footer and analytics

Key Patterns

Index Page Detection​

Essays with index: True in frontmatter take a lighter transform path. They still run interwiki conversion, but skip footnote checking, annotation creation, and inflation adjustment:

let indexp = indexp' == "True"
let pw = if indexp then convertInterwikiLinks p
else walk footnoteAnchorChecker $ convertInterwikiLinks p

Safe ID Generation​

Headers are checked for invalid CSS characters (periods, colons, hashes) which would break JS/CSS selectors. A mismatch prints a red warning; it is a fatal error only if the ID becomes empty after filtering:

headerSelflinkAndSanitize x@(Header a (href,b,c) d) =
let href' = T.filter (`notElem` ['.', '#', ':']) href in
... when (href' /= href) $ printRed "Invalid ID for header after filtering!..."
if href' == "" then error "..." else ...

Progress Field Decoration​

Status/confidence fields get visual progress indicators via completionProgressHTML:

progressField "status" "status-plus-progress" <>
progressField "confidence" "confidence-plus-progress"

Duplicate Header Detection​

Fails the build (fatal error) if two top-level headers have identical text:

duplicateTopHeaders :: Pandoc -> [String]
duplicateTopHeaders = duplicates . query topHeaderTexts

Plain→Para Normalization​

HTML/Markdown roundtripping can produce Plain blocks instead of Para blocks in list items, causing <li>text</li> instead of <li><p>text</p></li>. Since gwern.net doesn't intentionally use Plain anywhere, a blanket rewrite converts all Plain to Para:

wrapInParagraphs :: Pandoc -> Pandoc
wrapInParagraphs = walk go
where go (Plain strs) = Para strs
go x = x

Ref Map Timestamp​

The template context includes $refMapTimestamp$ set to the most recently modified file in metadata/annotation/id/. This is used for cache-busting the /ref/ annotation lookup system.


Configuration

Environment Variables​

VariableEffect
SLOW=trueRun full test suite, expensive transforms

CLI Arguments​

./hakyll build                    # Build all .md files
./hakyll build note/foo.md # Build single file
./hakyll build --annotation-rebuild # Rewrite all annotations
./hakyll build --annotation-missing-one-shot # Write missing, then exit
./hakyll watch # Dev server with live reload
./hakyll clean # Remove _site/ and _cache/

YAML Frontmatter Fields​

Required:

  • title - Page title (can include HTML like <em>)
  • created - ISO date (YYYY-MM-DD)
  • status - Writing stage: abandoned/notes/draft/in progress/finished/obsolete
  • description - Page summary

Optional:

  • modified - Last edit date
  • confidence - Certainty validated by C.yamlValidConfidences (certain, highly likely, likely, possible, unlikely, highly unlikely, remote, impossible, log, emotional, fiction)
  • importance - Topic significance: 0-10 or N/A
  • author - If not Gwern
  • thumbnail - OG image path
  • thumbnail-text - Alt text for thumbnail
  • thumbnail-css - CSS classes for thumbnail
  • css-extension - Additional body classes (e.g., dropcaps-de-zs)
  • tags - List of tag slugs
  • index - True for tag directories (skips some transforms)
  • error404 - True for the 404 page (adds noindex, loads guesser JS)
  • placeholder - Skips footer SSI

Integration Points​

Databases Read​

DatabaseLoaderPurpose
Archive metadatareadArchiveMetadataAndCheckURL→local path mappings
Link metadatareadLinkMetadataSlowAnnotation data (title, author, abstract)
Size DBannotationSizeDBFile sizes for links

Files Written​

FileWriterPurpose
Annotation fragmentswriteAnnotationFragments/metadata/annotation/*.html
Blog entrieswriteOutBlogEntriesNewsletter content
ID→URL mappingwriteOutID2URLdb/ref/ cache

Modules Called​

The local custom imports include:

  • Image: dimensions, optimization
  • Inflation: dollar adjustment
  • Interwiki: [WP:...](WP:...) expansion
  • LinkArchive: archive.org localization
  • LinkBacklink: backlink detection
  • LinkMetadata: annotation system
  • LinkMetadataTypes: metadata and size database types
  • Tags: tag rendering
  • Typography: text polish
  • Utils: helpers
  • Test: test suite
  • Config.Misc: date utilities
  • Metadata.Author: author cleanup and linking
  • Metadata.Date: date range formatting
  • LinkID: ID mapping
  • Blog: newsletter generation
  • Utext: Unicode-rich plain text conversion

See Also