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:
These are three simple examples of websites built with elm-starter
:
- https://elm-starter.guupa.com/ (Code)
- https://elm-todomvc.guupa.com/ (Code)
- https://elm-spa-example.guupa.com/ (Code)
- https://elm-physics-example.guupa.com/ (Code)
- 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:
Slack's previews (note how different urls have different snapshot and meta-data):
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.
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:
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.
Builds the app for production to the elm-stuff/elm-starter-files/build
folder.
Launches a server in the build
folder.
Open http://localhost:9000 to view it in the browser.
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/
tomy-elm-app/src-elm-starter/
-
Copy the file
elm-starter/src/Index.elm
tomy-elm-app/src/Index.elm
-
Copy the function
conf
fromelm-starter/src/Main.elm
tomy-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 commandsnpm 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 extrasource-directory
inelm.json
, the same as inelm-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!
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.
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.
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 idelm
. - 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.
When setting up the app with Netlify, input these in the deploy configuration:
- Build command:
npm run build
(ornode ./src-elm-starter/starter.js start
) - Publish directory:
elm-stuff/elm-starter-files/build
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
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.
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
orBrowser.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.
- Most of the logic is written in Elm, including the code to generate all necessary files:
index.html
(generated fromIndex.elm
usingzwilias/elm-html-string
)sitemap.txt
manifest.json
service-worker.js
robots.txt
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:
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.
Support https://github.com/kraklin/elm-debug-transformer out of the box for nice Debug.log
messages in the console.
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
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
There are three global objects available
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
is another global object that contains the handle of the Elm app.
This is the object exposed by the compiler used to initialize the application.
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.
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
- Javascript and CSS to generate the initial
index.html
are actually strings :-( src-elm-starter/starter.js
, the core ofelm-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
* [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)
* [function bootstrap] (see above)
* [function command_serverDev]
* start `elm-go` at http://localhost:8000
* [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)
* [function bootstrap] (see above)
* [function command_generateDevFiles] (see above)
..TO BE CONTINUED
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
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: