Deploy an Elm Frontend to Cloudflare Pages
While experimenting with the Cloudflare platform, I wrote a small Elm frontend which gets some data from a Workers hosted backend and renders it. This was very much a toy project to understand how these tools can fit together. Here I describe how I deployed this Elm frontend to Cloudflare Pages.
(In an earlier incarnation of the work, I served HTMX from the Workers based backend, but this had issues serving go-echarts based content and ultimately I felt this solution resulted in a very unclear frontend/backend distinction; for this reason, I started looking at a dedicated Elm frontend).
As Elm is designed specifically for frontend use and can compile to static content, it’s a good fit for Cloudflare Pages and quite a compelling proposition with Cloudflare Pages being free. However, there is little documentation on how to deploy Elm applications to Cloudflare.
I used the elm-land
framework to build my simple app - it only has a couple
of pages and pulls data from an OpenAPI-compatible API. When working in
development mode, it’s straightforward to run elm-land server
to serve the
frontend while working on it.
To deploy the application, it’s first necessary to build a production version
of it by compiling the Elm application to static HTML/Javascript. elm-land build
can be used to do this - it generates the static frontend in the dist
directory.
🕙 15:38:19 ❯ elm-land build
🌈 Elm Land (v0.19.5) build was successful.
⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺
To deploy the frontend, the Cloudflare
wrangler
toolchain was
used. (See
here
if you require instructions on setting up wrangler
). Create a wrangler.toml
file so wrangler
knows how to deploy the frontend.
name = "<YOUR-APPLICATION-NAME>"
pages_build_output_dir = "./dist"
compatibility_date = "2024-08-19"
[dev]
port = 8989
local_protocol = "http"
This tells wrangler
where the content is located and how to serve the content
when run locally.
🕙 15:38:08 ❯ wrangler pages dev
⛅️ wrangler 3.68.0 (update available 3.72.0)
-------------------------------------------------------
No Functions. Shimming...
[wrangler:inf] Ready on http://localhost:8989
⎔ Starting local server...
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ [b] open a browser, [d] open Devtools, [c] clear console, [x] to exit │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
A version of frontend is served locally on port 8989
(as specified the
wrangler.toml
); it can then be accessed on http://localhost:8989
from a
browser.
Satisfied that the frontend is behaving as expected, deploying to Cloudflare Pages is as simple as:
🕙 15:45:03 ❯ wrangler pages deploy
🌍 Uploading... (3/3)
✨ Success! Uploaded 2 files (1 already uploaded) (2.36 sec)
🌎 Deploying...
✨ Deployment complete! Take a peek over at https://<REDACTED>.pages.dev
Hit the endpoint above and you will see your application hosted on Cloudflare Pages.
Differentiating between development and production deployments
A common issue when deploying frontends is to be able to point a development frontend at, say, a staging API, while a production frontend must point to a production API. Hence, a mechanism is required to be able to specify such differences in deployments and specifically, in this case, which backend should be used by the frontend.
Elm has support for this with some documentation highlighting this specific scenario; however, not all the details were clear to me so I note them here.
The basic mechanism requires use of Environment Variables (see
here).
Environment variables can be passed to Elm from Javascript, where they can be
picked up from the runtime using process.env
standard mechanisms provided by
node
. Using this mechanism, it’s possible to specify an environment variable
when running the development server as follows:
$ API_ENDPOINT="http://my.test.endpoint" elm-land server
and this can be accessed via Flags
in the Elm application.
That works fine when running the development server which somehow has a runtime; however, Cloudflare Pages only supports serving of static content which does not provide a runtime - the content is run in the browser and does not have access to environment variables from the serving environment. Consequently, these parameters must be embedded in the static content.
elm-land
has support for embedding environment variables into the static
assets. It supports reading key-value pairs from a .env
file and embedding
these in the static content such that they can be accessed via process.env
and passed to the Elm application. (As these variables are embedded in the
static content, no secrets should be included via this mechanism).
Note that there is some specific, non-obvious, detail here - environment
variables are read from the .env
file; the prefix ELM_APP_
is applied to
these variables, and they are statically included within the frontend. This
prefix is then stripped from these variables when the frontend code runs in the
browser. It’s not absolutely necessary to know this detail, but it is
documented in some places on the internet and it was not clear to whether I
needed to specify environment variables with this ELM_APP_
prefix or not - I didn’t.
Tidying up
The above can be used to serve the frontend via a pages.dev
domain; usually
you need to add a Custom Domain to serve it from your domain.
You probably need to perform some CORS configuration on your API to enable content to be served to the frontend.
If you’re feeling brave, you can set up continuous deployment. Cloudflare Pages does not support Elm builds by default (supported frameworks are listed here) so it would probably be necessary to set up a github action which performs the build and pushes to Cloudflare - perhaps a topic for another post…