(λ (x) (create x) '(knowledge))

Over-Engineering a Blog: Part2

A technical overview of how LambdaCreate works · April 8, 2020

In the first post I eluded to the fact that LambdaCreate is written in Lua, and then I completely ignored that to talk about Docker. Maybe I just really like Docker, maybe when I wrote the initial post my CSS rendering caused scroll creep and I had to spend meticulous hours debugging overflow: auto; and if I wrote about everything all at once the blog post would have been horrendous. In truth, it's all of that. A huge shout out to my friend Kaleb Sego for helping with the CSS issues, you made this next post possible!

Lapis: A Lua web Framework

The Lua side of things is actually pretty interesting. The web framework I chose was Lapis. It's a great little Lua/Moonscript web frame that runs on top of Nginx/Openresty, which allows for Lua code to be executed directly by the web server. This results in wicked fast, and dynamic page servicing and generation! The author, Leafo, is incredibly kind and active. He runs a small discord server which was an immense help when I was trying to wrap my head around how Lapis worked.

But we're not here for thanks and praise, we're here for the technical grit! Before we begin it should be noted that "<>" has been removed from etlua snippets, as they render inside of the code snippets instead of self escaping.

The Lua Side

My blog is broken down into a few different parts:

  • The web frame app and it's config
  • Some helper Lua scripts
  • A series of etlua templates
  • And an Nginx config & other static content
Our Index, Posts, and 404 return are all generated by Lua executed by Openresty, we define functions for each series of pages, and service them as etlua templates which get populated with data by inline Lua execution. The simplest example of this is our 404 return.

function app:handle_error(err, trace) return { redirect_to = self:build_url("404") } end app:match("404", "/404", function() return { render = "e404", layout = "layout" } end)

When Lapis processes an HTTP request it looks to the app.lua file to run functions based on the URL match provided. So when we path to http://www.lambdacreate.com/404, it matches the /404 path request and renders the e404 etlua template inside of the layout.etlua template, and returns that to the user. And the way these etlua templates work in tandem with Lapis is dead simple, you have a render() function that converts etlua to HTML, or a conent_for('segment') function that matches a route defined in app.lua, which is then rendered as the body of the layout template. All of that looks like this:

In our layout we call this function:

% content_for('inner') %

To populate this template:

I'm not sure how we got here, but something isn't right.. · a href="%= url_for("index") %":: Return ::

This same methodology is used to pass around our posts and render them inside of the playout template, which is just a copy of the layout template with some additional changes to allow for more redirects. The "" calls are the unique part of etlua that allows Lua execution, these render the output of a Lua function into HTML. These embedded functions can even be used to pass around data from the URLs to the templates. I do this in order to get the posts to render!

In out app.lua we define our posts url as:

app:match("/posts/:key", function(self) self.alist = posts.archive self.last = posts.getUpdated() return { render = true, layout = "playout" } end)

What this does is it matches all URL requests of /posts/key as a variable. Self.params.key inside of the Lapis/etlua template refers to a params table populated by the :args given in a URL. So when we say /posts/1, it produces a table of { key = 1 } inside of the Lapis application. Then inside of the playout template we can use this param value to populate the post itself with:

% render("views.posts." .. params.key) %

All in all, it's a very pleasant experience that results in a simple workflow. I can write all of my blog posts as etlua templates, which more or less share the same syntax as HTML, but I don't need to copy boilerplate perpetually, and if I change the way the site CSS is built, or how/where the posts are generated, I can do so in my layout templates without affecting any of my posts! The only other point of note here is the posts.lua file.

Lapis can work readily with mysql or psql, but for something like this, it's utterly overkill. So I'm using plain lua tables to define my post information. I keep all of my post metadata in a posts.archive table, which additionally has some helper functions to return that information to Lapis, such as posts.getUpdated() to generate the Updated: Date string in my footer. And for the sake of being my long winded self, here's what all of that looks like for this post.

posts.archive = { { title = "Over-engineering a Blog: Part 2", fname = "3", desc = "A technical overview of how LambdaCreate works", pdate = "April 8, 2020 } }

Once we've defined our post info, we just drop the 3.etlua template into /posts, and it gets rendered when the URL is called. Delightfully simple. Eventually I would like to populate the post title/description with the information found in posts.archive, but passing params.key as a key arg in something like alist[paramKey]["title"] has returned nothing but errors. The source for the blog can be found here. for those curious.