Site logo

Creating a Static Site Generator

October 2026

For years I wanted the site generator for ko(9) to support live reloading so I could immediately see my edits rendered in the browser. I built a simple Swift web server for that purpose, but it wasn’t easy to hack it into the site generator I was using, Publish. I ended up writing an entirely new static site generator from scratch, keeping alive that great programming tradition of spending far more time fiddling with a blog than actually writing articles for it 😅

I was generally happy with Publish and had customised it to my liking over the years. It comes with everything included — a decent Markdown parser called Ink and a DSL for writing HTML templates (Plot) — which is why I was reluctant to replace it. Some of the improvements I had in mind were going to be difficult to make to Publish, and it has also been abandoned for years now. Plus it’s nice to have a low-priority side project and actually write some code by hand again!

The result is a site generator that does exactly what I need and nothing I don’t need. This article is the very first one second I’m publishing with it.

Development workflow

Several factors made this project particularly satisfying.

Firstly, I was replacing an existing system which had clearly-defined outputs — the generated site — so I could just tweak the new code until the output was identical. The whole rendered site is in Git so whenever I regenerate the HTML code it’s easy to spot any differences. Better than any unit tests.

Secondly, the components of the system were fairly independent and could be swapped out one at a time. For example, I kept using Plot and Ink to parse Markdown and render HTML while I built the file management part, and then replaced each of those in turn. After each step, I verified the generated HTML matched (or contained deliberate improvements) and removed the old dependency.

I created a rough list of tasks to be done, broken up into arbitrary phases which were in chunks of an hour or two of work each. It’s rewarding to tick off each item and check it in, and also provides a natural break point to stop coding.

Here’s a snippet from my actual task list:

Phase 1

- ✅ Scan for markdown files in a dir (non recursive)
- ✅ Load & transform into HTML
- ✅ Start a simple local server
- ✅ Map URLs to files, serve HTML

Phase 2

- ✅ Generate indexes (home screen, posts, tags)
- ✅ Output path (e.g. posts/foo/index.html)
- ✅ Write files to output path

Phase 3

- ✅ RSS feed
- ✅ Sitemap.xml
- ✅ Site config (title, base URL etc)
- ✅ Match output of Publish

Modelling a web site

Apart from Markdown parsing and HTML generation which are the bare minimum for a site generator, I really wanted it to capture my mental model of a website. This means abstracting site resources (articles, indexes, images) away from the source files, output paths and URLs.

Resource IDs

Instead of dealing with paths and URLs, every resource is referred to by a simple globally-unique ID. This lets me link to any document from Markdown like this: [current post](@new-site-generator) which renders as: current post. I don’t need to worry about what the URL to that document is or where the document comes from. It’ll always resolve to the correct link when the HTML is rendered.

The ID is usually derived from the source filename and there are a some predefined resource IDs like “@home” or “@tags”. Naturally, all resource IDs are verified when the site is generated, so it’s impossible for me to link to a file (or even an anchor within an article) which doesn’t exist.

Resource types

Every type of file on the site is represented by a class that conforms to the Resource protocol. Each resource type is created with input (a source file or list of other resources) and knows how to render an output file. The protocol also contains basic details like title and modified date. I currently have these types of Resource:

  • Article: a post like this one, rendered from Markdown
  • ArticleIndex: a generated list of articles, like Posts or the home page
  • StaticFile: images or CSS files which are used as-is
  • TagIndex and TagsIndex: a generated list of articles for a particular tag like swift or All tags
  • RSSFeed and SiteMap: specialised XML generators for a list of articles

Adding a new resource type is straightforward; the SiteMap class is only 40 lines of code including rendering.

Resource paths

Every resource has three paths which are related but slightly different:

  1. A source path for the original file like content/posts/some-article.md. The source can be a folder as for ArticleIndex or omitted for virtual indexes like TagIndex.
  2. An output path for the generated file: output/posts/some-article/index.html
  3. A remote path which is the canonical URL on the server: /posts/some-article/

Resources are created by scanning particular folders for Markdown files (articles) or any files (static resources like CSS and images). Each resource type knows how to derive its remote and output paths (and resource ID) from the source path.

Core features

The bulk of the work was finding replacements for the main features from the old setup: rendering Markdown to HTML, code syntax highlighting and sitemap generation.

Markdown parsing

The old site generator used Ink to parse the Markdown text that these articles are written in. Ink lets you customise the parsing using modifiers that can replace the generated HTML for a particular type of element. I used modifiers to add anchors to headings, convert straight quotes to “curly quotes”, and add syntax highlighting to code blocks.

I replaced Ink with the new official Swift Markdown parser, which is more powerful as a parser but doesn’t come with HTML conversion built-in. Unlike Ink where the modifiers only run during the Markdown to HTML conversion, Swift Markdown allows you to traverse and manipulate the parsed syntax tree. For example, to extract all the heading names out of a document, just create a struct that conforms to MarkupWalker and override the function corresponding to the type you’re interested in, visitHeading in this case. This is the all the code that’s needed:

struct HeadingWalker: MarkupWalker {
    var anchors: [String] = []

    mutating func visitHeading(_ heading: Heading) {
        anchors.append(heading.slug)
    }
}

var walker = HeadingWalker()
walker.visit(markdown)

print(walker.anchors)

Swift Markdown doesn’t natively support the “front matter” metadata at the top of my Markdown files — it gets parsed as a <h2> element — but that was easy enough to extract.

For HTML conversion I just created a MarkupVisitor to render each of the Markdown elements to a HTML tag. This is a simplified version; in reality I returned HTML DSL elements instead of directly outputting String:

struct HTMLConverter: MarkupVisitor {
    mutating func visitInlineCode(_ inlineCode: InlineCode) -> String {
        "<code>\(inlineCode.plainText)</code>"
    }
    
    // plus 20-30 similar functions
}

Converting resource IDs to their resolved URLs was done by another MarkupRewriter. I threw away my curly quotes modifier since Swift Markdown converts those automatically.

HTML rendering

Most of the HTML for the site is generated from the Markdown articles, but I still need a way to create the page templates and index pages. One way would be to use a templating language which substitutes placeholders in text, but since I didn’t need much raw HTML, I decided to specify the templates in Swift code:

div(class: "wrapper") {
    article {
        div(class: "content") {
            h1(title)
            p(inline: true) {
                i(published.formatted(.dateTime.month(.wide).year()))
            }

This snippet comes from the article template that generates the HTML source of this very page:

<div class="wrapper">
    <article>
        <div class="content">
            <h1>Creating a Static Site Generator</h1>
            <p><i>October 2026</i></p>

It was a bit fiddly getting the pretty-printed HTML to match the output from the old site generator but it’s really worth having that reference to compare against. And why shouldn’t generated code look nice and readable?

The HTML renderer consists of three parts:

  1. A HTMLTag type which knows how to render itself to a string
  2. A @resultBuilder that makes the Swift DSL look cleaner
  3. A collection of helpers for creating HTML tags

Here’s the tag type without the rendering code:

struct HTMLTag {
    let name: String
    let attribs: KeyValuePairs<String, String?>
    let children: [HTMLElement]
    let inline: Bool
}

Why KeyValuePairs? It can be initialised with a dictionary literal, but unlike an actual Dictionary it maintains the order of elements, which is essential for deterministic output.

A HTMLElement type is also needed because “plain text” is a valid node type in HTML. The .escaped case indicates that the string is already rendered HTML and doesn’t need any extra processing for special characters (used for syntax-highlighted code).

enum HTMLElement {
    case tag(HTMLTag)
    case text(String)
    case escaped(String)
}

The helper functions are not strictly necessary but they make the DSL code easier to read, since I can use the HTML tag name instead of HTMLTag(...) everywhere. I ended up with about 30 of them that look like this:

func h1(_ str: String) -> HTMLElement {
    .tag(HTMLTag(#function, children: [.text(str)], inline: true))
}

func img(src: String, alt: String? = nil) -> HTMLElement {
    .tag(HTMLTag(#function, attribs: ["src": src, "alt": alt]))
}

Using #function makes the HTML tag name the same as the Swift function name for easy copy-pasting.

Since the functions have to be global, name collisions become an issue. If this Swift Evolution pitch on scoping result builder functions ever gets accepted that would make writing DSLs simpler.

However, after creating a load of short but repetitive functions, I switched to using a struct with callAsFunction instead:

struct TagBuilder {
    let tagName: String

    /// Creates a tag with child tags
    func callAsFunction(
        class cls: String? = nil, @HTMLBuilder children: () -> [HTMLElement]
    ) -> HTMLElement {
        .tag(HTMLTag(tagName, attribs: ["class": cls], children: children()))
    }

    /// Creates a tag with text content
    func callAsFunction(_ textContent: String) -> HTMLElement {
        .tag(HTMLTag(tagName, children: [.text(textContent)]))
    }
}

// Define a TagBuilder for each HTML tag that can have child tags
let div = TagBuilder("div")
let h1 = TagBuilder("h1")
let p = TagBuilder("p")

// Write HTML like:
div(class: "content") {
    h1(title)
    p("Hello world")
}

That works fine for tags with child tags, but does require each possible attribute to be an explicit function argument. I also created a “dynamic” tag which accepts any attribute name but can’t have child tags:

@dynamicCallable
struct DynamicTagBuilder {
    let tagName: String

    func dynamicallyCall(
        withKeywordArguments args: KeyValuePairs<String, String>
    ) -> HTMLElement {
        .tag(HTMLTag(tagName, attribs: args))
    }
}

let img = DynamicTagBuilder("img")

// Can accept any attribute names:
img(src: site[.headerImage].remotePath, alt: "Site logo", foo: "bar")

It seems you cannot have a dynamicallyCall function which also has a callAsFunction trailing closure. I reported this as a Swift compiler bug a few years back but it has not been picked up yet.

The previous solution with individual functions only allowed the valid attributes for each HTML tag, but I didn’t really need that level of strictness.

XML rendering

A couple of the documents on the site are XML, not HTML: the RSS feed and the site map. To render these I used the built-in XMLElement type, mainly just to see how easy it was to work with. In retrospect, I could have just created my own renderer but it worked out to only 80 lines of helper code including another basic result builder.

I didn’t bother making nicely-named functions for each element type because there was so little XML to generate. Here’s the Swift source to render my site map:

XMLDocument(
    XMLElement("urlset", attribs: [
        "xmlns": "http://www.sitemaps.org/schemas/sitemap/0.9",
    ]) {
        for resource in resources {
            XMLElement("url") {
                XMLElement("loc", resource.fullUrl.absoluteString)
                XMLElement("lastmod", resource.modified.ymd)
            }
        }
    }
).xmlData(options: [.nodePrettyPrint, .nodeCompactEmptyElement])

I could have also implemented my HTML renderer using XMLElement but building my own gave me more control over the output.

Syntax highlighting

Since these articles are mainly about coding, there are unsurprisingly a lot of code snippets on the site. The old site generator had a plugin which internally used HighlightJS for syntax highlighting. I wasn’t really interested in re-implementing a code parser, so I just ended up doing it the same way. After adding the JS library to the app bundle, it’s possible to call it like so:

func highlight(code: String) -> String {
    let hljs: JSValue = {
        let context = JSContext()!
        let jsPath = Bundle.module.url(
            forResource: "highlight",
            withExtension: "js"
        )!
        let js = try! String(contentsOf: jsPath, encoding: .utf8)
        context.evaluateScript(js, withSourceURL: jsPath)
        return context.objectForKeyedSubscript("hljs")!
    }()

    let result = hljs.invokeMethod("highlightAuto", withArguments: [code])!

    let highlighted = result.objectForKeyedSubscript("value").toString()!

    return pre(class: "hljs") {
        code(inline: true) {
            .escaped(highlighted)
        }
    }
}

This just loads the Javascript source, calls the highlightAuto function, extracts the return value and wraps it in an HTML tag.

There’s a few force-unwraps here but for a low stakes utility app I prefer to fail loudly instead of adding error handling that achieves the same effect. I also don’t really have a problem with using a well-supported library like this even if it is in another language. The syntax highlighting gets baked into the HTML so I don’t need to actually serve the Javascript from my site.

New features

I’ve added a few nifty features that the old site generator didn’t have:

  • Detecting unused assets. Since I’m scanning every link on the site, it’s easy to see which images or CSS files are not used anywhere.
  • External link checker. Makes a HEAD request to every site linked from my articles. It only runs on demand and has already picked up a few dead links in old articles.
  • Automatic MIME types. The <link> tags in the page head contain the type of the linked file (such as type="image/png"). I already built a MIME-type detector for my simple web server so I reused it for this too.
  • One-step publishing. A single command moves a draft Markdown file into the right place and updates the published date to now. In future I might even have it sync to the server over SFTP.

And finally, the feature which kicked off this whole quest…

Live reloading

At last, I can switch between Xcode (my Markdown editor) and Safari and see my latest edits shown instantly. It was totally worth rewriting the whole site generator just for this 😂

I wrote a file watcher to notify when any of the source Markdown files were modified and reload them automatically. This uses the File System Events API in macOS which I have to say is one of the ugliest, hardest to use frameworks in Swift. Check out this delightful little snippet:

var context = FSEventStreamContext(
    version: 0,
    info: Unmanaged.passUnretained(self).toOpaque(),
    retain: nil,
    release: nil,
    copyDescription: nil
)

var stream = FSEventStreamCreate(
    nil,
    Self.callback,
    &context,
    paths as CFArray,
    FSEventStreamEventId(kFSEventStreamEventIdSinceNow),
    latency,
    FSEventStreamCreateFlags(
        kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagUseCFTypes
    )
)

static let callback: FSEventStreamCallback = {
    (_, info, numEvents, eventPaths, eventFlags, _) in
    let watcher = Unmanaged<FileWatcher>.fromOpaque(info!).takeUnretainedValue()
    let paths = (Unmanaged<CFArray>.fromOpaque(eventPaths).takeUnretainedValue())
        as NSArray as! [String]
    let flags = UnsafeBufferPointer(start: eventFlags, count: numEvents)
    // ...
}

Yikes.

Swift makes it very clear with all the Unmanaged and Unsafe calls required that this is not a modern API. It doesn’t even play well with classes unless you pass around opaque pointers to self.

Even when you do get it working it seems to fire off all kinds of crazy event sequences. File deletion sometimes sends “removed” and “created” notifications, renaming a file can send “renamed” and “modified”, and deleting a file sends “renamed” (I assumed as it’s moved into Trash).

It did the job though!

But while I was writing this article I wondered “why don’t I just reload the source Markdown whenever I get a HTTP request for it?”, since I’m only going to be refreshing the page when I’ve made a change I want to see. I ended up deleting the whole file watcher and replacing it with two lines of code. Let that be a lesson in building something the proper way instead of the simple way I guess.

Even better, since Xcode has some magic where it automatically saves files when you view them in another app, I don’t even need to hit Save. Combined with a tiny snippet of Javascript — on the local server only of course — the page refreshes automatically when I flip back to Safari with Cmd+Tab. That’s about as live as a static site editor gets.

Wrapping up

This is normally where I’d post the full source code for my site generator. I’m not going to though, because as I mentioned it’s customised exactly to my needs and I have no interest in making it more generic. But mainly because the point was that it’s a fun exercise to build your own.

It looks like I’m all out of excuses now though, I’m going to have to finally tackle that backlog of half-finished articles!


Any comments or questions about this post? ✉️ nick @ this domain.

— Nick Randall

Tagged with: