Index pages

Right now, the experience of browsing our tree of generated HTML is a little unsatisfying because there are no index pages — we have to know what pages exist and manually enter a valid URL.

We can fix that by adding another tree transform. Wrapping a tree with this transform will:

  1. add “index.html” as one of the inner tree’s keys if it’s not already defined
  2. define a default value for “index.html” if one isn’t already defined

The default value will be an HTML page listing the tree’s other keys as links.

Generate a single index page

First let’s write a function that returns a reasonable default index page for a tree that doesn’t define one.

/* In src/index/indexPages.js */

// Return a default index page for the given tree.
async function indexPage(tree) {
  // Collect all the keys in the tree.
  const keys = [];
  for (const key of await tree.keys()) {
    // Ignore the key `index.html`
    if (key !== "index.html") {

  // Map the keys to HTML links.
  const links = => {
    const basename = key.replace(/\..+$/, "");
    return `      <li><a href="${key}">${basename}</a></li>`;

  // Incorporate the link HTML into a simple page.
  const html = `<!DOCTYPE html>
  return html;

If the little more branch of our HTML tree looks like this:

g Hello, **David**. -> Hello, **Eve**. ->

Then invoking indexPage on this branch will return:

<!DOCTYPE html>
      <li><a href="David.html">David</a></li>
      <li><a href="Eve.html">Eve</a></li>

Transform a tree by adding index pages

Using the default indexPage function above, let’s now create a tree transform. This will accept any async tree, and return a new tree with an index.html key.

/* In src/index/indexPages.js */

export default function indexPages(tree) {
  return {
    async get(key) {
      let value = await tree.get(key);
      if (value === undefined && key === "index.html") {
        // No index page defined; create one
        return indexPage(tree);
      } else {
        // If we're returning an async subtree, add index pages to it too.
        const isAsyncDictionary =
          typeof value?.get === "function" && typeof value?.keys === "function";
        return isAsyncDictionary ? indexPages(value) : value;

    async keys() {
      // Yield the keys of the tree and remember what they are.
      const keys = new Set(await tree.keys());
      // Add index.html to the set of keys.
      return keys;

If we use this to transform the more branch of the HTML tree, the transformed tree now includes an index.html page:

g David.html <p>Hello, <strong>David</strong>.</p> ->David.html David.html Eve.html <p>Hello, <strong>Eve</strong>.</p> ->Eve.html Eve.html index.html <!DOCTYPE html> <html> <body> <ul>… ->index.html index.html

Incorporate the index page transform

We can apply this indexPages transform on top of our object, file, and function-based HTML trees. For example, the file-based tree now looks like:

/* src/index/htmlFiles.js */

import tree from "./files.js";
import indexPages from "./indexPages.js";
import transform from "./transform.js";

export default indexPages(transform(tree));

These transforms are just functions, so we can apply as many tree transforms as we want.

In this case, the order of function calls matters: when the indexPages transform is iterating through the keys of a tree, we want it to see keys that end in .html so that it create links to the HTML pages. If we applied the indexPages transform first, it would create links to the .md files.

From inside the src/index directory, serve the HTML tree again.

$ cd../index
$ node serve
Server running at http://localhost:5000. Press Ctrl+C to stop.

Browse the site to see index pages at each level of the hierarchy.

This server can be good enough for development, and we could easily deploy it in this state. But since this HTML content is currently all static, it would be nice to render the HTML content as .html files we can deploy directly.

Before moving on, in the terminal window, stop the server by pressing Ctrl+C.


Next: Copy a tree »