Skip to content

lucamug/elm-starter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

elm-starter

elm-starter is an experimental Elm-based Elm bootstrapper that can also be plugged into already existing Elm applications.

Post "elm-starter", a tool for the Modern Web.

Example of the installed version, with and without Javascript enabled:

elm-starter

Demos

These are three simple examples of websites built with elm-starter:

Collection of examples elm-physics-example

Characteristics

  • Generate a PWA (Progressive Web Application)
  • Mostly written in Elm
  • Pages are pre-rendered at build time
  • Works offline
  • Works without Javascript(*)
  • SEO
  • Preview cards (Facebook, Twitter, etc.) work as expected
  • Installable both on desktop and on mobile
  • High score with Lighthouse
  • Friendly notifications: "Loading...", "Must enable Javascript...", "Better enable Javascript..."
  • Potentially compatible with all Elm libraries (elm-ui, elm-spa, etc.)
  • Hopefully relatively simple to use and maintain
  • Non invasive (you can easily add/remove it)
  • Works with Netlify, Surge, etc.
  • Supports websites living in subfolder

Lighthouse report:

Lighthouse report

Slack's previews (note how different urls have different snapshot and meta-data):

Slack's previews

How to bootstrap a new project

elm-starter is not published in npm yet and it doesn't have a specific command to bootstrap a project, so the way it works now is cloning this repo.

The fastest way is to click here. This will automatically clone the repo and publish in Netlify.

Deploy to Netlify

Otherwise the steps are:

git clone https://github.com/lucamug/elm-starter
mv elm-starter my-new-project
cd my-new-project
rm -rf .git
npm install

Done! elm-starter is installed.

To start using it you should create your own git repository, add files and make a commit (example: git init && git add . && git commit -m "initial commit").

These are the available commands:

$ npm start

Runs the app in the development mode. Open http://localhost:8000 to view it in the browser.

Edit src/Main.elm and save to reload the browser.

Also edit src/Index.elm and package.json for further customization.

$ npm run build

Builds the app for production to the elm-stuff/elm-starter-files/build folder.

$ npm run serverBuild

Launches a server in the build folder.

Open http://localhost:9000 to view it in the browser.

How to use elm-starter in existing Elm application

Let's suppose your existing project is in my-elm-app

  • Clone elm-starter with
    $ git clone https://github.com/lucamug/elm-starter.git

  • Copy the folder elm-starter/src-elm-starter/ to my-elm-app/src-elm-starter/

  • Copy the file elm-starter/src/Index.elm to my-elm-app/src/Index.elm

  • Copy the function conf from elm-starter/src/Main.elm to my-elm-app/src/Main.elm (remember also to expose it)

  • If you don't have package.json in your project, add one with $ npm init

  • Be sure that you have these values in package.json as they will be used all over the places:

    • "name" - An npm-compatible name (cannot contain spaces)
    • "nameLong" - The regular name used all over the places, like in the <title> of the page, for example
    • "description"
    • "author"
    • "twitterSite" - A reference to a Twitter handle (Just the id without "@")
    • "twitterCreator" - Another Twitter handle, refer to [Twitter cards markups](https://developer.twitter.com/en/docs/ tweets/optimize-with-cards/overview/markup)
    • "version"
    • "homepage"
    • "license"
    • "snapshotWidth" - default: 700 px
    • "snapshotHeight" - default: 350 px
    • "themeColor" - default: { "red": 15, "green": 85, "blue": 123 }
  • Add node dependencies with these commands

    npm install --save-dev chokidar-cli
    npm install --save-dev concurrently
    npm install --save-dev elm
    npm install --save-dev elm-go
    npm install --save-dev html-minifier
    npm install --save-dev puppeteer
    npm install --save-dev terser
  • Add src-elm-starter as an extra source-directory in elm.json, the same as in elm-starter/elm.json

  • Add these commands to package.json (or run them directly)

      "scripts": {
        "start":       "node ./src-elm-starter/starter.js start",
        "build":       "node ./src-elm-starter/starter.js build",
        "serverBuild": "node ./src-elm-starter/starter.js serverBuild"
      },

Done!

sandbox vs. element vs. document vs. application

In Elm there are several ways to start an application:

Among these, the first two take over only one specific node of the DOM, the other two instead take over the entire body of the page. So we need to follow two different strategies.

Case for sandbox & element

In the Index.elm you need to create a <div> that will be used to attach the Elm application.

In the elm-starter example we use a <div> with id elm. In Index.elm, see the line:

    ++ [ div [ id "elm" ] [] ]

Then we use this node to initialize Elm:

var node = document.getElementById('elm');
window.ElmApp = Elm.Main.init(
    { node: node
    , flags: ...you flags here...
    }

Then, to be sure that the node created by the static page generator is replaced later on, we need to add such <div> in the view of Main.elm like this:

view : Model -> Html.Html Msg
view model =
    Html.div
        [ Html.Attributes.id "elm" ]
        [ ...you content here... ]

Note: You can change the id from elm to anything you like.

Case for document & application

This case require a different approach. You can see an example in the elm-spa-example.

The main differences compared to the above approach are:

  • You don't need to create a specific <div> with id elm.
  • You need to move all Javascript section of the page into the htmlToReinjectInBody section (see Index.elm as example).

htmlToReinjectInBody will be injected after the page is generated during the build process assuring that the system will work also in this case.

Netlify

When setting up the app with Netlify, input these in the deploy configuration:

  • Build command: npm run build (or node ./src-elm-starter/starter.js start)
  • Publish directory: elm-stuff/elm-starter-files/build

Other CI/CD platforms

There are cases where some CI process is not able to install and use Puppeteer properly.

One of these cases was solved installing chromium manually running

RUN apk update && apk add --no-cache bash chromium

Then setting up some environment variables:

ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser

Then extra paramater were added to elm-starter

--no-sandbox
--disable-setuid-sandbox
--disable-dev-shm-usage

More info at https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-on-alpine

(*) Applications working without Javascript

Working without Javascript depends on the application. The elm-starter example works completely fine also without Javascript, the only missing thing is the smooth transition between pages.

The elm-todomvc example requires Javascript. Note that in this example, compared to Evan's original TodoMVC, I slightly changed the CSS to improve the a11y (mainly lack of contrast and fonts too small).

The elm-spa-example partially works without Javascript. You can browse across pages but the counters are not working.

elm-starter and elm-todomvc use Browser.element, while elm-spa-example use Browser.application.

The setup for these two cases is a bit different. Browser.application requires to use htmlToReinjectInBody (see Index.elm) because Elm is wiping out all the content in the body. Also the node where Elm attach itself needs to be removed (see node.remove() ).

The working folder of elm-starter is elm-stuff/elm-starter-files. These files are automatically generated and should not be edited directly, unless during some debugging process.

Advanced stuff

htmlToReinjectInHead and htmlToReinjectInBody

These two functions are used to re-inject some HTML after Puppeteer generated the page. This can be useful in several cases:

  • Using Browser.document or Browser.application (see above).
  • Using Google Tags, Google Analytics or other advanced JavaScript dependencies that should run only once. In these cases better not to add these JavaScript snipped during the development, but inject them using htmlToReinjectInHead only after the generation of HTML is done.

File generation

Disabling pre-rendering

Is possible to disable pre-rendering just passing an empty list to Main.conf.urls. In this case the app will work as "Full CSR" (Full Client-side Rendering)

This is an example of application with pre-rendering disabled. In this case also the WebGL animation caused some time out issue with puppeteer and it would not be necessary anyway to have this application render without Javascript as it based entirely on Javascript.

Not the message when Javascript is disabled:

elm-physics-example

How to customize your project

The main two places where you can change stuff are:

  • src/Index.elm
  • src/Main.elm (conf function)

elm-starter is opinionated about many things. If you want more freedom, you can change stuff in

  • src-elm-starter/**/*.elm

The reason Main.conf is inside Main.elm is so that it can exchange data. For example:

  • title: Main.conf -> Main
  • urls: Main.conf <- Main

Moreover Main.conf is used by src-elm-starter to generate all the static files.

elm-console-debug.js for nice console output

Support https://github.com/kraklin/elm-debug-transformer out of the box for nice Debug.log messages in the console.

Changing meta-tags

For better SEO, you should update meta-tags using the predefined port changeMeta that you can use this way:

Cmd.batch
    [ changeMeta { querySelector = "title", fieldName = "innerHTML", content = title }
    , changeMeta { querySelector = "meta[property='og:title']", fieldName = "content", content = title }
    , changeMeta { querySelector = "meta[name='twitter:title']", fieldName = "value", content = title }
    , changeMeta { querySelector = "meta[property='og:image']", fieldName = "content", content = image }
    , changeMeta { querySelector = "meta[name='twitter:image']", fieldName = "content", content = image }
    , changeMeta { querySelector = "meta[property='og:url']", fieldName = "content", content = url }
    , changeMeta { querySelector = "meta[name='twitter:url']", fieldName = "value", content = url }
    ]

You can validate Twitter preview cards at https://cards-dev.twitter.com/validator

elm-starter

Configuration

You can verify the configuration in real-time using elm reactor:

node_modules/.bin/elm reactor

and check the page

http://localhost:8000/src-elm-starter/Application.elm

Globally available objects

There are three global objects available

ElmStarter

ElmStarter contain metadata about the app:

{ commit: "abf04f3"   // coming from git
, branch: "master"    // coming from git
, env: "dev"          // can be "dev" or "prod"
, version: "0.0.5"    // coming from package.json
}

This data is also available in Elm through Flags.

ElmApp

ElmApp is another global object that contains the handle of the Elm app.

Elm

This is the object exposed by the compiler used to initialize the application.

Website living in subfolder

If your website need to live in a subfolder, including all the assets, simply specify the subfolder in homepage of package.json.

    "homepage": "https://example.com/your/sub/folders",

This will adjust all the links for you automatically. So for example, elm.js instead being loaded as

<script src="/elm.js"></script>

It will be loaded as

<script src="/your/sub/folders/elm.js"></script>

Remember to account for these extra folders in your Elm route parser.

Extra commands

To avoid possible conflicts with other commands in package.json, al commands are also available with a prefix:

$ npm run elm-starter:boot
$ npm run elm-starter:start
$ npm run elm-starter:generateDevFiles
$ npm run elm-starter:build
$ npm run elm-starter:buildExpectingTheServerRunning
$ npm run elm-starter:serverBuild
$ npm run elm-starter:serverDev
$ npm run elm-starter:serverStatic
$ npm run elm-starter:watchStartElm

This list also include extra commands that can be useful for debugging

Limitations

  • Javascript and CSS to generate the initial index.html are actually strings :-(
  • src-elm-starter/starter.js, the core of elm-starter, is ~330 lines of Javascript. I wish it could be smaller
  • If your Elm code relies on data only available at runtime, such as window size or dark mode, prerendering is probably not the right approach. In this case you may consider disabling pre-rendering and use other alternatives, such as Netlify prerendering

Note

  • The smooth rotational transition in the demo only works in Chrome. I realized it too late, but you get the picture

How does it work internally

Command start

* [function bootstrap]
    * Set initial values for some folder and file names
    * Compile `Worker.elm`
    * Execute `Worker.elm` using data set initially and other data coming from `package.json` and command line options
    * `Worker.elm` generates a configuration file that will be used for the next tasks. A copy of it is accessible at http://localhost:8000/conf.json
* [function command_start]
    * [function command_generateDevFiles]
        * Clean the `developmennt` working folder
        * Generate files as per `conf.json` and also copy over assets
        * Touch `src/Main.elm` so that `elm-go` recompile and the browser refresh
    * Starts two commands: `serverDev` and `watchStartElm` (see below)

Command serverDev

* [function bootstrap] (see above)
* [function command_serverDev]
    * start `elm-go` at http://localhost:8000

Command watchStartElm

* [function bootstrap] (see above)
* Watch all Elm files in `elm-starter` folder and `src/Index.elm` for modifications
* If there is a change, it run the command `generateDevFiles` (see below)

Command generateDevFiles

* [function bootstrap] (see above)
* [function command_generateDevFiles] (see above)

..TO BE CONTINUED

Non-goals

Things that elm-starter is not expected to do

  • Doesn't generate Elm code automatically, like Route-parser, for example
  • Doesn't touch/wrap the code of your Elm Application
  • Doesn't do live SSR (Server Side Render) but just pre-render during the build
  • Doesn't change the Javascript coming out from the Elm compiler
  • Doesn't create a web site based on static files containing Markdown
  • There is no "hydration", unless Elm does some magic that I am not aware of.

You can find several of these characteristics in some of the similar projects.

Using as reference the table at the bottom of the article Rendering on the Web, elm-starter can support you in these rendering approach

  • Static SSR
  • CSR with Prerendering
  • Full CSR

It cannot help you with

  • Server Rendering
  • SSR with (re)hydration

Similar projects

These are other projects that can be used to bootstrap an Elm application or to generate a static site:

Here are similar projects for other Languages/Frameworks: