I’ve been using Elm to generate this site for a few months. It’s still highly experimental and incomplete but I hope that other Elm enthusiasts may find it useful.
At the moment, it works for my needs and allows me to generate this site with a single command, but it requires manual intervention if I’m doing things like deleting pages or adding new Elm dependencies, and I’m sure there are many use cases it doesn’t address.
I think I have something barebones but useful so I published it on NPM under the name of Elmstatic.
This is the current set of features:
style-elements
, html
or any other package that generates Html msg
values)elm-css
stylesheets in Elm code (but you can use plain old stylesheets if you like)The basic workflow is like this:
$ npm install -g elmstatic
$ mkdir mysite
$ cd mysite
$ elmstatic init
$ elmstatic
$ cd output
$ http-server
This is the file structure of the scaffold:
|- Pages
|- About.elm
|- Post.elm
|- Posts.elm
|- Posts
|- 2018-01-01-using-elmstatic.md
|- Resources
|- img
|- logo.png
|- config.json
|- elm-package.json
|- Page.elm
|- Styles.elm
|- Tags.elm
|- template.html
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": "output",
"index": "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"
}
}
}
siteTitle
goes into the <title>
tag in HTML.
outputDir
is a relative path from the site’s directory.
index
names a page which is going to become index.html
at root level. In the example above, it’s set to posts
, which means that Elmstatic will use Pages/Posts.elm
to generate two pages: /posts/index.html
and /index.html
.
The object under the feed
key specifies parameters for the RSS feed.
elm-package.json
is a standard issue Elm file. If you need additional Elm packages in your code, add them here.
template.html
is the HTML template used for every page:
<!doctype html>
<html>
<head>
<title>${title}</title>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/languages/elm.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css">
<link href="//fonts.googleapis.com/css?family=Open+Sans|Proza+Libre|Inconsolata" rel="stylesheet" type="text/css">
</head>
<body>
${content}
<link rel="stylesheet" href="/css/default.css">
</body>
</html>
${content}
gets replaced with the HTML for a given page. This is where you can add custom styles, custom JavaScript, analytics or Google Fonts, for example. I toyed with the idea of moving this template to Elm as well, but I figured there wouldn’t be much value in it.
The scaffold file looks like this:
module Styles exposing (..)
import Css exposing (..)
import Css.Foreign exposing (..)
import Css.Colors exposing (..)
import Html
import Html.Styled
import Json.Decode
codeStyle =
[ fontFamily [ "Inconsolata", monospace ]
, fontSize (rem 1)
]
styles : Html.Html msg
styles =
global
, class "markdown"
[ descendants
[ a [ color <| hex "348aa7" ] ]
, each [ h1, h2, h3, h4, h5, h6 ] [ fontFamily [ "Proza Libre", "Helvetica", sansSerif ] ]
, code codeStyle
, pre
[ descendants [ code codeStyle ] ]
]
]
|> Html.Styled.toUnstyled
It’s an elm-css
stylesheet. The generator looks specifically for the Styles.styles
function.
It’s needed to style Markdown (within nodes with the .markdown
class) and any other external DOM nodes which you aren’t able to style via Elm.
This stylesheet gets converted into /css/default.css
which is linked in template.html
.
The scaffold is set up with a couple of default tags which you can change as you see fit:
module Tags exposing (..)
import String exposing (toLower)
type Tag
= Books
| Personal
| Software
| UnknownTag -- This is required
fromString : String -> Tag
fromString s =
case toLower s of
"books" ->
Books
"personal" ->
Personal
"software" ->
Software
_ ->
UnknownTag
Tags are applied to blog posts. Since tags are specified in Markdown files, there is no compile time enforcement. UnknownTag
is there to catch any misspelled or nonexistent tags - and then you can go to the /tags/unknowntag
page to check if there are any posts with invalid tags.
In retrospect, I’m not sure if I even need the union type - maybe just a list of valid tags would be sufficient.
For each tag that has posts, the generator creates a page with a list of the posts under /tags/<tag name>
.
Page.elm
contains the bulk of the code which describes the layout and styling. It exports functions which are then used by specific pages. For example, this is the view
function:
view : Header Variations m -> List (Element PageStyles Variations m) -> Html.Html m
view header contentElems =
viewport styleSheet <|
column Main
[ center, width (percent 100) ]
[ header
, column Main
[ width <| px 800, paddingTop 10, paddingBottom 50, spacingXY 0 10, alignLeft ]
contentElems
, footer
And here is the view
function from About.elm
which uses functions from Page.elm
:
pageContent =
"""
You can write something about *yourself* here using `Markdown`.
"""
view : a -> Html.Html msg
view _ =
Page.view Page.topLevelHeader
[ Page.title "About the Author"
, Page.markdown pageContent
]
Each of the modules in the Pages directory gets converted into a corresponding HTML page by Elmstatic.
These functions provide templates for a single post and for a list of posts.
For example, the generator will take the Markdown from Posts/2018-01-01-using-elmstatic.md
, pass it to the Pages.Post.view
function and use its result to generate HTML.
Note that the Elm naming conventions for modules apply here, so each module name has to be prefixed with Pages
, eg Pages.Post
.
Everything in the Resources
directory gets copied verbatim to the root level of the output directory. So, for example, Resources/img/logo.png
becomes available as myblogdomain.com/img/logo.png
on the published site. This is where you can add additional stylesheets, scripts and images.
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.
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 has to have one or more tags. For each tag which has associated posts, Elmstatic generates a post list page.
The body of the post is written in Markdown and appears after the preamble.
This structure is modelled on the output of Jekyll. For the scaffold described above, the output structure will look like this:
|- about
|- index.html
|- css
|- defaults.css
|- img
|- logo.png
|- posts
|- 2018-01-01-using-elmstatic
|- index.html
|- index.html
|- tags
|- personal
|- index.html
|- software
|- index.html
|- index.html
|- rss.xml
I used Jekyll for my site for a long time. While Jekyll is convenient enough for a blog, adding and managing various pages in addition to blog posts was always a bit of a pain.
At one point I also discovered the style-elements
library for Elm, and it was far more pleasant to use for layouts than CSS. Things made sense in a way they never did with CSS.
This gave me the idea that I could whip up my own static site generator which would allow me to use Elm for generating pages. After all, I’m not doing anything fancy, I don’t even have comments on my blog posts!
Following the great tradition of yak shaving, I set out to experiment and find out how much Elm I can use and how it could work. Naturally, I thought it would only take a couple of days, and naturally, I was wrong. It took quite a few days to get even a barebones version working.
First of all, I had to find a way to convert Elm code into HTML.
It turned out that there is an NPM package called elm-static-html which relies on another package called node-elm-compiler
to invoke the Elm compiler and generate HTML output from Html msg
values returned by view functions. I could borrow this approach with slight modifications for my purposes. Since style-elements
functions produce Html msg
values in the end, I could use style-elements
without problems.
I quickly figured out that it makes no sense to write posts in Elm, both because I already had an archive of posts and because Markdown is simply much more convenient. Here, I ran into a problem: the default Elm package for generating Markdown, elm-markdown, does it at runtime using JavaScript. That’s far too late for me! I wanted my posts converted before I published them.
Luckily, I found another package, pablohirafuji/elm-markdown, which is a pure Elm implementation of Markdown parsing. It allows me to produce Html msg
values from Markdown by calling Markdown.toHtml
. Great, it’s just what I needed!
I could plug Markdown into my style-elements
layouts like this:
markdown : String -> Element PageStyles v msg
markdown content =
Element.html <|
Html.div [ Html.Attributes.class "markdown" ] (Markdown.toHtml Nothing content)
-- ...
viewport styleSheet <|
column Main
[ center, width (percent 100) ]
[ header
, column Main
[ width <| px 800, paddingTop 10, paddingBottom 50, spacingXY 0 10, alignLeft ]
[ markdown "Here comes the **Markdown**" ]
, footer
]
The only problem was that because I had all this HTML generated by another package, I couldn’t apply the style-elements
styling to it. I settled on styling Markdown via a CSS file (hence the extra div
with markdown
class in the markdown
function).
It was also important for me to have syntax highlighting for code snippets, and I used Highlight.js for that rather than trying to do something in Elm. Highlight.js can highlight Elm code.
Finally, I used the feed package from NPM to generate RSS feeds, and I had a working site generated from Elm.
The downside of elm-static-html
was that it used native modules which are going to become unavailable in Elm 0.19. I asked about the native module issue on Slack, and Noah told me that after Elm 0.19 is out, only elm-static-html-lib
will be compatible, using some new approach that doesn’t involve native modules.
I decided to convert my generator to elm-static-html-lib
(I wrote a post about elm-static-html-lib
).
It was fairly straightforward, and along with a bit of future-proofing, I also cleaned up the generator code quite a lot and even got better performance.
I also noticed that the Css.Foreign
module of rtfeldman/elm-css produces Html msg
values via its global
function (it creates a script
node containing CSS). This meant that I could easily adapt my generator to produce a CSS file from Elm code!
elm-static-html-lib
is unable to handle some scenarios:
These things are on my list of future improvements.