Rendering Elm to static HTML with Selenium

2020-07-04

Suppose you have a simple page on a website with part of its content generated by an Elm program. To also make (the initial view of) the content available to users or simple web crawlers who do not run JavaScript, it can be useful to generate a static HTML copy. This can also help to prevent flickering when the website loads.

Previously I used elm-static-html static-html-from-elm for this, but in combination with Html.Styled from elm-css the output was not exactly the same as when running the Elm code in a browser and there were encoding problems. Hence, why not use a browser to generate the static HTML?

We can do this using Selenium.

First, we make an HTML file which works in your local browser, say render.html: See also the Elm guide about Embedding in HTML.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <script src="MyElmProject.min.js"></script>
  </head>
  <body>
    <div id="MyApp"></div>
    <script>
    Elm.Main.init({
      node: document.getElementById("MyApp")
    });
    </script>
  </body>
</html>

Second, we use Selenium to control a “headless”, i.e. invisible, Chrome to open the HTML file, run the Elm script and print out the innerHTML. For example, save the following as static-via-selenium.py.

from os import getcwd
from selenium import webdriver

options = webdriver.ChromeOptions()
options.headless = True
driver = webdriver.Chrome(options=options)
url = "file://" + getcwd() + "/render.html"
driver.get(url)
divContent = driver.find_element_by_id('MyApp').get_attribute('innerHTML')
driver.quit()
print(divContent)

Finally, the following Makefile will build the .js, .min.js and .static.html files. For .min.js we follow the Elm guide about minification.

all:

MyApp.js: src/*.elm
    elm make --optimize --output=$@

MyApp.min.js: MyApp.js
    uglifyjs $< --compress \
        'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' \
        | uglifyjs --mangle --output=$@

MyApp.static.html: render.html MyApp.min.js
        python3 static-via-selenium.py > $@

You can now use the content of MyApp.static.html as a static replacement by putting it inside the <div id="MyApp"></div> in index.html. We leave it as an exercise for the reader to also automate this last step.