Jan MiksovskyArchive AboutFeedSearchContact

Code is more coherent than configuration: comparing a sample blog in Web Origami and Eleventy

This post is the second in a series comparing the same sample blog in Web Origami and Eleventy:

Today let’s look at how both projects define the overall structure of the site and consider whether they can present a coherent picture of what you’re building.

Like most static site generators, Eleventy leverages the tree-like structure of a folder hierarchy to approximate the tree-like structure of a site. The good news is that the file system itself gives you the best picture you’re going to get of the resulting site. That’s also the bad news.

Here’s the folder structure of this Eleventy project, including the relevant source files:

_config/
  filters.js
_data/
  eleventyDataSchema.js
  metadata.js
_includes/
  layouts/
    base.njk
    home.njk
    post.njk
  postslist.njk
content/
  blog/
    blog.11tydata.js
  blog.njk
  content.11tydata.js
  index.njk
  sitemap.xml.njk
  tag-pages.njk
  tags.njk
eleventy.config.js

The above organization alone may not mean much to the uninitiated, and sadly folders on their own can’t have comments. Nevertheless, experienced Eleventy developers can probably envision the resulting site, especially if they also scan the lengthy configuration file.

There are also many little files that configure different parts of the site’s construction, like content/content.11tydata.js:

export default {
  layout: "layouts/home.njk",
};

Most of these configuration files have no explanatory comments, by which I only conclude that we’re not expected to look at them. But if the average user isn’t expected to look at these files, why not have comments for those users that do look at them?

The above file sets a path to a Nunjucks layout, but I couldn’t see how it was used. As discussed last time, configuration-oriented tools tend toward “action at a distance” behavior that is hard to intuit. It’s also the case that there’s just not much to go on here when searching for answers.

I eventually learned that a file called content.11tydata.js is a directory-specific data file that implicitly associates its exported data with the containing folder. In this case, it defines a default layout property that will be applied as the base template for other templates in the content folder, like content/index.njk.

Most of the Eleventy configuration code feels like this. The site builds a blog as advertised, but it feels like substantial work to piece together the site’s construction to the point where you could change it.

In contrast, the premise of a coding-focused approach like Origami is that you describe what you want in code. Given that freedom, most Origami users elect to define their site’s top-level tree of resources in a single file, providing a coherent map of the project. Here’s the whole site.ori file for the sample blog:

// This file defines the structure of the entire blog site
{
  about: {
    // About page
    index.html: templates/base.ori.html(Origami.mdHtml(about.md))
  }

  // Static assets like stylesheets
  assets/

  // Blog area
  blog: {
    // Blog index page
    index.html = templates/blogIndex.ori.html(posts.ori)

    // Create a folder for each post
    ...Tree.map(posts.ori, {
      key: (post, key) => `${ key }/`
      value: (post, key, tree) => {
        // Index page for post folder
        index.html: templates/post.ori.html(post, key, tree)
        // Any associated images
        ...post.images
      }
    })
  }

  feed: {
    // Blog feed in RSS format
    feed.xml = Origami.rss(feed.ori(posts.ori))
  }

  // Home page
  index.html = templates/index.ori.html(posts.ori)

  // Tags area
  tags: {
    // Tag index page
    index.html: templates/tagIndex.ori.html(tags.ori)

    // Create a folder for each tag
    ...Tree.map(tags.ori, {
      key: (group, tag) => `${ Origami.slug(tag) }/`,
      value: (group, tag) => {
        index.html: templates/tag.ori.html(group, tag)
      }
    })
  }

  // Not Found page
  404.html = templates/base.ori.html(Origami.mdHtml(404.md))
}

// Add a sitemap for all of that
→ (site) => {
  ...site
  sitemap.xml = Origami.sitemap(site, { base: metadata.yaml/url })
}

Even if you don’t know Origami or JavaScript, you can probably squint and perceive the structure of the final site. All references here are explicit and by name.

For example, at the top you can see that the about area contains a page called index.html. The formula for that page may be unclear to you, but it contains references to the files templates/base.ori.html and about.md, so you can look at those. Even if you don’t know what something does, you at least have a name to search for. Searching the Web Origami documentation for the remaining term in that formula finds the built-in function, Origami.mdHtml.

This single site.ori file pulls together the bulk of the logic behind the site. I think such a coherent, text-based map of the site is enormously helpful in understanding and remembering how the parts fit together. You can also ask Origami to draw a visual diagram of the running site to confirm your understanding.

Other posts in this series:

  1. Code is easier to follow than configuration
  2. Code is more coherent than configuration [this post]
  3. Code is more expressive than configuration [coming]
  4. Code is more concise than configuration [coming]

Code is easier to follow than configuration: comparing a sample blog in Web Origami and Eleventy

This post series is for people who want to build or rebuild a site.

You may have heard of Eleventy, a popular static site generator, and maybe heard it’s simple to use. To evaluate that simplicity, I’ll compare a sample blog in Eleventy to the same blog in Web Origami. This will be similar to my comparison last year of Astro and Origami.

If you’re shopping for a site-building tool, I hope this series can help inform your decision. If you already use Eleventy, I’m happy you’ve found something that works for you. As I said last year, anything that makes people more likely to create a site is fantastic.

A difference in strategy

Eleventy works like most static site generators: you run the tool, it searches inside your project for certain folders and files, then processes them to create an output directory with your site’s HTML pages and other resources. You influence this process through configuration, setting various parameters to adjust what Eleventy does. You generally set those parameters through JavaScript files, although the emphasis in those files is on defining parameterized objects or enabling plugins.

In Web Origami you focus on defining the site you want with code. You do this in standard JavaScript or the smaller Origami dialect of JavaScript, which is essentially JavaScript expressions with embedded file paths. The code does whatever you tell it to do. In this case, it defines a blog site’s tree of resources, transforming the markdown posts into browsable HTML and a feed.

This difference between configuration and coding is similar to the difference between working with numbers in Intuit QuickBooks and Microsoft Excel. The former is configured; the latter lets you calculate whatever you want.

Configuration is generally sold as simpler than coding, and most people intuitively feel that should be the case. But I believe that, for making sites, coding is superior in four specific ways:

  1. Code is easier to follow than configuration.
  2. Code is more coherent than configuration.
  3. Code is more expressive than configuration.
  4. Code is more concise than configuration.

Configuration can certainly let you achieve impressive results in complicated domains that you probably couldn’t code yourself, but sites just aren’t that complicated. It’s actually easier to code your own site from scratch than to create one by configuring a tool.

Experiment setup

I copied Eleventy’s recommended starting point for new blogs, the eleventy-base-blog template project, studied that until I felt I understood its construction, then ported it to Web Origami. This gave me two versions of the same blog:

Both demos are about as close as I can easily make them. For a cleaner comparison, I made a few modifications to the original Eleventy project:

  1. The original project had an introductory message with instructions to remove it, so I removed it.
  2. The original used a plugin for image optimization, but reproducing the effects of that would complicate this analysis, so I removed it.
  3. I removed the original’s XSLT stylesheet for the blog feed, as XSLT is being deprecated by Chrome; WebKit and Gecko will likely follow. (I’m not saying the deprecation is warranted, but given the state of things I felt the stylesheet was a distraction.)

Beyond that I tried to port all observable behavior; some minor differences remain. For example, the Eleventy project uses PrismJS for syntax highlighting while the Origami solution uses the slightly different HighlightJS. With more work, the sites could be made even more identical, but I don’t think that would change the overall results of this experiment.

Code is easier to follow than configuration

With two versions of the same project in hand, let’s start evaluating them by considering which version is easier to follow. If you’re coming fresh to the project, can you answer the question: How does it work?

That can be a hard question, so let’s start with a simpler one: What is calling what?

For template projects like eleventy-base-blog, the README typically instructs you to build the site with a command like npm run build. That’s the main entry point to the build process. I tried to search forward from there and follow links to related files.

I got stuck.

I constructed a partial map of what calls what:

The files floating in space aren’t directly referenced by any other files. Some of the file names suggest what roles those files play, but it was still mysterious to me how they actually played those roles.

I eventually found an Eleventy documentation page called Order of Operations providing an “advanced” description of most (but not all) of what was going on. I then had Claude Code guess/explain how the remaining files worked. This clarified that, e.g., Eleventy lets you register JavaScript functions as “filters” you can call from templates. I hadn’t been able to work out for myself that many of the .njk files were invoking code in filters.js.

I was then able to flesh out the above dependency diagram, adding what I understand to be the implicit connections as dashed lines:

Many of the connections in this project are dashed lines representing “action at a distance” — if you don’t already know how the system works, such connections are hard to discover or intuit. This may be acceptable for something you will use all the time, but it certainly does make learning the system (or coming back to it) more difficult.

Let’s now try to answer the “What is calling what?” question for the Web Origami blog, again starting from the build command:

ori copy src/site.ori, clear files:build

Even if the meaning of that command is unclear, you can still see an explicit reference to the file site.ori. If you open that file, you’ll see it contains references to all the files it calls.

You can repeat that process, following links from one file to another, to recover the entire graph of source file calls:

The Origami project has no hidden associations, so all the lines are solid. Everything happens because an explicit line of code makes it happen.

This property of an Origami project makes it much easier to follow what the project does. When I read someone else’s Origami project, it doesn’t matter how they’ve written it. I can always start at the build command and work forward to find all the code. The project’s author also benefits from this same guarantee when they read their own project after some time away from it.

Other posts in this series:

  1. Code is easier to follow than configuration [this post]
  2. Code is more coherent than configuration
  3. Code is more expressive than configuration [coming]
  4. Code is more concise than configuration [coming]

Who else would use a shared Electron library to create and deploy Netlify sites?

I’m interested in helping to create a shared JavaScript library for letting Electron apps authenticate with Netlify (and potentially GitHub/GitLab pages) via OAuth for the purpose of creating new projects and uploading files to existing projects.

The users in the web publishing ecosystem are benefiting from Electron becoming a de facto standard for user-facing tools. As Niki relates, Electron is winning because “native has nothing to offer”. Many developers are voting for Electron these days; I’m one of them.

One task I want my Electron app to perform for end users is helping them select or create a site on a static site host and later deploy locally-built files to that site. Netlify is an attractive target because it supports OAuth; GitHub Pages and GitLab Pages are others.

As it stands today, many tools that want to perform this task on the user’s behalf msut guide the user through creating an account with a host, obtaining a developer credential such as an access token, then copying the token and various other details into the tool. This is complex enough for a developer — and ridiculously complex for a non-developer. I think Publii’s walkthrough of this process is as clear as possible and it’s probably still daunting to many people that might want to create a site.

Netlify offers developers the possibility of an OAuth-based UI flow, but that’s a non-trivial thing to create from scratch:

Even with the help of AI, creating and maintaining this would be some work. But much of this work would be generic — so it could be implemented in a library shared by multiple tools.

A conceptual, back-of-the-envelope API sketch for a hypothetical NetlifyPublish library:

// Select the site or create a new one
const siteDetails = await NetlifyPublish.selectSite({ name: "My blog" });

if (siteDetails) {
  // Successful, deploy build to site
  const files = await doTheBuild(); // However the tool wants to do that
  const success = await NetlifyPublish.deploySite(siteDetails, files);
}

There are many details to hammer out — how is work split across the main Electron process and the renderer? To what extent can the server component be generalized and shared as a community service? On the renderer side, how is the UI made modal: a <dialog> or a separate BrowserWindow? How are the built files represented? How is the UI themed? etc.

If you work on a tool that would might benefit from such a shared library, or are interested in participating in its design and/or implementation, please get in touch!

Promoting a design and development tool through comics

To increase awareness of cool features in Web Origami, I kicked off a weekly comic series with a Mastodon post:

4 comic panels

Each 4-panel comic will deliver a short, standalone story. I thought a 4-panel comic would be a perfect format for Twitter-like sites that allow 4 images per post. The images should add visual interest to a user’s feed, and the user can read the comic right there. Each comic will also be available as regular HTML on the Origami site.

It takes a couple of hours to rough out a little story, come up with code examples, ensure they work, and revise as necessary. I write the comic script in YAML, indicating who is talking, what they’re saying, and what should appear in the panel.

The rest of the process is automated:

  1. It’s easy to compile that YAML script to HTML using Origami itself.
  2. Origami can easily runs the code samples and inlines the results directly into the comic so that the terminal session and browser panels are 100% accurate.
  3. To confirm code continues to work as Origami evolves, I use Origami’s Dev.changes builtin to test the site and flag any changes in code output.
  4. I capture HTML comic panels as PNG images using Origami’s screenshot extension.
  5. I use Origami and JavaScript to upload the images to Mastodon and make a post using additional information from the YAML script.

2025 Web Origami year-end report

Goals for 2025

My top-line goals for 2025 were:

😐 Nurture a small, healthy, respectful user community. I’m happy that the small circle of existing Origami users seem happy with the system and continue using it. A number of new people tried Origami as well, although most of them fell into the “lurker” category; I have no idea whether they are still using it or, if not, why it didn’t meet their needs. So pragmatically speaking, I didn’t do as well on this goal as I’d hoped. One new Origami site, the documentation for the open-source WESL project, was interesting to me because it uses a GitHub wiki as the content management system.

😃 Make it easier for users to make sites they’re happy with through docs, bug fixes, and features. Most of the work I did this year was driven by user feedback. Some existing users moved additional projects of theirs from other systems to Origami, a good indication of user satisfaction.

The big feature investments I aimed to make in 2025 were:

😃 Language Server Protocol implementation. I wanted to implement an LSP server so that development tools like Microsoft VS Code (and others) could provide code completion and inline error feedback. An LSP is a complex beast, and tackling this goal was a significant undertaking. The LSP is now part of the Origami VS Code extension and published in the VS Code marketplace.

😃 Screencast engine. I wanted a way of introducing new users to Origami through a media somewhere between a video and traditional documentation. A video is insanely time-consuming to produce and can’t be kept up to date; traditional docs are easier to maintain but less interesting to newcomers. I developed a system for creating comics and published an initial introductory comic. I’ve always loved comics and am delighted with how this turned out. I have more plans for the comics system in 2026.

😐 Feature complete for JavaScript expressions. I mostly accomplished what I wanted, which was feature parity with JavaScript expressions, but during the year I moved the goal posts. I ended up investing far more in this area than I’d expected; see below. There are still a few remaining unsupported JavaScript features in Origami that I’m looking forward to implementing.

☹️ Inspector/debugger. My plan was to create a browser-hosted tool that lets a user interactively inspect the call tree of an Origami file so they can more easily diagnose errors and better understand how their code works. I spent a month on this, but it proved to be a bigger job than I’d expected and I ultimately set it aside to pursue other priorities. I plan on trying this again in 2026.

JavaScript with paths

The Origami language has evolved in stages, growing closer and closer to JavaScript, but at the beginning of 2025 there were still distinct differences.

Chief among them was that Origami required the / slash operator as a way of extracting a property from an object. Instead of writing post.title like in JavaScript, you had to write post/title. Origami used a slash to preserve the use of the . period as a valid character in file names like data.json.

Feedback from Origami users indicated that this difference in syntax presented a persistent stumbling block, and I eventually developed a file name heuristic that lets the Origami parser determine whether a . period represents part of a file name or a property access. The parser can handle expressions like data.yaml[0].name and generally do what users expect.

This change was a small but important step forward towards turning Origami into a dialect of JavaScript: “JavaScript with paths”. To that end, other important changes included:

Map-based trees

Another significant foundational change this year was in the way Origami represents hierarchical tree-like structures. The higher levels of the Web Origami project rest on a foundational notion of a tree of nodes that may require asynchronous calls to traverse. For the past few years, these trees have been defined by a small proprietary interface, essentially a stripped-down version of the standard JavaScript Map class. Origami didn’t use the Map class itself because it was some quirks.

However, experiments suggested ways to work around those quirks, so I rewrote the foundations of Origami to use Map as a general interface.

Now most of the tree structures you work with in Origami, including those based on in-memory objects, the file system, and local data like JSON/YAML files, are represented with standard Map instances. Because Map doesn’t support async calls, Origami uses an async variation called AsyncMap to represent network resources.

These architectural changes also necessitated extensive revision of Origami’s documentation, including a completely rewritten pattern walkthrough discussing the use of Map-based trees to represent a wide variety of data sources and operations.

Other additions

Along the way there were many other additions to Origami:

Fixing the under-appreciated JavaScript Map class and using it to construct a build system

JavaScript has a Map class for holding key/value pairs, but it’s underused and underappreciated. If you fix the class’ limitations, you can use Map as a building block to create really interesting things.

A pattern of yellow blocks of color with black edges Photo: Cun Mo, Unsplash

A quick review of Map

The Map class lets you associate keys with values:

const m = new Map();

m.add("a", 1);
m.add("b", 2);

m.get("a"); // 1
m.get("b"); // 2
m.get("c"); // undefined

m.keys(); // "a", "b"
m.values(); // 1, 2

The Map class has a number of advantages over plain objects for storing data, but JavaScript syntax makes it easier to create and work with objects so Map doesn’t get used as often as it should.

Extending Map

Map also has one key advantage over a plain object: Map is a class you can extend, so you can expose any key/value data store as a Map.

For example, we can write a FileMap class that makes the contents of a file system folder available as a Map. This ignores the map’s built-in storage, and instead gets the keys and values from the file system:

import * as fs from "node:fs";
import path from "node:path";

export default class FileMap extends Map {
  constructor(dirname) {
    super();
    this.dirname = path.resolve(process.cwd(), dirname);
  }

  get(key) {
    const filePath = path.resolve(this.dirname, key);
    let stats;
    try {
      stats = fs.statSync(filePath);
    } catch (error) {
      if (error.code === "ENOENT") {
        return undefined; // File not found
      }
      throw error;
    }

    return stats.isDirectory()
      ? new this.constructor(filePath) // Return subdirectory as a map
      : fs.readFileSync(filePath); // Return file contents
  }

  *keys() {
    try {
      yield* fs.readdirSync(this.dirname);
    } catch (error) {
      if (error.code === "ENOENT") {
        // Directory doesn't exist yet; treat as empty
      } else {
        throw error;
      }
    }
  }
}

This is pretty amazing! You can easily work with files in code.

const markdown = new FileMap("./markdown");

markdown.keys(); // ["post1.md", "post2.md", …]
markdown.get("post1.md"); // "This is **post 1**."

Most of the time you work with files in code, you’re just reading a list of files and getting their contents, so something like this is much easier to work with than the full file system API.

Better yet, you can immediately pass this kind of file system Map to any code that understands a Map. That provides a desired separation between data storage and code that works on data.

Limitations to work around

Sadly, the Map class is cumbersome to extend: its standard methods like clear(), entries(), and values() will ignore your get() and keys() methods.

markdown.entries(); // empty array ☹️

For comparison, the Python language provides a Mapping abstract base class that is much more helpful than JavaScript’s Map. When you inherit from Mapping, you only need to define a small set of core methods, and the base class uses your definitions to provide the remaining methods.

To compensate for this and other issues with Map, we can create our own base class that inherits from Map but provides all the expected methods:

export default class BetterMapBase extends Map {
  // Override entries() method to call overridden get() and keys()
  *entries() {
    for (const key of this.keys()) {
      const value = this.get(key);
      yield [key, value];
    }
  }
  …
}

If we derive our FileMap class from this improved base class, the full set of Map methods work as expected:

markdown.entries(); // [["post1.md", <contents>], …] 🤩

Transforming a map

Once your data is in Map form, you can manipulate it in interesting ways without having to worry about how the original data is defined. For example, you can transform a map of markdown to a map of HTML.

const html = new HtmlMap(markdown);

html.keys(); // ["post1.html", "post2.html", …]
html.get("post1.html"); // "<p>This is <strong>post 1</strong>.</p>\n"

Transforming the values of data often implies a transformation of the keys, so maps are ideal representations of such operations.

Creating a build system with maps

This Map approach is a pattern you can explore in more detail. Because it’s a pattern, you can use it without taking on any new dependencies.

But you can take advantage of the Web Origami project’s async-tree library, which includes:

You can use this library to construct things like a build system for generating the static files for a site. A sample blog project uses the library to represent the various stages of the build process as Map-based trees:

1. Content tree → 2. Site tree → 3. Build tree
  1. The source content in the file system is represented as a tree of Map objects.
  2. These are transformed with map-based operations and composed into the desired tree of site resources.
  3. The site tree is copied directly into a Map-based representation of the folder that will hold the build output.

The entire build.js build process is very concise and boils down to a copy operation:

import { FileMap, Tree } from "@weborigami/async-tree";
import site from "./site.js";

// Build process writes the site resources to the build folder
const buildTree = new FileMap(new URL("../build", import.meta.url).pathname);
await Tree.clear(buildTree); // Erase any existing files
await Tree.assign(buildTree, site); // Copy site to build folder

By using Map as the fundamental building block, this sample project generates the site’s static files using only two dependencies, the async-tree library and a markdown processor.

The AsyncMap variant lets you represent a network data source as an object that has the same methods as a Map but the methods are asynchronous. This lets you pull content from sources like Dropbox or Google Drive using a much simpler API, so you can incorporate content directly from the network into your build process.

Creating a simple blog in Python with Origami concepts

I’ve extended my series of blog architecture comparison posts from earlier this year by porting the sample reference blog from Origami and plain JavaScript to create a Python version (source, demo).

The occasion was reading about improvements in async features in today’s release of Python 3.14, which prompted me to write some Python code for the first time in a long while.

The sample blog doesn’t actually need to make async network requests, so it ended up only using sync functions — but all those sync functions are still lazy and do work only when necessary. And with the improvements in Python 3.14, it might be possible to create an AsyncMapping abstract base class as a variation of Mapping to handle the sorts of async network operations that Origami can do, e.g., reading post content directly out of Dropbox or Google Drive.

Creating a static site

Python’s audience has always included people who don’t think of themselves primarily as programmers. That aligns with what I’m trying to do in Origami, so it’s interesting to explore using Python as a possible substrate for Origami ideas.

I’m not familiar with Python static site generators, but they seem to generally take the same framework approach as their Node.js counterparts: impose a particular folder structure, provide a magic transformation of that structure to static files, and offer a degree of customization through configuration.

Like all the blog implementations in this series, this Python project rejects that approach entirely. Instead, the focus is on creating useful functions and abstractions for defining the site you want to create. It leaves you to put the parts together in a way that makes sense to you. You are always in control and can entirely satisfy your requirements.

Lazy maps

Like the JavaScript versions of the sample blog, the Python version attempts to use native language constructions whenever possible. It makes heavy use of Python’s Mapping abstract base class to represent collections which are fundamentally lazy: they do essentially no work when constructed. Only when asked for their keys, or for a particular value for a key, will they do substantive work.

An example of this is the project’s Folder class, which wraps a file system folder as a lazy Mapping (specifically, a MutableMapping, which can be updated after it’s created).

Another example of this are the project’s operations that take one Mapping as input and return a new, transformed Mapping. For example, map_extensions, can convert a virtual collection of .md files into a corresponding collection of .html files.

post_html_docs = map_extensions(folder, ".md->.html", md_doc_to_html)

When backed by a Folder containing post.md, the resulting map-of-a-map says that it contains a post.html – but it hasn’t done the real work for that yet. When you ask for post.html, it will ask the underlying Folder for post.md, translate the markdown content to HTML, then return that result. (The actual data pipeline is slightly more complex; see the ReadMe.)

You can use a debugger to inspect the value of a map like this at runtime with a command like:

list(post_html_docs.items())

This lets you confirm that the map’s keys and values are what you expect.

The beauty of working at this abstract Mapping level is that your code doesn’t need to care how a particular collection is defined – your code can handle real files or generated-on-demand files in exactly the same way.

Defining a site tree with maps

At the project’s highest level, site_tree.py defines the root of the site’s tree of resources as a Mapping. Most of those top-level parts of the sites are also Mapping instances: some like assets and images are real folders; others are virtual collections like posts.

# from src/blog_demo/site_tree.py

# The site tree is a tree with Mappings for interior nodes and the desired
# resources as leaves. The top level of the tree can invoke functions to
# generate the content on demand. Areas which are just Mappings are inherently
# lazy and are defined directly.
site_tree = invoke_fns({
    "about.html": lambda: about_html(),
    "assets": Folder(here / "assets"),
    "feed.json": lambda: json.dumps(feed(), indent=2),
    "feed.xml": lambda: json_feed_to_rss(feed()),
    "images": Folder(here / ".." / ".." / "images"),
    "index.html": lambda: pages_area()["1.html"],  # same as pages/1.html
    "pages": lambda: pages_area(),
    "posts": posts_area(),
})

This programmatic approach lets you dictate precisely how you want to compose the parts of your site; you’re not trapped within the confines of a framework and someone else’s expectations.

Building and serving

As in Origami, the site’s tree of resources is used in two very different ways.

First, a small web server that accepts any Mapping-based tree converts URL requests like /assets/styles.css into an array of keys ["assets", "styles.css"] that are used to traverse the tree. Given the site_tree.py code above, the first ”assets” key retrieves the Folder wrapper for the real assets folder; the second key retrieves the styles.css file in that folder. Because the site tries to be as lazy as possible, the site only does the work necessary to fulfill the specific request.

Second, building the static site files is simply a matter of copying the site’s virtual tree of resources into a real tree in the file system.

# from src/blog_demo/__main__.py

def build(m: Mapping):
    """
    Given a mapping representing the site structure, copy the entire tree into
    the build folder to create static files.
    """
    build_folder = Folder("build")
    build_folder.clear()
    build_folder.update(m)

A MutableMapping is created for the build output folder; it’s mutable so that files can be written into it.

The site’s tree of resources is copied directly into it using the completely standard update() method to copy one map into another. That boils down the essence of a static site generator to the single line:

build_folder.update(m)

This walks through the source tree m, calling __getitem__ to get each resource. That will trigger the generation of said resource. The result will be passed to the build’ folder’s __setitem__ method to create the corresponding output file.

Assessment

Let’s compare this Python blog port with the earlier Origami and JavaScript versions.

In terms of source code size, the Python version is written as a demo application and a separate library for map manipulations. That makes it roughly comparable to the blog using Origami’s async-tree library. Accordingly, I’ll factor out the library portion of the code, and the reusable JSON feed-to-RSS translation to measure the non-reusable bytes in the demo application, including both Python (.py) files and Jinja (.j2) templates.

The Python version comes in at 10021 bytes, just a bit more than the 9450 bytes for the async-tree version. For this blog application, at least, both Python and JavaScript are comparably expressive. (Origami is still more concise.)

Python and Node.js are completely different environments, so it’s not possible to make an apples-to-apples comparison of the weight of the project dependencies. And in any case, measuring dependencies by total file size can only give an extremely coarse approximation of potential complexity. Still, I thought it was interesting to measure the total size on disk of the Python project’s site-packages as an analogue for node_modules.

The Python version weighs more than the async-tree version. It’s less than Origami, but Origami is also doing a lot more. (Astro is still the most complex answer to the problem, and I don’t believe it’s actually doing enough interesting work to justify its size.)

Finally, let’s look at the time required to build the blog’s static files:

Python comes in at 0.24s — a hair faster than the zero-dependency JavaScript version, making it the fastest of all the blog versions I’ve created so far.

Conclusion

This was a really interesting experiment! It was a ton of fun to write Python code again.

It seems completely feasible to serve and build a site in Python in a lightweight fashion using a library (under your control) instead of a big framework (that takes control from you). Python seems like a great substrate for Origami ideas. It’s well-designed and widely-used Mapping abstract base class is a natural way to represent your source content as a tree that you can transform into the tree of resources you want for your site.

Making Origami expressions easier to learn and use

Over this summer I’ve shifted the Origami expression language to be a closer dialect of JavaScript expressions. Feedback from early adopters indicated that some differences from JavaScript created trouble; eliminating those would improve the language.

Origami is now essentially JavaScript expressions plus paths, with a few additional syntactic features that make it easier to define a site.

The language supports a conceptual model called Content/Transformation you may find helpful in thinking about how to build a site.

The syntax changes to the language included:

Origami is still missing some esoteric JavaScript expression features (e.g., function parameter destructuring), but those are all on the list tackle. The goal is that you should be able to copy any JavaScript expression and evaluate it as an Origami expression.

Why introduce an alternative expression language to JavaScript?

Watch the motion comic introducing the language, or walk through how to create a basic blog.

Write a very concise static site generator with Origami expressions

Last year I created a sample blog called #pondlife to show how to create a basic blog in Web Origami. Recently I rewrote that same blog three new ways, giving me four implementations to compare:

  1. Minimalist JavaScript with zero dependencies: blog post, source, site
  2. JavaScript using the async-tree library: blog post, source, site
  3. Origami original blog post, source, site
  4. Astro, a popular static site generator framework: blog post, source, site

All four approaches produce the same static blog site — but there are significant differences in how much code each approach entails and how many dependencies they require.

JavaScript isn’t a great fit for making sites

The Origami version is functionally the same as the JavaScript version using the async-tree library. The Origami is able to be more concise by using a dialect of JavaScript expressions. These cut out a fair bit of JavaScript boilerplate so the final source code can focus on the blog I’m trying to create.

The JavaScript language was originally designed for wiring up interactivity on individual pages. It was not designed to create sites, to interact with file system trees or network resources, or to parse and manipulate the contents of files.

Node.js and similar environments do make those things possible in JavaScript — which is great! But the fact remains that JavaScript itself is a poor fit for making sites. Even basic site tasks require you to write quite a bit of JavaScript code.

Example: Generating the About page

As an example, let’s look at the aboutPage.js code in the async-tree version of the blog that reads in a trivial markdown file and transforms it into the simple About page:

import { marked } from "marked";
import fs from "node:fs/promises";
import markdownDocument from "../markdownDocument.js";
import page from "./page.js";

// About page: transforms about.md to HTML and applies the page template
export default async () => {
  const buffer = await fs.readFile(
    new URL("../about.md", import.meta.url).pathname
  );
  const document = await markdownDocument(buffer);
  return page({
    ...document,
    // Transform the body to HTML
    body: marked.parse(document.body),
  });
};

This gets a reference to the markdown file relative to the JavaScript module, reads in the file buffer, calls a helper to parse that as markdown with front matter, transforms the resulting object to one whose body property is HTML, then passes that to our site’s basic page template to produce the final HTML page.

The above code works but feels verbose for what it’s doing.

Origami is JavaScript expressions plus paths

Many tasks in creating a site or other digital artifact can be handled by an expression — a bit of code that returns a value.

These are all types of expressions.

The Origami language is essentially JavaScript expressions with paths. This is paired with a library of built-in functions to handle tasks that commonly come up when creating sites and other software artifacts. Finally, a command-line interface (CLI) can display the result of an expression or, if the expression produces a complex tree structure, write that result out as a tree of files in the file system.

Expressions are concise

Origami expressions cut down the above code required for things like creating the About page.

With all that, we can replace the 17 lines of code shown earlier with a single line:

page.ori(mdHtml(about.md))

This is what I think a language that’s good for making web sites should look like. This line says: “Read in about.md, turn into HTML, and pour that into the base page template.”

It’s great that you can do this in JavaScript too, but compared to the above, JavaScript seems like an awkward fit.

Critically, when you write the above line of code, you are still in control of every step of the process. You can add new operations, change the order of operations, whatever you want.

In contrast, frameworks need to make assumptions about what you want to do. If their assumptions match your needs perfectly, you might not even need to write any code. But as my Astro blog post discussed, if your needs vary even a tiny bit from the framework’s defaults, you may end up writing quite a lot of code.

Distilling the essential site definition

We can make a similar reduction of this site.js code that defines the overall structure of the async-tree blog:

import { FileTree, map, paginate } from "@weborigami/async-tree";
import jsonFeedToRss from "@weborigami/json-feed-to-rss";
import jsonFeed from "./jsonFeed.js";
import posts from "./posts.js";
import aboutPage from "./templates/aboutPage.js";
import multiPostPage from "./templates/multiPostPage.js";
import singlePostPage from "./templates/singlePostPage.js";

// Group posts into pages of 10
const pages = map(await paginate(posts, 10), {
  extension: "->.html", // Add `.html` to the numeric keys
  value: multiPostPage, // Apply template to the set of 10 posts
});

// Convert posts to a feed object in JSON Feed schema
const feed = await jsonFeed(posts);

//
// This is the primary representation of the site as an object. Some properties
// are async promises for a single result, others are async trees of promises.
//
export default {
  "about.html": aboutPage(),
  assets: new FileTree(new URL("assets", import.meta.url)),
  images: new FileTree(new URL("../images", import.meta.url)),
  "index.html": pages.get("1.html"), // same as first page in pages area
  "feed.json": JSON.stringify(feed, null, 2),
  "feed.xml": jsonFeedToRss(feed),
  pages,
  posts: map(posts, singlePostPage),
};

By leaning on Origami’s implicit imports, exports, and async/await support and using the language’s built-in functions, we can reduce the above code to this expression:

// Define the blog's overall structure
{
  // About page uses the basic page template
  about.html = page.ori(mdHtml(about.md))

  // Assets (fonts, styles) and images are included as is.
  assets
  images

  // Feeds render the posts in JSON Feed and RSS formats
  (feed) = feed.ori(posts.ori)
  feed.json = json(feed)
  feed.xml = rss(feed)

  index.html = pages/1.html  // same as first page in pages area

  // Group posts into pages of 10
  (paginated) = paginate(posts.ori, 10)
  pages/ = map(paginated, { extension: "→.html", value: multiPostPage.ori })

  // The posts area contains a page for each individual post
  posts/ = map(posts.ori, singlePostPage.ori)
}

The above code is still entirely explicit about declaring the site it produces; Origami is making no assumptions about what you want. The above code is not configuring a blog engine. It’s defining a blog engine from scratch.

I find the Origami version easy enough to read that I can quickly look over a site definition like this — even a definition written by someone else — and quickly apprehend the construction of the entire site.

Assessment

Let’s see how the Origami version compares against the three blog versions discussed earlier.

The Origami source code is distinctly smaller than all other forms of the code, coming in at just 5749 bytes:

Chart comparing source code size across three blog versions, Origami is smallest

The Origami version using Origami v0.2.12 comes with a non-trivial 33Mb of node_modules, although that’s still less than a third of the 117Mb of node_modules for the Astro version:

Chart comparing node modules across three blog versions, zero-dependencies is smallest

The Origami build time is slightly slower than the zero-dependency and async-tree versions that use only JavaScript. This reflects the cost of the Origami parser and runtime. That said, the Origami version is still faster than Astro for this project:

Chart comparing build times across three blog versions, zero-dependencies is fastest

What do you actually need to learn?

Origami is a dialect of JavaScript expressions. If you don’t already know JavaScript, you’ll need to learn some basics to be productive in Origami. If you do know JavaScript, you need to be aware of the set of JavaScript features supported in Origami and some syntax differences.

Learning a new language dialect does represent some real mental work for you. But Astro and most of the other popular SSG frameworks often impose new language dialects too. In Astro’s case, you need to learn a dialect of JSX, which itself is a dialect of JavaScript and a dialect of HTML.

If you don’t want to learn a new language, I think the async-tree version of the blog is quite nice. An experienced JavaScript developer can take advantage of it without needing to learn a new language at all.

Conclusion

All these projects produce the same site and, depending on who you are and what your goals are, any of them might be appropriate for you.

I myself prefer solutions that are more explicit and less magic. It’s generally a good idea to work as close to the platform as possible; both of the JavaScript versions discussed here have their merits. That said, Origami is so concise and flexible that I can be extremely productive in it and it’s fun to use. I generally prefer it for making sites these days.

Read the other posts in this series:

  1. Static site generators like Astro are actually pretty complex for the problems they solve
  2. This minimalist static site generator pattern is only for JavaScript developers who want something small, fast, flexible, and comprehensible
  3. Making a small JavaScript blog static site generator even smaller using the general async-tree library
  4. Write a very concise static site generator with Origami expression [this post]

Making a small JavaScript blog static site generator even smaller using the general async-tree library

Using the async-tree library substantially cuts down the source code for a minimalist static site generator (SSG) in JavaScript, at a very modest cost in dependencies. The result is still fast and flexible.

In the first post in this series, I recreated a simple blog in Astro that felt complicated. I rewrote the blog in plain JavaScript with zero dependencies. This post discusses yet another rewrite, this one predicated on sharing code.

You can look at the final async-tree blog source code and the live site.

Okay, maybe a few dependencies

The zero-dependency version felt quite good, although insisting on no dependencies was a little extreme.

While half the source code was unique to the project, the features in the other half can be cleanly handled by libraries, like:

These are both pure functions, a much easier kind of dependency to take on. You decide when to call the function and what input to give it; it gives you back a result without any side effects. This contract greatly reduces the potential for surprise or frustration.

The async-tree library

The remaining sharable code in the zero-dependency version comprises generic utility functions:

Since these are completely generic, they’re worth sharing — so over the past 4 years I’ve been working on a library called async-tree that handles these and other tasks.

The async-tree library builds on the idea that most of the hierarchical structures we work with can be abstracted to asynchronous trees. When creating a site, we rarely care about how data is stored; we just want to render it into static resources like HTML.

Our collection of markdown documents, for example, is physically stored in the file system — but that’s irrelevant to our static site generator. All we care about are the keys (the file names) and the values (the markdown text with front matter). We can think about this collection of markdown documents as an abstract tree that could be anywhere in memory, on disk, or in the cloud:

Tree diagram showing a root node pointing to three markdown files

If all we want to do is traverse this tree, APIs like Node’s fs API are overkill. We just want a way of getting keys and values. This is much closer in spirit to a JavaScript Map. Unlike Map, we can handle more cases by making our methods async.

This is the AsyncTree interface:

export default interface AsyncTree {
  get(key: any): Promise<any>;
  keys(): Promise<Iterable<any>>;
  parent?: AsyncTree | null;
}

This is an interface (not a class) that’s easy to define for any almost any collection-like data structure. Such async collections can be nested to form an async tree — a tree of promises.

Abstractions come a cost. In exchange for a considerable degree of power and flexibility, you have to wrap your brain around an unfamiliar concept. “A tree of promises?” It might take a while to wrap your head around that.

I will say that, from several years of experience, it’s ultimately very beneficial to view software problems like static site generation as reading, transforming, and writing async trees.

Example: reading markdown, reading posts

As an example, to get the first file from our markdown folder, we can construct an AsyncTree for that folder using the library’s FileTree helper, then call the tree’s get method:

import { FileTree } from "@weborigami/async-tree";
const files = new FileTree(new URL("markdown", import.meta.url));
const first = await files.get("2025-07-04.md");

Here FileTree is roughly similar to our quick-and-dirty zero-dependency code that read a folder tree into memory. But FileTree is more efficient because it doesn’t read the complete set of files into memory; it only does work when you look up a key’s value with get.

Our posts.js function turns that collection of markdown file buffers into a completely different form: a set of plain JavaScript objects with .html names that are stored in memory. Despite these significant differences, if we want to get the first post from that collection, we can still use the same get method:

import posts from "./src/posts.js";
const first = await posts.get("2025-07-04.html");

Totally different data structure, same get method.

Example: pagination

Another reason to work with collections as abstract trees is that a consistent set of operations can be defined for them regardless of their underlying storage representations.

For example, the zero-dependency version includes a one-off paginate helper that accepts a collection of posts and returns an array grouping the posts into sets of 10. The paginated posts can then be mapped to HTML pages using the project’s own mapObject helper function.

// Group posts into pages of 10
const pages = mapObject(paginate(posts, 10), (paginated, index) => [
  `${parseInt(index) + 1}.html`, // Change names to `1.html`, `2.html`, ...
  multiPostPage(paginated), // Apply template to the set of 10 posts
]);

The async-tree library offers the same functionality as a general paginate function which can be applied to a tree defined by any means, including our set of posts. The paginated results can then be turned into HTML with another generic tree operation, map.

// Group posts into pages of 10
const pages = map(await paginate(posts, 10), {
  extension: "->.html", // Add `.html` to the numeric keys
  value: multiPostPage, // Apply template to the set of 10 posts
});

Mapping the values of a collection often implies changing the file extension on the corresponding keys, so the map function includes an extension option to easily add, change, or remove extensions.

Site definition

As with the zero-dependency version, the async-tree version of the blog defines the overall structure of the site in extremely concise fashion in site.js:

// Group posts into pages of 10
const pages = map(await paginate(posts, 10), {
  extension: "->.html", // Add `.html` to the numeric keys
  value: multiPostPage, // Apply template to the set of 10 posts
});

// Convert posts to a feed object in JSON Feed schema
const feed = await jsonFeed(posts);

//
// This is the primary representation of the site as an object. Some properties
// are async promises for a single result, others are async trees of promises.
//
export default {
  "about.html": aboutPage(),
  assets: new FileTree(new URL("assets", import.meta.url)),
  images: new FileTree(new URL("../images", import.meta.url)),
  "index.html": pages.get("1.html"), // same as first page in pages area
  "feed.json": JSON.stringify(feed, null, 2),
  "feed.xml": jsonFeedToRss(feed),
  pages,
  posts: map(posts, singlePostPage),
};

That’s the whole site. This is the most concise way I know to define a site in JavaScript.

I find this kind of concise overview invaluable when I return to a project after a long break, and a quick glance refreshes my understanding of the site’s structure.

Build

Once the site is defined, building the site is just a matter of copying files from the virtual world to the real world. Here’s the whole build.js script:

import { FileTree, Tree } from "@weborigami/async-tree";
import site from "./site.js";

// Build process writes the site resources to the build folder
const buildTree = new FileTree(new URL("../build", import.meta.url).pathname);
await Tree.clear(buildTree); // Erase any existing files
await Tree.assign(buildTree, site); // Copy site to build folder

The async-tree library provides a set of helpers in a static class called Tree. These provide a full set of operations like those in the JavaScript Map class so that AsyncTree interface implementors don’t have to define those methods themselves, making it easier to create new AsyncTree implementations to read data directly out of new data sources.

Assessment

We can compare this async-tree version of the blog with the earlier Astro and zero-dependency versions. All three versions create the same site.

The async-tree version makes strategic use of libraries for markdown processing, RSS feed generation, and manipulating objects and files as trees. This removes over half the code from the zero-dependency version, so async-tree has only 9K handwritten source code, the smallest of the three:

Chart comparing source code size across three blog versions, async-tree is smallest

This comes at a modest cost of 1.5Mb of node_modules, or about 1% of the 117Mb of node_modules for the Astro version:

Chart comparing node modules across three blog versions, zero-dependencies is smallest

The async-tree version is still extremely fast, just a hair slower than the zero-dependency version:

Chart comparing build times across three blog versions, zero-dependencies is fastest

Nice!

Impressions

Like the zero-dependency version, this async-tree version was fun to write.

The introduction of a limited set of dependencies to this project felt fine. The small libraries I’m using here all do their work as pure functions, so I’m still in control over what’s going on. I don’t have to wrestle with plugins, lifecycle methods, or complex configuration like I would have to in a mainstream SSG framework. I’m just calling functions!

Debugging async JavaScript code is harder than debugging regular, synchronous code. The debugger I use in VS Code does a fairly good job of it, but it’s still not possible to inspect the value of variables across async stack frames. That can make it harder to figure out what’s gone wrong at a breakpoint.

That said, I once again made good use of the ori CLI to check various pieces of the site in the command line. That let me confirm that individual pieces worked as expected, as well as serve the site locally to inspect the evolving site.

All in all, I think this async-tree approach is a really interesting way to build sites. It’s significantly less JavaScript than the zero-dependency version, while it’s still very fast and light on package weight. You stay in control.

Since I wrote the async-tree library, I can’t provide an objective assessment of how difficult it is to use.

The library deserves more comprehensive documentation than it currently has; I’ve generally focused my documentation writing on the higher-level Origami language and its set of builtins. If you’re intrigued by this more foundational, general-purpose async-tree library, let me know. I can help you out and prioritize documenting it in more detail.

Improvable?

As small and focused as the source for this async-tree version is, it can be made even smaller! Next time I’ll revisit the original sample blog that started this post series and show the benefits of writing it in Origami.

Read the other posts in this series:

  1. Static site generators like Astro are actually pretty complex for the problems they solve
  2. This minimalist static site generator pattern is only for JavaScript developers who want something small, fast, flexible, and comprehensible
  3. Making a small JavaScript blog static site generator even smaller using the general async-tree library [this post]
  4. Write a very concise static site generator with Origami expression

Older posts