Elmstatic: an Elm-to-HTML static site generator

Elmstatic allows you to write the HTML layouts and the styles for the pages of your site in Elm. It’s fairly unopinionated: you can generate whatever HTML you want, however you want — all you need to do is define a suitable main function in each layout.

A summary of current features:

  • Pages are generated from Elm code (you can use elm-ui, html or any other package that generates Html msg values)
  • CSS is generated from elm-css stylesheets in Elm code (but you can use plain old stylesheets if you like)
  • Fully customisable layouts for posts and lists of posts
  • Optional subsections with their own posts (eg /postgres and /elm on this site)
  • Posts can be written in Markdown or elm-markup (but you can actually treat content as any format if you like)
  • Posts can have multiple tags
  • Future-dated posts are considered to be drafts and excluded from the build by default (or included with a flag)
  • A page with a list of posts is generated for each tag
  • RSS is generated for the posts, including a feed with all posts and a feed per subsection
  • Code blocks have syntax highlighting via Highlight.js (but you can set up something else)
  • Watch mode where Elmstatic watches for file changes and rebuilds the site (live reload can be achieved by combining with browser-sync).

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 Markdown scaffold:
$ elmstatic init
  • Alternatively, to generate an elm-markup scaffold, supply the flag:
$ elmstatic init --elm-markup
  • Change the generated files to your liking, then generate the output:
$ elmstatic build
  • Run an HTTP server in the output directory to test out the site:
$ cd _site
$ http-server
  • Publish to your host of choice (see instructions for several services below).

Live reload

Live reload can be achieved by using Elmstatic watch mode together with browser-sync (or another HTTP server that supports live reload):

  • Install browser-sync:
$ npm install -g browser-sync
  • Run Elmstatic in watch mode in your site directory:
$ elmstatic watch
  • Run browser-sync in the output directory (<site dir>/_site by default):
$ cd _site
$ browser-sync start --server --files "." --no-ui  --reload-delay 500 --reload-debounce 500

How things are organised

This is the file structure of the Markdown 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.json

If you’ve used Jekyll (a Ruby-based static site generator), you will find this familiar. If you choose to go with elm-markup content format, you’ll have .emu files instead of .md, and the frontmatter will be a |> Metadata block in elm-markup format. Read the announcement of elm-markup support to find out how to work with elm-markup.

A site consists of pages linked in any way you like, and posts. The content of pages and posts is written in Markdown or elm-markup, 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 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 or elm-markup content of each page, while files in _posts store the content of posts.

_pages, _posts and _resources are optional — you can have a site without any or all of these (but to get rid of _posts, you will need to change the default config).

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.elm

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 goes here --> 
</body>
</html>

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>, add custom styles, custom JavaScript, analytics or Google Fonts, for example.

_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 ->
            Ok <| layout content.title [ markdown content.content ]

_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 frontmatter section that looks like this in Markdown:

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

or like this in elm-markup:

|> Metadata
    title = Introducing Elmstatic
    tags = software elm

The frontmatter in Markdown files is in YAML format.

The title appears both at the top of the post as well as on post list pages.

A 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 or elm-markup format and appears after the frontmatter section.

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.

Deployment

Deploying via GitHub

The easiest option is to use the docs/ folder:

  • Create a new repository and clone it
  • In the repository directory, create a new site with elmstatic init
  • Edit config.json to set the output directory to docs
  • Build the site with elmstatic build and add all the files to the repository
  • In the repository settings on GitHub, find the GitHub Pages section and set the source to “master branch /docs folder”.

After this, the site should become available at https://<your github name>.github.io/<repo name>/.

Deploying via Netlify

  • Create a new site with elmstatic init
  • Push the files to a GitHub repository
  • Add _site to .gitignore - with this setup, you don’t need to commit the generated output
  • Create a site on Netlify and connect the GitHub repository to it
  • The build command can be, for example: npm i -g elm@latest-0.19.1 && npm i -g elmstatic && elmstatic build -v
  • The publish directory should be _site if you haven’t changed the output location in config.json

Now the site should be rebuilt every time you push source changes to GitHub.

Deploying via GitLab

.gitlab-ci.yml file should look like this for deploying to GitLab:

image: node:current

pages:
  cache:
    paths:
    - node_modules
    - /root/.elm
    - elm-stuff
  script:
  - npm install -g elm --unsafe-perm=true
  - npm install -g elmstatic
  - elmstatic
  - mv _site public
  artifacts:
    paths:
      - public
  only:
    - master

.gitignore should exclude additionally exclude _site in this scenario:

elm.js
elm-stuff/
_site/

Who is using this?

There are a few sites that I’m aware of:

Do you have a public Elmstatic site? Please contact me and let me know so I can add it here!

Would you like to dive further into Elm?
📢 My book
Practical Elm
skips the basics and gets straight into the nuts-and-bolts of building non-trivial apps.
🛠 Things like building out the UI, communicating with servers, parsing JSON, structuring the application as it grows, testing, and so on.
Practical Elm