Displaying a tree in the console is fine for playing around or debugging, but we can do much more interesting things with a tree — like serve it to a web browser.
Let’s build a small tree server directly on top of Node’s http API. (If we were already using a server like Express, it would be straightforward to adapt this same idea into a server middleware function that handles a specific portion of a larger site.)
Using the Map interface to model the nodes of the site tree lets us browse content regardless of how that content is stored or generated.
Treat a URL as a series of tree keys #
The first thing is to recognize a URL as a tree traversal — we can treat a URL path as a series of keys to follow through a tree.
Specifically, we convert a string URL path like /foo/bar into an array of keys ["foo", "bar"].
/* In src/site/serve.js */
// Convert a path-separated URL into an array of keys.
function keysFromUrl(url) {
const keys = url.split("/");
if (keys[0] === "") {
// The path begins with a slash; drop that part.
keys.shift();
}
if (keys[keys.length - 1] === "") {
// The path ends with a slash; replace that with index.html as the default key.
keys[keys.length - 1] = "index.html";
}
return keys;
}
If the path ends in a slash like foo/, this produces the keys ["foo", "index.html"].
Traverse a tree #
We can then iteratively follow this array of keys through a deep tree of Map nodes to a final value:
/* In src/site/serve.js */
// Traverse a path of keys through a map.
function traverse(map, ...keys) {
let current = map;
for (const key of keys) {
current = current.get(key);
if (current === undefined) {
// Can't go any further
return undefined;
}
}
return current;
}
The tree itself is acting as a web site router.
Handle requests using a tree #
Putting these together, we can build a listener function that uses a tree to respond to HTTP requests.
/* In src/site/serve.js */
// Given a tree, return a listener function that serves the tree.
function requestListener(map) {
return function (request, response) {
console.log(request.url);
const keys = keysFromUrl(request.url);
let resource;
try {
resource = traverse(map, ...keys);
} catch (error) {
console.log(error.message);
}
if (resource) {
// Send to client
response.writeHead(200, { "Content-Type": "text/html" });
response.end(resource);
return true;
} else {
// Not found
response.writeHead(404, { "Content-Type": "text/html" });
response.end(`Not found`, "utf-8");
return false;
}
};
}
This converts a request’s URL into an array of keys, then returns the resource it finds there. If no value is found, the listener responds with 404 Not Found.
Serve the tree #
Finally, we start the server at a default port, serving the tree defined in site.js.
/* src/site/serve.js */
import http from "node:http";
import site from "./site.js";
const port = 5000;
…
// Start the server.
const server = http.createServer(requestListener(site));
server.listen(port, undefined, () => {
console.log(
`Server running at http://localhost:${port}. Press Ctrl+C to stop.`
);
});
Trying our server #
From inside the src/site directory, start the server:
$ node serve
Server running at http://localhost:5000. Press Ctrl+C to stop.
Browse to that local server. The simple blog index page should appear.
Click a post to view it.
Maps can be lazy #
Because we’ve designed all our Map classes to be lazy, when you start the server, no real work is done beyond starting the HTTP listener.
The tree only generates the HTML when you ask for it by browsing to a page like posts/post1.html:
- The server asks the top-level map for
posts. That returns anHtmlMap. - The server asks the
HtmlMapforpost1.html. - The
HtmlMapasks the inner markdown map forpost1.md. - The inner markdown map returns the value for
post1.md. If we’re using aFileMapto represent the markdown, this gets the content ofpost1.mdfrom the file system. - The
HtmlMapconverts the markdown content to HTML and returns that. - The server gets the HTML as the final resource and sends it to the client.
Because all the maps involved are lazy, navigating to posts/post1.html doesn’t do any of the work required to generate the other pages post2.html or post3.html.
Flexible #
This server is already pretty interesting!
We’ve got a simple site but can flexibly change the representation of the data. Having done relatively little work, we can let our team write content in markdown. Unlike many markdown-to-HTML solutions, the translation is happening at runtime, so an author can immediately view the result of markdown changes by refreshing the corresponding page.
Each of our underlying object, file, or function-based trees has its advantages. For example, we can serve our function-based tree to browse HTML pages which are generated on demand.
Edit src/site/site.js so that the markdown comes from the function-based map from fn.js instead of files.js.
Browse to a page like posts/post4.html. Corresponding markdown will be generated on demand by the FunctionMap and converted to HTML:
This is post 4.
Before moving on, in the terminal window, stop the server by pressing Ctrl+C.
Next: Build by copying »