A Static Website Generator in 94 lines of Lua

# 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:
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

```lua:
-- 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

```lua:helper.lua
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`:

```lua: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/`.

```make:Makefile
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/`:

```make:Makefile
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.

```lua:fix_paths.lua
-- 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:

```lua:src/index.lua
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 &copy;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:

```lua:lib/view.lua
local function TODO(...) return H.div(attr{class="todo"}, ...) end
return {TODO=TODO}
```

```lua:src/index.lua
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 &copy;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:
```make:Makefile
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
```

## Example

An example of a site using this generator
[
![mixtapes.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33835/1e931833-948f-e21e-eb07-26e13c3a5bdd.png)
](https://lazythunk.com/mixtapes.html)