Intro
Over the years I've maintained a few different static websites. In the past I've run into the same problems with almost every static website generators:
- π’ Installing the library on a new system is a pain.
- π’ Updating the library breaks everything.
- π’ I forgot how the generator library works.
- π’ I haven't worked in the language the generator uses in a long time and getting up to speed is annoying.
- π’ The generator can't handle partial updates.
I've settled on using Lua
+Make
and it's refreshing how much easier it is to maintain.
The key component has been my 94 line html.lua
library. It isn't perfect but it has everything I need:
- π No templating language to remember
- π It looks/acts like the DOM
- π can be understood at a glance
- π composing DOM nodes and writing helpers is easy
Example Usage
Here's what it looks like in practice:
Lua 5.4.3 Copyright (C) 1994-2021 Lua.org, PUC-Rio
> H = require "html"
-- a tag
> H.div("γγγ°γγ―")
<div>γγγ°γγ―</div>
-- tags can take tags or strings
> H.p(H.div("Hello"), "world", "!")
<p><div>Hello</div>world!</p>
-- tags combine with strings
> H.div("Hello") .. " world!"
<div>Hello</div> world!
-- tags can have attributes
> H.div(attr{class="error"}, "Error!")
<div class="error">Error!</div>
-- attributes can be given in any order and combine
H.div("Error: ", attr{class="error"}, "exception!", attr{aria="sr-only"})
<div aria="sr-only" class="error">Error: exception!</div>
Dom
Tags also work as DOM-like nodes and are composable
-- tags are DOM-like
local ul = H.ul()
for i=1,3 do
ul:append(H.li(i))
end
print(ul)
-- "<ul><li>1</li><li>2</li><li>3</li></ul>
View Helpers
Creating helper functions is easy
local function strip(s)
return s:gsub("^%s+", ""):gsub("%s+$", "")
end
function code_example(code)
return H.code(H.pre(strip(code)))
end
print(code_example[[
function square(n)
return n * n
end
]])
--[[<code><pre>function square(n)
return n * n
end</pre></code>
]]--
html.lua
Here's the complete implementation for html.lua
:
local Attr = {}
local AttrMeta = {
__index = Attr,
__tostring = function(attr)
local str = ""
for k,v in pairs(attr.fields) do
str = string.format("%s %s=%q", str, k, v)
end
return str
end
}
function Attr.merge(self, attr)
for k,v in pairs(attr.fields) do
if self.fields[k] ~= nil then self.fields[k] = self.fields[k] .. " " .. v else self.fields[k] = v end
end
end
function is_attr(t)
return getmetatable(t) == AttrMeta
end
function attr(fields)
return setmetatable({fields=fields}, AttrMeta)
end
local Tag = {}
local TagMeta = {
__index = Tag,
__concat = function(tag, x)
return tostring(tag) .. tostring(x)
end,
__tostring = function(tag)
if tag.options.is_singular then
local closing_slash = " /"
if tag.options.no_closing_slash then
closing_slash = ""
end
return string.format("<%s%s%s>%s", tag.name, tag.attributes, closing_slash, tag:content())
else
return string.format("<%s%s>%s</%s>", tag.name, tag.attributes, tag:content(), tag.name)
end
end
}
function Tag.content(tag)
local str = ""
for i,v in ipairs(tag.contents) do
str = str .. tag.contents[i]
end
return str
end
function Tag.append(self, ...)
local args = {...}
for i,v in ipairs(args) do
if is_attr(v) then self:attr(v) else table.insert(self.contents, v) end
end
return self
end
function Tag.attr(self, attr)
self.attributes:merge(attr)
return self
end
function tag(name, opt, ...)
return setmetatable({
name=name,
options=opt,
contents={},
attributes=attr{}
}, TagMeta):append(...)
end
local is_singular = {
img = true,
input = true,
link = true,
meta = true
}
return setmetatable({
tag=tag, -- make your own tags
attr=attr, -- specify attributes
doctype=function(...)
-- example custom tag
return tag("!DOCTYPE html", {is_singular=true, no_closing_slash=true}, ...)
end
}, {
-- dynamically generate tags
__index=function(t, name)
t[name] = function(...) return tag(name, {is_singular=is_singular[name]}, ...) end
return t[name]
end
})
Setup
To start a new static website I usually make these files:
www/
Makefile
fix_paths.lua
src/index.lua
lib/html.lua
Makefile
The Makefile pretty simple. It will look for any .lua
file in src/
and create a corresponding .html
file in rel/
.
CP= cp -r
RM= rm -rf
MKDIR= mkdir
LUA= lua -l fix_paths
REL=rel/
SRC=src/
HTML= $(patsubst $(SRC)%.lua, $(REL)%.html, $(wildcard $(SRC)*.lua))
all: $(REL) $(HTML)
$(REL)%.html: $(SRC)%.lua
$(LUA) $< > $@
$(REL):
$(MKDIR) $(REL)
clean:
$(RM) rel/
.PHONY: all clean
For static files I'll usually add a rule to copy files to rel/
based on how I want to organize things. For example if I just put all my js/css files under assets/
:
ASSETS= $(REL)assets
all: $(REL) $(HTML) $(ASSETS)
$(ASSETS):
$(CP) assets $(ASSETS)
fix_paths.lua
The fix_paths.lua
file modifies the lua path to load modules under lib/
where html.lua
is located. This is a common pattern. You could also add your path to your luarocks
directory as well.
-- fix lua paths for src dir
package.path = 'lib/?.lua;src/?.lua;' .. package.path
package.cpath = 'src/?.so;src/?.so;' .. package.cpath
index.lua
And here's an example page:
local H = require "html"
local function layout(title, ...)
local head = H.head(
H.title(title),
H.meta(attr{name="viewport", content="width=device-width, initial-scale=1.0"}),
H.meta(attr{charset="UTF-8"})
)
local body = H.body(...)
body:append(H.div(attr{id="copyright"}, "Content ©2022"))
return H.doctype(H.html(head, body))
end
local function strip(s)
return s:gsub("^%s+", ""):gsub("%s+$", "")
end
function code_example(content)
return H.pre(H.code(strip(content)))
end
function homepage()
return layout(
"Hello World!",
H.div("Hello World!"),
code_example[[
function foo()
return "OK!"
end
]]
)
end
-- make will put the output from this script into `rel/index.html`
print(homepage())
This code could be cleaned up quite a bit but it's just an example. I usually put my helpers in a module like lib/view.lua
then import them in my pages:
local function TODO(...) return H.div(attr{class="todo"}, ...) end
return {TODO=TODO}
local V = require "view"
function homepage()
return H.div(TODO("write this html"))
end
So my common elements are all in one place but that's entirely optional. I find that how to organize your code is pretty trivial.
Usage
To generate html just run make
:
$ make
lua -l fix_paths src/index.lua > rel/index.html
$ cat rel/index.html
<!DOCTYPE html><html><head><title>Hello World!</title><meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta charset="UTF-8" /></head><body><div>Hello World!</div><pre><code>function foo()
return "OK!"
end1</code></pre><div id="copyright">Content ©2022</div></body></html>
Deploying
I use AWS for my static sites so deploying to Route 53 is usually just a matter of syncing to my S3 bucket.
I usually add a make deploy
rule for this:
AWS= aws
.PHONY: deploy
deploy:
$(AWS) sync rel/ s3:bucket-name --dryrun --delete
Which has --dry-run
by default. I then type the command without it if I'm happy with the output:
$ aws sync rel/ s3:bucket-name --delete