Back to posts


YASSS - Yet Another Static Site Server

Having just made a static site generator the first post on it might as well be about how I made it since I don't have any plans for what I'm going to write about.

A static site generator simply creates the necessary HTML to show some web pages, there's no database or REST API, just HTML (maybe some simple javascript). The world does not need another static site generator, there are probably thousands and I would guess nearly all of them are better than this. But I'm stuck at my parents house over Christmas and writing one in nim sounded like a quick, easy project to amuse myself with.

I have used jekyll in the past which is my only experience with these sort of things, so YASSS copies the basics from what I remember: write some blog-style posts in markdown with a yaml header, run a command and out pops your html files which can be rendered with something like github pages. So for this I'm using soasme's nim-markdown library which converts markdown to html and then sticking this directly in the body of a html template using nim-mustache (again from soasme). I then link to these posts from a main index.html template.

Fetching posts

First we need to find all the markdown files in a given directory, in this case ./posts/. This should be fairly easy in most languages, in nim it's a case of:

import std/[os, sequtils, strformat]

proc getAllPosts(fpath: string = "./posts"): seq[string] =
  return walkFiles(fmt"{fpath}/*.md").toSeq().map(readFile)

This uses nim's walkFiles() with a wildcard to get only the markdown (.md) filenames and then uses readFile to read the file into memory, returning a sequence where each element is a post as a raw string.

Parsing the frontmatter

The frontmatter is used as a way to store some basic metadata about the post such as title, date, and whether it should be public. This is copied from jekyll where they use a yaml header at the top of the markdown post between --- lines like so:

 ---
 title: my first post
 date: 2022-12-25
 public: false
 ---

 # heading
 ## subheading
 Content goes here.

In jekyll there's a whole load customisations you can put in the frontmatter, but for this I'll stick with the title (as appears in the index), the date which is used to sort the posts in the index, and public as a boolean flag whether the posts should be made public or not. I've also added a description value which is parsed, but this isn't used anywhere yet.

Now we could just fetch those lines between the --- lines and parse them with a yaml parser (or probably any other key-value parser), but I think there's already enough dependencies in this project so I wrote my own bug-ridden key-value parser.

proc parseFrontMatter(text: string): FrontMatter =
  ## parse frontmatter from raw markdown text
  var
    isFrontMatter = false
    frontMatterLines: seq[string]
    title, formattedTitle, description, date: string
    public: bool = true
  for line in text.split("\n"):
    if line.startswith("---"):
      isFrontMatter = not isFrontMatter
      continue
    if isFrontMatter:
      frontMatterLines.add(line)
  # parse out key-value pairs from frontMatterLines
  for line in frontMatterLines:
    if line.startswith("title"):
      title = line.split(":")[^1].strip()
      formattedTitle = formatTitle(title)
    if line.startswith("description"):
      description = line.split(":")[^1].strip()
    if line.startswith("date"):
      date = line.split(":")[^1].strip()
    if line.startswith("public"):
      public = line.split(":")[^1].strip().toLower().startswith("t")
  return FrontMatter(
    title: title,
    formattedtitle: formattedTitle,
    description: description,
    date: date,
    public: public
  )

This has 2 steps, first get the lines between the two --- delimiter lines:

var
  isFrontMatter = false
  frontMatterLines: seq[string]
for line in text.split("\n"):
  if line.startswith("---"):
    isFrontMatter = not isFrontMatter
    continue
  if isFrontMatter:
    frontMatterLines.add(line)

then the second step is to get the key-value pairs from those lines and return it has a FrontMatter object:

for line in frontMatterLines:
  if line.startswith("title"):
    title = line.split(":")[^1].strip()
    formattedTitle = formatTitle(title)
  if line.startswith("description"):
    description = line.split(":")[^1].strip()
  if line.startswith("date"):
    date = line.split(":")[^1].strip()
  if line.startswith("public"):
    public = line.split(":")[^1].strip().toLower().startswith("t")
return FrontMatter(
  title: title,
  formattedtitle: formattedTitle,
  description: description,
  date: date,
  public: public
)

This is pretty ugly, it just checks the start of the line for the key value, and splits on : to find the value, and I can already see how it would break if someone has a : character in their title. So maybe it will get tidied up at some point.

Parsing the markdown body

Parsing the markdown body is even easier, just loop through lines in the files which aren't in the frontmatter, join them together into a single string and use nim-markdown to convert the markdown contents to html.

import markdown

proc parseBody(text: string): string =
  ## get body from raw markdown text that includes frontmatter
  var
    body: seq[string]
    isFrontMatter = false
  for line in text.split("\n"):
    if line.startswith("---"):
      isFrontMatter = not isFrontMatter
      continue
    if not isFrontMatter:
      body.add(line)
  return markdown(body.join("\n"))

Creating the post html from a template

The aim now is to stick the html (converted from markdown) into the body of a simple html template. For this I'm using nim-mustache which implements mustache as a templating language.

<html lang="en">
  <head>
    <title>{{ title }}</title>
    <meta charset="UTF-8">
    <link href="css/style.css" rel="stylesheet">
  </head>
  <body>
    <p><a href="index.html">Back to posts</a></p>
    <hr>
    {{{ body }}}
  </body>
</html>

Here we insert a title into {{ title }}, and the body of the post in {{{ body }}}. The body is within three curlies as we want to insert the raw html, otherwise mustache escapes it for us which we don't want.

I've removed it from the above example, but there's also some highlight.js scripts I'm pulling from a CDN so I can have syntax highlighting and mathjax to render $\LaTeX$.

Creating the index page

The index page is just a list which links to each post in date order, so we're just looping through the posts in a template.

The nim code:

proc createIndex(posts: seq[Post]) =
  ## create context for index template
  var frontMatters: seq[FrontMatter]
  for post in posts:
    frontMatters.add(post.frontMatter)
  frontMatters = frontMatters.filter(x => x.public == true)
  frontMatters.sort(cmpDate, Descending)
  let context = newContext(searchDirs=TemplateDir)
  context["posts"] = %frontMatters
  var
    index_html = "{{ >index }}".render(context)
    save_path = fmt"{PublicDir}/index.html"
  writeFile(save_path, index_html)

Which is made from the template:

<html lang="en">
  <head>
    <link href="css/style.css" rel="stylesheet">
  </head>
  <body>
    <h1>Posts</h1>
    <hr>
    <ul>
      {{ #posts }}
      <li><a href="{{ formattedTitle }}.html">{{ date }}: {{ title }}</a></li>
      {{ /posts }}
    </ul>
  </body>
</html>

Github actions

I've yet to figure this out. I guess if you can read this then I did figure it out but forgot to update this post.

My plan is to push posts in markdown to a github repository, and then have github actions use YASSS to generate the HTML which is then accessible with github pages. This should be simple in theory, there's plenty of tutorials out there about using custom github actions with github pages.