A key benefit of building a site with the Map Tree pattern is that we can seamlessly move between viewing that content in the console or the browser.
We can also easily render that tree as regular “static” files (i.e., files that don’t change often). While we could deploy our server as is, deploying the site as static files is much cheaper, faster, and more reliable.
Build pipeline #
Systems for generating static sites have a “build” process that creates a folder with all the site’s files. In many of these systems, the build system and the server work very differently.
But for map-based trees, the build pipeline is conceptually very simple:
- We define a tree of the source markdown content — as an object, as real files, or as values generated by a function.
- We use one or more transforms to create a virtual tree of the final content that we want — here, HTML. We can directly serve and browse this virtual tree during development.
- We want to copy the virtual tree of HTML pages into some persistent form such as real
.htmlfiles. We can then deploy these files.
The transformation from tree 1 to tree 2 is handled by our site.js file — but how should we go from tree 2 to tree 3?
The key insight is that the set of static files we want to end up with is exactly the same as the virtual tree 2. We just need to copy that tree to produce tree 3!
Copying a map-based tree #
We can write a copy function that copies one Map-based tree into another:
/* src/site/copy.js */
// Copy the source map into the target map
export default function copy(source, target) {
for (const [key, sourceValue] of source.entries()) {
if (sourceValue instanceof Map) {
// Subtree; recurse to copy
let targetValue = target.get(key);
if (targetValue === undefined) {
// Target key doesn't exist; create empty subtree
target.set(key, {});
// Retrieve the newly created subtree
targetValue = target.get(key);
}
copy(sourceValue, targetValue);
} else {
// Copy the value from the source to the target.
target.set(key, sourceValue);
}
}
return target;
}
The source tree can be read-only or read/write; the target map has to be read/write.
Build real files from virtual content #
We’re now ready to build real static files for our site by copying the virtual tree of HTML pages into a real file system folder. All we need to do is wrap a real build folder in a read/write FileMap to indicate the destination for the static files:
/* src/site/buildFiles.js */
import { fileURLToPath } from "node:url";
import FileMap from "./FileMap.js";
const dirname = fileURLToPath(new URL("build", import.meta.url));
export default new FileMap(dirname);
We can create a build.js script that copies the virtual tree defined in site.js into that real build folder:
/* src/site/build.js */
import buildFiles from "./buildFiles.js";
import copy from "./copy.js";
import site from "./site.js";
buildFiles.clear();
copy(site, buildFiles);
This first clears the contents of the build folder, then copies everything in the site tree into that folder. As the copy operation traverses the site tree, it triggers the work required to construct each resource. Each resource is saved into the build folder as a static file.
Building #
Use the build script to copy the virtual site into a new build folder:
$ ls build
ls: build: No such file or directory
$ node build
$ ls build
index.html posts
Inspect the individual files in the build folder to confirm their contents. You can also use our json utility to dump the entire build to the console:
$ node json buildFiles.js
{}
We can confirm that this is the same JSON we saw before when dumping site.js to the console.
Browse the built HTML files #
You could now deploy the HTML files in the build folder anywhere.
As a quick test, serve the build folder with any static server, such as http-server.
$ npx http-server build
Starting up http-server, serving build
(You could also temporarily hack serve.js to serve the tree defined by builtFiles.js instead of siteTree.js. Everything here’s a map-based tree, and you can serve any of those trees the same way.)
Browse to the static server and confirm that the static results are the same as what you can see running the dynamically-generated tree.
The results will look identical, but a key difference is that no real work is necessary to display the HTML files served from the build folder.
Before moving on, in the terminal window, stop the server by pressing Ctrl+C.
In this walkthrough, the markdown-to-HTML translation happens almost instantly, but in real projects, the data or transformations could easily take some time. Viewing an individual page might require non-trivial work, resulting in a perceptible delay before the page appears. Building the pages into static files performs all the work at once, so your users can browse the resulting static files as fast as the web can deliver them.
We’ve now solved our original problem: we’ve created a system in which our team can write content for our web site using markdown, and end up with HTML pages we can deploy.
A general approach for building things #
Here we’re using real markdown files to create virtual HTML files and then save those as real HTML files. But this type of build pipeline doesn’t really have anything to do with the web specifically — HTML pages are just a convenient and common example of content that can be created this way.
You could apply this same Map Tree pattern in build pipelines for many other kinds of artifacts: data sets, PDF documents, application binaries, etc. The pattern can benefit any situation in which you are transforming trees of values.
- In some cases, the source information will be an obvious tree. In others, you might start with a single block of content (a document, say) and parse that to construct a virtual tree.
- You can then apply multiple transforms to that source tree to create additional virtual trees, each one step closer to your desired result.
- Finally, you can save the last virtual tree in some persistent form. That might be a hierarchical set of files as in the example above, or you might reduce the tree in some fashion to a single result, perhaps a single file.
In some cases, the source information will reside on a server. You could copy that information locally — but you could also read the data from its server location. To account for the slower speed of reading from a network, you can extend the map paradigm to asynchronous data.
Next: Asynchronous maps »