Pull in typed Elm data to your pages

Whether your data is coming from markdown files, APIs, a CMS, or all of the above, elm-pages lets you pull in just the data you need for a page. No loading spinners, no Msg or update logic, just define your data and use it in your view.

module Route.Repo.Name_ exposing (Data, Model, Msg, route)
type alias Data = Int
type alias RouteParams = { name : String }
route : StatelessRoute RouteParams Data ActionData
route =
RouteBuilder.preRender
{ head = head
, pages = pages
, data = data
}
|> RouteBuilder.buildNoState { view = view }
pages : BackendTask error (List RouteParams)
pages =
BackendTask.succeed [ { name = "elm-pages" } ]
data : RouteParams -> BackendTask FatalError Data
data routeParams =
BackendTask.Http.getJson
"https://api.github.com/repos/dillonkearns/elm-pages"
(Decode.field "stargazer_count" Decode.int)
|> BackendTask.allowFatal
view :
App Data ActionData RouteParams
-> View Msg
view app =
{ title = app.routeParams.name
, body =
[ h1 [] [ text app.routeParams.name ]
, p [] [ text ("Stars: " ++ String.fromInt app.data) ]
]
}

Combine data from multiple sources

Wherever the data came from, you can transform BackendTasks and combine multiple BackendTasks using the full power of Elm's type system.

type alias Project =
{ name : String
, description : String
, stars : Int
}
all : BackendTask FatalError (List Project)
all =
Glob.succeed
(\projectName filePath ->
BackendTask.map3 Project
(BackendTask.succeed projectName)
(BackendTask.File.rawFile filePath BackendTask.File.body |> BackendTask.allowFatal)
(stars projectName)
)
|> Glob.match (Glob.literal "projects/")
|> Glob.capture Glob.wildcard
|> Glob.match (Glob.literal ".txt")
|> Glob.captureFilePath
|> Glob.toBackendTask
|> BackendTask.allowFatal
|> BackendTask.resolve
stars : String -> BackendTask FatalError Int
stars repoName =
Decode.field "stargazers_count" Decode.int
|> BackendTask.Http.getJson ("https://api.github.com/repos/dillonkearns/" ++ repoName)
|> BackendTask.allowFatal

SEO

Make sure your site previews look polished with the type-safe SEO API. `elm-pages build` pre-renders HTML for your pages. And your SEO tags get access to the page's BackendTasks.

head :
App Data ActionData RouteParams
-> List Head.Tag
head app =
Seo.summaryLarge
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = app.data.image
, alt = app.data.description
, dimensions = Nothing
, mimeType = Nothing
}
, description = app.data.description
, locale = Nothing
, title = app.data.title
}
|> Seo.article
{ tags = []
, section = Nothing
, publishedTime = Just (Date.toIsoString app.data.published)
, modifiedTime = Nothing
, expirationTime = Nothing
}

Full-Stack Elm

With server-rendered routes, you can seamlessly pull in user-specific data from your backend and hydrate it into a dynamic Elm application. No API layer required. You can access incoming HTTP requests from your server-rendered routes, and even use the Session API to manage key-value pairs through signed cookies.

module Route.Feed exposing (ActionData, Data, Model, Msg, RouteParams, route)
type alias RouteParams = {}
type alias Data =
{ user : User
, posts : List Post
}
data :
RouteParams
-> Request
-> BackendTask FatalError (Response Data ErrorPage)
data routeParams request =
request
|> withUserOrRedirect
(\user ->
BackendTask.map (Data user)
(BackendTask.Custom.run "getPosts"
(Encode.string user.id)
(Decode.list postDecoder)
|> BackendTask.allowFatal
)
|> BackendTask.map Server.Response.render
)
withUserOrRedirect :
(User -> BackendTask FatalError (Response Data ErrorPage))
-> Request
-> BackendTask FatalError (Response Data ErrorPage)
withUserOrRedirect withUser request =
request
|> Session.withSession
{ name = "session"
, secrets =
BackendTask.Env.expect "SESSION_SECRET"
|> BackendTask.allowFatal
|> BackendTask.map List.singleton
, options = Nothing
}
(session ->
session
|> Session.get "sessionId"
|> Maybe.map getUserFromSession
|> Maybe.map (BackendTask.andThen withUser)
|> Maybe.withDefault (BackendTask.succeed (Route.redirectTo Route.Login))
|> BackendTask.map (Tuple.pair session)
)
getUserFromSession : String -> BackendTask FatalError User
getUserFromSession sessionId =
BackendTask.Custom.run "getUserFromSession"
(Encode.string sessionId)
userDecoder
|> BackendTask.allowFatal
view :
App Data ActionData RouteParams
-> Shared.Model
-> Model
-> View (PagesMsg Msg)
view app shared model =
{ title = "Feed"
, body =
[ navbarView app.data.user
, postsView app.data.posts
]
}

Forms Without the Wiring

elm-pages uses progressively enhanced web standards. The Web has had a way to send data to backends for decades, no need to re-invent the wheel! Just modernize it with some progressive enhancement. You define your Form and validations declaratively, and elm-pages gives you client-side validations and state with no Model/init/update wiring whatsoever. You can even derive pending/optimistic UI from the in-flight form submissions (which elm-pages manages and exposes to you for free as well!).

module Route.Signup exposing (ActionData, Data, Model, Msg, RouteParams, route)
type alias Data = {}
type alias RouteParams = {}
type alias ActionData = { errors : Form.Response String }
route : RouteBuilder.StatefulRoute RouteParams Data ActionData Model Msg
route =
RouteBuilder.serverRender { data = data, action = action, head = head }
|> RouteBuilder.buildNoState { view = view }
type alias ActionData =
{ errors : Form.Response String }
view :
App Data ActionData RouteParams
-> Shared.Model
-> View (PagesMsg Msg)
view app shared =
{ title = "Sign Up"
, body =
[ Html.h2 [] [ Html.text "Sign Up" ]
-- client-side validation wiring is managed by the framework
, Form.renderHtml "signup" [] (Just << .errors) app () signUpForm
]
}
data :
RouteParams
-> Request
-> BackendTask FatalError (Response Data ErrorPage)
data routeParams request =
BackendTask.succeed (Response.render {})
head : RouteBuilder.App Data ActionData RouteParams -> List Head.Tag
head app =
[]
action :
RouteParams
-> Request
-> BackendTask FatalError (Response ActionData ErrorPage)
action routeParams request =
case request |> Request.formData formHandlers of
Just ( response, parsedForm ) ->
case parsedForm of
Form.Valid (SignUp okForm) ->
BackendTask.Custom.run "createUser"
-- client-side validations run on the server, too,
-- so we know that the password and password-confirmation matched
(Encode.object
[ ( "username", Encode.string okForm.username )
, ( "password", Encode.string okForm.password )
]
)
(Decode.succeed ())
|> BackendTask.allowFatal
|> BackendTask.map (\() -> Response.render { errors = response })
Form.Invalid _ _ ->
"Error!"
|> Pages.Script.log
|> BackendTask.map (\() -> Response.render { errors = response })
Nothing ->
BackendTask.fail (FatalError.fromString "Expected form submission."
errorsView :
Form.Errors String
-> Validation.Field String parsed kind
-> Html (PagesMsg Msg)
errorsView errors field =
errors
|> Form.errorsForField field
|> List.map (\error -> Html.li [ Html.Attributes.style "color" "red" ] [ Html.text error ])
|> Html.ul []
signUpForm : Form.HtmlForm String SignUpForm input Msg
signUpForm =
(\username password passwordConfirmation ->
{ combine =
Validation.succeed SignUpForm
|> Validation.andMap username
|> Validation.andMap
(Validation.map2
(\passwordValue passwordConfirmationValue ->
if passwordValue == passwordConfirmationValue then
Validation.succeed passwordValue
else
Validation.fail "Must match password" passwordConfirmation
)
password
passwordConfirmation
|> Validation.andThen identity
)
, view =
\formState ->
let
fieldView label field =
Html.div []
[ Html.label
[]
[ Html.text (label ++ " ")
, Form.FieldView.input [] field
, errorsView formState.errors field
]
]
in
[ fieldView "username" username
, fieldView "Password" password
, fieldView "Password Confirmation" passwordConfirmation
, if formState.isTransitioning then
Html.button
[ Html.Attributes.disabled True ]
[ Html.text "Signing Up..." ]
else
Html.button [] [ Html.text "Sign Up" ]
]
}
)
|> Form.init
|> Form.hiddenKind ( "kind", "regular" ) "Expected kind."
|> Form.field "username" (Field.text |> Field.required "Required")
|> Form.field "password" (Field.text |> Field.password |> Field.required "Required")
|> Form.field "password-confirmation" (Field.text |> Field.password |> Field.required "Required")
type Action
= SignUp SignUpForm
type alias SignUpForm =
{ username : String, password : String }
formHandlers : Form.ServerForms String Action
formHandlers =
Form.initCombined SignUp signUpForm