Elmstatic: an Elm-to-HTML static site generator

Elmstatic is in an early stage of development but I’ve been using it to generate this site for a while.

This is the current set of features:

  • Pages generated from Elm code (you can use elm-ui, html or any other package that generates Html msg values)
  • CSS generated from elm-css stylesheets in Elm code (but you can use plain old stylesheets if you like)
  • Page templates for posts and lists of posts (fully customisable)
  • Optional subsections with their own posts (eg /postgres and /elm on this site)
  • Posts are written in Markdown
  • Posts can have multiple tags
  • A page with a list of posts is generated for each tag
  • RSS is generated for the posts
  • Code highlighting via Highlight.js (but you can set up whatever you want)

How to use it

Elmstatic requires Elm 0.19 and uses the elm executable.

The basic workflow is like this:

  • Install the package from NPM:
$ npm install -g elmstatic
  • Create a directory for your site:
$ mkdir mysite
$ cd mysite
  • Run the init command in your site directory to generate a scaffold:
$ elmstatic init
  • Change the generated files to your liking, then generate the output:
$ elmstatic
  • Run an HTTP server in the output directory to test out the site:
$ cd _site
$ http-server
  • Publish to your host of choice (I use GitHub Pages)

  • If you want automatic re-generation on changes, you can use Chokidar, for example:

$ npm install -g chokidar-cli
$ chokidar . -d 1000 -s -i "elm.js" -i "elm-stuff/**/*" -i ".git/" -i "_site/**/*" -c elmstatic --initial

(Run the above chokidar command in the site directory.)

How things are organised

This is the file structure of the scaffold:

.
├── _layouts
│   ├── Elmstatic.elm
│   ├── Page.elm
│   ├── Post.elm
│   ├── Posts.elm
│   ├── Styles.elm
│   └── Tag.elm
├── _pages
│   ├── about.md
│   └── contact.md
├── _posts
│   ├── 2019-01-01-using-elmstatic.md
│   ├── 2019-01-02-another-post.md
│   └── index.md
├── _resources
│   ├── img
│   │   └── logo.png
│   └── styles.css
├── config.json
├── elm.js
└── elm.json

If you’ve used Jekyll (a Ruby-based static site generator), you will find this familiar.

A site consists of pages linked in any way you like, and posts. The content of pages and posts is written in Markdown, while the HTML layout of all pages and posts is described in Elm. You can specify the layout of a given page or post in the Markdown frontmatter.

The frontmatter section also allows you to set other metadata, like title and tags.

The site can also include other files which are stored in _resources.

_layouts contains all of the Elm code. There is a module for each separate page layout, a module for styles, and a utility module called Elmstatic.

Files in _pages store the Markdown content of each page, while files in _posts store the content of posts.

config.json

config.json contains the site-specific parameters which Elmstatic inserts into appropriate places. The default config looks like this:

{
    "siteTitle": "Author's fancy blog",
    "outputDir": "_site",
    "copy": {
        "/posts": "/"
    },
    "feed": {
        "title": "Author's fancy blog",
        "description": "Author's blog posts",
        "id": "https://example.com",
        "link": "https://example.com/posts",
        "image": "https://example.com/img/logo.png",
        "copyright": "Copyright: Author",
        "language": "en",
        "generator": "Elmstatic",
        "author": {
          "name": "Author",
          "link": "https://example.com/about"
        }
    },
    "tags": ["other", "software"]
}

siteTitle goes into the <title> tag in HTML.

outputDir is a relative path from the site’s directory.

copy is used to give aliases to pages. The scaffold specifies that /posts will also be available as the root level index page. This aliasing is done as a postprocessing step, by copying files.

The object under the feed key specifies parameters for the RSS feed.

The tags key supplies an array of valid tags for posts. If a post has a tag that’s not in this list, elmstatic will generate an error. This key is optional. If it’s absent, then no validation is performed on post tags.

Additionally, you can add the optional elm key to provide a path to the elm executable relative to the site’s directory. It’s useful if you don’t have elm available in your PATH. For example, you can install Elm 0.19 locally into your site directory with npm install elm, which will place elm into node_modules/.bin/elm. Then you can point elmstatic to it by adding the elm key to config.json:

  "elm": "node_modules/.bin/elm"

elm.json

elm.json is a standard issue Elm project file. If you need additional Elm packages in your code, add them here.

_layouts/Elmstatic.html

This file provides the functions to generate the page layouts. The key function is layout which is used to define the main function of each layout module.

The default HTML template generated by this module looks like this:

<!doctype html>
<html>
<head>
    <title>${title}</title>
    <meta charset="utf-8">
    <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.1/highlight.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.1/languages/elm.min.js"></script>
    <script>hljs.initHighlightingOnLoad();</script>
    <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.1/styles/default.min.css">
    <link href="//fonts.googleapis.com/css?family=Open+Sans|Proza+Libre|Inconsolata" rel="stylesheet" type="text/css">
</head>
<body>
${content}
</body>
</html>

${content} gets replaced with the HTML nodes for a given page. In addition to the content itself, you are free to add custom styles, custom JavaScript, analytics or Google Fonts, for example.

Note that there’s nothing special about this file. You can modify it to suit your needs, for example if you need to generate different tags in <head>.

_layouts/Page.elm

This is a layout for pages. Each layout must export a main function. In this module, it’s defined as:

main : Elmstatic.Layout
main =
    Elmstatic.layout Elmstatic.decodePage <|
        \content ->
            layout content.title [ markdown content.markdown ]

_layouts/Post.elm, _layouts/Posts.elm, _layouts/Tag.elm

These are layouts for individual posts and lists of posts.

_layouts/Styles.elm

This module exports an elm-css stylesheet, which is an Html msg value. This is then included in other layouts.

_pages/about.md, _pages.contact.md

These are files with page content. They use the Page layout unless another layout is specified in the frontmatter (eg layout: MyCustomPage).

_posts/2019-01-01-using-elmstatic.md, _posts/2019-01-02-another-post.md

Posts are handled very similarly to Jekyll.

The post files have a particular naming scheme: YYYY-MM-DD-kebab-case-post-url-slug.md. Elmstatic extracts the date of the post and the URL for it from the file name.

Future-dated posts are considered to be draft posts, and excluded from the generated HTML by default. To include draft posts, run elmstatic draft instead of just elmstatic.

Additionally, each post file starts with a preamble that looks like this:

---
title: "Introducing Elmstatic"
tags: software elm
---

The title has to be quoted. It appears both at the top of the post as well as on post list pages.

The post can have one or more tags. For each tag which has associated posts, Elmstatic generates a tag page with a list of posts containing that tag.

The body of the post is written in Markdown and appears after the preamble.

The default layout used for post pages is Post.

_posts/index.md

This file is provided to allow you to customise the title of the post list, and potentially to use a custom layout. By default, the Posts layout is used.

_resources

This directory contains any additional files you’d like to copy over to the root of the site. So, for example, _resources/img/logo.png becomes available as myblogdomain.com/img/logo.png on the published site.

Directory structure of the output

This structure is modelled on the output of Jekyll. For the scaffold described above, the output structure will look like this:

_site
├── 2019-01-01-using-elmstatic
│   └── index.html
├── 2019-01-02-another-post
│   └── index.html
├── about
│   └── index.html
├── contact
│   └── index.html
├── img
│   └── logo.png
├── index.html
├── posts
│   ├── 2019-01-01-using-elmstatic
│   │   └── index.html
│   ├── 2019-01-02-another-post
│   │   └── index.html
│   └── index.html
├── rss.xml
├── styles.css
└── tags
    ├── other
    │   └── index.html
    └── software
        └── index.html

Subsections

In addition to the root level pages like /contact and /about, this site (korban.net) has a couple of subsections: korban.net/postgres and korban.net/elm. These are defined via subdirectories of _pages, and Elmstatic handles them in a particular way.

The structure of _pages for this site looks like this:

_pages
├── about.md
├── consulting.md
├── contact.md
├── courses.md
├── elm
│   ├── about.md
│   ├── book.md
│   ├── contact.md
│   ├── elmstatic.md
│   └── uicards.md
├── postgres
│   ├── about.md
│   ├── book.md
│   ├── contact.md
│   └── pgdebug.md
└── projects.md

Correspondingly, under _posts I have:

_posts
├── elm
│   ├── 2018-01-23-decoding-json-to-nested-record-fields-in-elm.md
│   ├── 2018-03-15-how-to-read-elm-types-like-html-msg.md
|   └── ...
└── postgres
|   ├── 2013-11-11-installing-postgis-with-homebrew.md
|   ├── 2014-11-18-visualising-postgis-data-from-shell.md
|   └── ...
├── 2009-12-06-add-case-insensitive-finders-by-extending-activerecord.md
├── 2010-03-04-delete-expired-activerecord-based-sessions-regularly.md
└── ...

Because I wanted to have different navigation in the subsections, I created additional layouts for these pages and posts in _layouts: ElmPage.elm, ElmPost.elm, PostgresPage.elm, PostgresPost.elm and so on. These layouts are specified in the .md files in subsections.

Most of the special handling is to do with posts. /posts/elm only lists posts from the _posts/elm directory, and /posts/postgres only lists posts from the _posts/postgres directory. However, /posts aggregates all of the posts into a single list, meaning it lists posts from the root level as well as posts from each subsection.

Each section is treated as an additional tag for posts in that section, so Elmstatic also generates /tags/elm and /tags/postgres pages.

Looking for the nuts-and-bolts guide to creating non-trivial real world apps in Elm?

My book, Practical Elm for a Busy Developer, skips the basics and gets right into explaining how to do practical stuff. Things like building out the UI, communicating with servers, parsing JSON, structuring the application as it grows, testing, and so on. No handholding — the focus is on giving you more substance.

It’s up to date with Elm 0.19.

Pop in your email to get a sample chapter.

(You will also get notifications of new posts along with other mailing list only freebies.)

Book cover