Pages & routing
Source code: GitHub
This next guide will show you how pages, routing, and the elm-spa add command work together to automatically handle URLs in your elm-spa application.
The setup
Just like with the last guide, we can use elm-spa new and elm-spa server to get a brand new elm-spa project up and running:
elm-spa new
This generates the "Hello, world!" homepage from before:
elm-spa server

Adding a static page
elm-spa add /static static
This command adds a page at http://localhost:1234/static with the static template. This is similar to Home_.elm, but it has access to Shared.Model and Request in case we need data from either of those.
Here is the complete Static.elm file:
module Pages.Static exposing (page) import Page exposing (Page) import Request exposing (Request) import Shared import View exposing (View) page : Shared.Model -> Request -> Page page shared req = Page.static { view = view } view : View msg view = View.placeholder "Static"
The View.placeholder function just stubs out the view function with an empty page that only renders "Static" in the browser.
Visit http://localhost:1234/static to see it in action!
Making a layout
Before we continue, I want to make a layout with a navbar so that we can easily navigate between pages without manually editing the URL.
I'll create a file at src/UI.elm that looks like this:
module UI exposing (layout) import Html exposing (Html) import Html.Attributes as Attr layout : List (Html msg) -> List (Html msg) layout children = let viewLink : String -> String -> Html msg viewLink label url = Html.a [ Attr.href url ] [ Html.text label ] in [ Html.div [ Attr.class "container" ] [ Html.header [ Attr.class "navbar" ] [ viewLink "Home" "/" , viewLink "Static" "/static" ] , Html.main_ [] children ] ]
Using the layout in a page
Because it works from one List (Html msg) to another, we can add UI.layout in front of the body list on both pages:
-- src/Pages/Home_.elm view : View msg view = { title = "Homepage" , body = UI.layout [ Html.text "Homepage" ] }
-- src/Pages/Static.elm view : View msg view = { title = "Static" , body = UI.layout [ Html.text "Static" ] }
Use routes, not strings
In src/UI.elm, we had a function for rendering our navbar links that looked like this:
viewLink : String -> String -> Html msg viewLink label url = Html.a [ Attr.href url ] [ Html.text label ]
This function works great– but it's possible to provide a URL that our application doesn't have!
[ viewLink "Home" "/" , viewLink "Static" "/satic" ]
Here, I mistyped the URL /satic, but the compiler didn't warn me about it! Let's use the Route values generated by elm-spa to improve this experience:
import Gen.Route as Route exposing (Route) viewLink : String -> Route -> Html msg viewLink label route = Html.a [ Attr.href (Route.toHref route) ] [ Html.text label ]
By using the Gen.Route module from .elm-spa/generated, we can pass in a Route instead of a String:
[ viewLink "Home" Route.Home_ , viewLink "Static" Route.Static ]
This will prevent typos, but more importantly it allows the Elm compiler to remind us to update the navbar in case we remove Home_.elm or Static.elm in the future.
Deleting either of those pages changes the generated Gen.Route module, so the compiler can let us know that our UI.layout function has a broken link– before our users do!
Adding CSS
In UI.layout, we used Attr.class to provide our HTML with some CSS classes:
Html.div [ Attr.class "container" ] [ Html.header [ Attr.class "navbar" ] [ viewLink "Home" Route.Home_ , viewLink "Static" Route.Static ] ]
The container and navbar classes are used in our code, but not defined in a CSS file. Let's fix that by creating a new CSS file at public/style.css:
.container { max-width: 960px; margin: 1rem auto; } .navbar { display: flex; align-items: center; } .navbar a { margin-right: 16px; }
After creating style.css, we can import the file in our public/index.html entrypoint:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- import our new CSS file --> <link rel="stylesheet" href="/style.css"> </head> <body> <script src="/dist/elm.js"></script> <script> Elm.Main.init() </script> </body> </html>
Using the <link> tag as shown above (with the leading slash!) imports our CSS file. All files in the public folder are available at the root of our web application. That means a file stored at public/images/dog.png would be at http://localhost:1234/images/dog, without including public in the URL at all.
Adding more page types
elm-spa add /sandbox sandbox
elm-spa add /element element
elm-spa add /advanced advanced
These commands add in the other three page types described in the pages guide.
For each page, the View.placeholder function stubs out the view functions so you can visit them in the browser.
For example, http://localhost:1234/element should display "Element" on the screen.
Adding some dynamic routes
To add in dynamic routes, we can use elm-spa add again:
elm-spa add /dynamic/:name static
With this command, we just created a page at src/Pages/Dynamic/Name_.elm. When a user visits a URL like /dynamic/ryan or dynamic/123, we'll be taken to this page.
Let's tweak the default view function to render the dynamic name parameter from the URL:
-- src/Pages/Dynamic/Name_.elm view : Params -> View msg view params = { title = "Dynamic: " ++ params.name , body = UI.layout [ UI.h1 "Dynamic Page" , Html.h2 [] [ Html.text params.name ] ] }
We can provide in the req.params to the view function by telling our page function to pass it along:
page : Shared.Model -> Request.With Params -> Page page _ req = Page.static -- 👇 we pass in params here { view = view req.params }
Updating the navbar
Once we wire up these pages to use UI.layout, we can add links to the navbar:
-- src/UI.elm import Gen.Route as Route layout : List (Html msg) -> List (Html msg) layout children = let viewLink : String -> Route -> Html msg viewLink label route = Html.a [ Attr.href (Route.toHref route) ] [ Html.text label ] in [ Html.div [ Attr.class "container" ] [ Html.header [ Attr.class "navbar" ] [ Html.strong [ Attr.class "brand" ] [ viewLink "Home" Route.Home_ ] , viewLink "Static" Route.Static , viewLink "Sandbox" Route.Sandbox , viewLink "Element" Route.Element , viewLink "Advanced" Route.Advanced , Html.div [ Attr.class "splitter" ] [] , viewLink "Dynamic: Apple" (Route.Dynamic__Name_ { name = "apple" }) , viewLink "Dynamic: Banana" (Route.Dynamic__Name_ { name = "banana" }) ] , Html.main_ [] children ] ]
That's it!
Feel free to play around with the elm-spa add command to mix-and-match different pages.
As always, the source code for this example is available on GitHub
Next up: Storage