I’m very excited to share a project that I’ve been hard at work on for quite a while now. I hope you find it useful!
elm-pages v3 introduces two major new areas that open up a lot of new use cases: server-rendered routes, and scripts. You can read all about it in the announcement post here, and in the latest Elm package docs and the v3 docs site.
If you try it out, I’d love to hear from you! Also, feel free to join us in the #elm-pages Slack channel to ask questions or share what you build. Happy coding!
Thank you Martin! It was a real challenge to get everything to come together in this big release, I’m very relieved to ship it now. It was especially hard because the design of one part depended so much on everything else, so just shipping one small piece could end up with a local optimization in the design that results in a sub-optimal design for the overall framework. In the end, I just worked at it until I had removed all of the asterisks and felt that it was really nice to use. Several times that meant throwing a design away and starting it from scratch even. But I’m very grateful that I got to put in the time to make that happen!
Some amount of funding has come from my GitHub sponsors, who I’m really grateful to for their support. The GitHub sponsorship currently covers the amount I pay for monthly fees for hosting and editing Elm Radio episodes, however. A company has also been helping to sponsor my work, which has made a big difference to help support my time on this project, and the rest I’m funding from my own savings.
This is going to be really cool. One less objection for anybody who says they want next.js features to adopt elm, should that become a demand.
OT: This triggered me reading again for some liveview alternative implemented in python (personal desire to combine sql_alchemy with no api approach), which led me back to reactpy…Overall, I wonder if anybody will hesitate about running a node based server for elm-pages-v3 with backendtask, or if people will just embrace prisma et al. It definitely makes sense to start with node for MVP.
It’s not the MVP, that’s the architecture of elm-pages. At some point it might become less Node-specific and more pluggable with other JS runtimes like Deno, but the JavaScript Backend is a core design decision that’s not going away. Elm compiles down to JS, so a JS-based Backend is a good pairing for running Elm because it needs to execute the compiled Elm code and interop through JS (it’s using ports to communicate even though it builds some abstractions to do that in a more high-level way).
Keep in mind that you can run anything you want from NodeJS, including directly executing low-level C bindings. There are also some frameworks that make it easier to create bindings from NodeJS to Rust, for example. So lots of great options for writing the Custom BackendTask definitions in languages other than JavaScript. You could even compile Python, or an increasing number of backend languages, to WASM, and execute that natively from the Node backend. So the sky’s the limit with what you can interop with.
Epic! This looks like it has been a wild amount of work.
I find all the server rendered stuff pretty mind bendy so I didn’t quite grok all of it through the documentation. Might take a few passes.
The Model declared in the examples, does this work like any typical model one might find in your standard Elm SPA? So I can provide non serialisable data like functions etc and that will work no problem?
Yeah, it’s a lot to take in! I have a docs page on “The elm-pages Architecture” (TEPA? ). It’s a wrapper around a regular Elm app, so the init/update/Model all works the same way. The Model is not serialized, so you can have any data types in there. Only Data and ActionData are serialized (the additions on top of the standard Elm Architecture).
So in a nutshell, an elm-pages app resolves data (and action if it’s a non-GET request) on the server. That data that was resolved on the Backend is then available when the Elm app hydrates. It’s exactly the same as if you have a regular Elm app with the pattern of passing in some initial data from the server as Flags to your app, just that it’s handled for you under the hood to make it more seamless and type-safe.
I hope that’s helpful! Happy to answer more questions on the architecture, and feedback on how to improve the docs is very welcome.
I don’t know v3 well, but I remember spending a while trying to really understand what happened under the hood of nextjs. IIRC, this section of react-static’s (react-static is a minimalist alternative to next) docs helped me understand a little better at that time - GitHub - react-static/react-static: ⚛️ 🚀 A progressive static site generator for React..
Yes, I would definitely recommend upgrading to v3 for static sites as well! You’ll get to use Vite for your non-Elm assets (including in the dev server), and you’ll get the benefit of automatic serialization (no OptimizedDecoders, and a more efficient binary format). Lots of other small improvements, and in general being on the latest version will be better for future updates.
For upgrading, I would recommend these steps:
Use the elm-pages init command (or elm-pages-starter repo) to get the shell for a v3 app
Copy any configuration over to the v3 shell and get the basic boilerplate working with no errors with your baseline configuration for your app ported over
Modify the script/src/AddRoute.elm Script according to your conventions of your app (set it up to use the view library you use, like elm-css, etc., and if your site is all static routes then have it use Scaffold.Route.preRender instead of serverRender)
Use npx elm-pages run AddRoute <Module.Name> to add each route, and make sure that your routing works the same as your v2 app
Port your Route Modules’ data functions over one-by-one from your v2 code and get them compiling. If you have any non-serializable types (functions, or HTML), then those will need to be massaged into serializable types and then transformed into HTML or functions as needed from your view instead. You’ll also change OptimizedDecoder into a regular Json.Decode.Decoder, and rename DataSource to BackendTask, and add some BackendTask.allowFatal to migrate to the new BackendTask design.
Finally, copy your view, init, etc. into each of those routes.
You could rearrange and tweak those steps, but hopefully that gives you a sense of what’s involved and how to tackle the upgrade. Feel free to share feedback on those steps if you find anything that’s missing or could be improved. Hope that’s helpful, and hope you enjoy v3!
Yes, what happens is that elm-pages takes the Route Modules that you define and it compiles two versions of your app. One that runs on the Backend (that will execute during your build step, or on the server for server-rendered Routes), and one that runs on the Frontend.
Backend app
Resolves data for the given URL (uses ports under the hood to communicate with NodeJS as needed to read files, etc.)
Takes the resolved Route Module’s Data and calls the Route Module’s init. Throws away the Effect, it only needs the Model
Calls the Route Module’s view function with the Model from init and the resolved Data
Turns the return value of the Route Module’s view into the final HTML to be rendered using the Shared.view function, and adding the surrounding HTML for an elm-pages skeleton (<script> tags, <style> tags, etc.). It also includes a base64 encoded version of the binary encoded version of the Route Module’s Data.
Frontend app
The initial paint of the page is from the HTML (no JS/Elm) that was rendered out on the server as described above
After the initial paint is all done, elm-pages passes in this serialized data in as an Elm Flag, then renders the Route Module for the given URL. This time, init is called and the Effect is not thrown away but is actually performed. However, data is not performed (it is only ever performed in the Backend, as the name BackendTask implies). data is already available through the Flags that elm-pages managed under the hood for us.
Any subsequent page navigations will go and reach out to the Backend to resolve the Data for that URL. Since the rendered app is a single-page app (SPA), it does not do a full page load (no HTML is downloaded), instead it only downloads the binary serialized form of the Route Module’s Data. If it is a pre-rendered Route Module, then this is just loading a static file from a CDN. If it is a server-rendered route, then the server returns that binary content and resolves it at request-time.
Hope that is helpful! Let me know if there’s anything else that’s fuzzy about it. I would love to help demystify these things in the docs, so questions and pointers about which parts are unclear are very helpful!
So the sky’s the limit with what you can interop with.
What do you think the most popular Custom BackendTask language will be after js/ts? I started glancing at node + rust + wasm, after your prompting. I am guessing I can also find python and go examples.
Node is quite general purpose, so I would frame it more as a tool to get JSON back-and-forth between BackendTask.Custom.run (through the Node definitions in custom-backend-task.ts) in an elm-pages app. However you get that JSON data is up to you.
I imagine the vast majority of elm-pages users are using NodeJS, but again whatever you want to run in NodeJS that results in some JSON data will work, so it’s really up to you. I don’t see it as much of an elm-pages-specific question, it’s more of a question of nice ways to get JSON data into NodeJS. If you search on that topic you’ll find a lot more resources and tools to help with that, and whatever you come up with there will be an option for your elm-pages Custom BackendTask definitions.