Using the Ninja build system to generate this site

This static website is generated with a Python script that does not produce HTML files, but produces a build.ninja file, which is then processed by the Ninja build system to generate the final HTML files.

I don't implement all the source/target dependency graph logic myself, it's Ninja's job to take care of it (and build systems in general), and it knows how to do it right and efficiently, i.e., only (re)generating needed targets, leveraging parallelism to speed up the build time.

Ninja files are intended to be generated by a program rather than hand-written by a human. Ninja rules are very simple, beacuse the business logic decisions have been made by the program generating the Ninja build file: deciding what to map and how to map the source files to the destination HTML files, how to name each of them, how to process each of them. That's essentially what my static generator does.

Due to its nature, the Ninja build file may change often, but it's fine because Ninja maintains a log (.ninja_log file) so it knows how to rebuild targets correctly and efficienlty across multiple regenerations of the build.ninja file.

This is the job of my site generator:

The output file may look like this, the generator write a build-directive for each target it wants:
# Code generated. DO NOT EDIT.

rule justhtml
  command = cat $in | sed 's/:TITLE/$title/g' > $out
  description = $in -> $out

build www/colophon/index.html: justhtml header.html src/colophon.html footer.html
  title = Colophon

build www/contact/index.html: justhtml header.html src/contact.html footer.html
  title = Contact

build www/legal/index.html: justhtml header.html src/legal.html footer.html
  title = Mentions légales
...
...
...

The work Ninja does when nothing changed:

$ ./build.py 
ninja: no work to do.

After a new article is created or updated:

$ ./build.py
[1/1] header.html src/notes/site-generator-ninja.html footer.html -> www/notes/site-generator-ninja/index.html

After the global header has chaged (I have 11 pages generated in parallel on my 12-core machine, Ninja only displays one line):

./build.py     
[11/11] header.html src/index.html footer.html -> www/index.html

The generator is this one-file Python script (build.py), for now, because my current needs are so simple.

#!/usr/bin/env python3

import os
import re
from pathlib import Path

src = 'src'
dest = 'www'

header = 'header.html'
footer = 'footer.html'

default_title = "Lu's Website"


pattern = re.compile(r'<h1>(.+)</h1>')

with open('build.ninja', 'w') as f:
    f.write(f'''# Code generated. DO NOT EDIT.

rule justhtml
  command = cat $in | sed 's/:TITLE/$title/g' > $out
  description = $in -> $out
''')
    for hfile in Path(src).glob('**/*.html'):
        stem = hfile.parent / hfile.stem
        stem = str(stem).removeprefix(f'{src}/')
        if stem == 'index':
            continue

        # Fetch title (first h1)
        content = hfile.read_text(encoding='utf-8')
        m = pattern.findall(content)
        if not m:
            title = default_title
        else:
            title = m[0].strip()

        f.write(f'build {dest}/{stem}/index.html: justhtml {header} {hfile} {footer}\n  title = {title}\n')

    indexfile = Path(src) / 'index.html'
    if indexfile.is_file():
        f.write(f'build {dest}/index.html: justhtml {header} {indexfile} {footer}\n  title = Home\n')

os.system('ninja')

Okay, I have to admit: my needs are very basic (I don't even use Markdown!), so I can afford to toy around with a build system. I'll see if Ninja still make sense in a future where my needs may evolve.

Nevertheless, I use it for a year now, and it's fine for my purposes so far.