Enhancing an existing site with a custom plugin

Estimated reading time: 3 minutes.


Among static site generators that are compiled to native executables, soupault is unique in that it can be extended using a real scripting language rather than a template processor that evolved Turing completeness.

One of my main goals was to make a tool that can work with existing site structures, rather than make the user redo everything to fit a tool.

Today we'll see how to enhance an otherwise unmodified website with a custom plugin. I picked Neocities Districts website for a showcase. I'm not affiliated with Districts, I just like their website, but I also think it's a bit hard to navigate and could really benefit from alphabetic indices. Let's see how it could be done with soupault. Its authors are free to reuse the solution if they like it, of course. The result will be fully static and will not need any JS, so it will work even in text browsers and with JS disabled.

The idea

The basic idea is to create a list of clickable links that take you to a specific section.

If you look into the source of districts.neocities.org/arcadia for example, you'll notice that every section heading is an <h2> element, like <h2>A</h2>. This means we can easily reuse the heading content for the anchor. The heading for sites that start with a number is #, which may be problematic, but in practice id="##" works well in browsers, oddly.

Every page also has an <h1> element with the district name. A good place for the index would be before or after that heading, I went for the latter option.

So, here's the idea. First, insert a container for the index at the top of the page, before the first <h1> heading. For example, <div id="index">. Then, for every heading, create a link inside that element, so that <h2>A</h2> becomes <a href="#A">A</a>. And finally, add an id attribute to every heading so that those links actually work.

This is what the result will look like:

Writing the plugin

The plugin language is Lua 2.5, and the API is somewhat reminiscent of the JavaScript DOM API. To select one element you can use the HTML.select_one function, and to select all elements that match a certain selector you can use HTML.select.

One annoying part is that Lua 2.5 standard lacked a modern for loop for iterating over a list in numeric order. Well, the annoying thing about Lua in general is that arrays are really dicts indexed by integers, so any kind of traversal in numeric order is a hack. We'll use a simple loop with a counter, from 1 to size(headings).

The selector of the target element where links are inserted will be configurable. Plugins have access to their own config via config variable, with one caveat: you can only pass string options that way.

So, this is the plugin source:

-- Get the selector option from the config
selector = config["selector"]

-- Find the index container
index_container = HTML.select_one(page, selector)

-- Extract all second level headings from the <main> element
headings = HTML.select(page, "main h2")

max = size(headings)
n = 1

while n <= max do
  heading_content = HTML.inner_html(headings[n])

  -- Set an id for each heading to use it as an anchor
  HTML.set_attribute(headings[n], "id", heading_content)

  -- Create a link to the heading
  link = HTML.create_element("a", heading_content)
  HTML.set_attribute(link, "href", "#" .. heading_content)

  -- Insert the link to the index container
  HTML.append_child(index_container, link)

  n = n + 1

Setting it up

I've created a directory for the project, districts, then a subdirectory for the pages, districts/site. Then I've mirrored districts.neocities.org into districts/site with wget (it's small, so that hasn't created excessive load on the Neocities servers).

Then I've created a districts/plugins directory for plugins. It's not really necessary, but for a real site rather than a one time showcase, it's better to keep them in a separate directory.

Since the goal is to modify an existing site automatically, rather than create a site from a template and page bodies, I switched soupault to the HTML processor mode with generator_mode = false.

This is the complete config (districts/soupault.conf):

  strict = true

  verbose = true

  site_dir = "site"
  build_dir = "build"

  page_file_extensions = ["htm", "html"]

  generator_mode = false
  clean_urls = false

  doctype = "<!DOCTYPE html>"

# Load the plugin
  file = "plugins/alphabetic-index.lua"

# Insert the container for the index
  # Special pages don't need it
  exclude_page = ["index.html", "updates/index.html", "about/index.html", "buttons/index.html"]
  widget = "insert_html"
  selector = "h1"
  action = "insert_before"
  html = '<div id="index" style="display: flex; justify-content: space-evenly;"> </div>'

# Now call the plugin
  after = "insert-index-container"
  exclude_page = ["index.html", "updates/index.html", "about/index.html", "buttons/index.html"]
  widget = "alphabetic-index"

  # Available to the plugin as config["selector"]
  selector = "div#index"

Plugins, once loaded, can be configured just like built-in widgets. Here we are using an exclude_page option to prevent it from running on pages that don't need an index, and an after option to make sure the plugin only runs after the index container is inserted by the insert-index-container widget.

Now the only thing left is to run soupault in the districts directory, and check out the result in build, for example with python3 -m http.server --directory build/.

If you look inside build/arcadia/index.html or inspect the page with debugger, you'll see the new elements inside:

And that's all really. As you can see, there's no need to subscribe to someone else's workflow to use soupault.