Update docs.

This commit is contained in:
Dillon Kearns 2023-05-17 11:40:08 -07:00
parent a010d93a9d
commit d873003ef8
3 changed files with 151 additions and 94 deletions

View File

@ -1,73 +0,0 @@
---
description: Understanding BackendTasks
---
# `BackendTask`s
It doesn't matter _where_ a `BackendTask` came from.
For example, if you have
```elm
type alias Author =
{ name : String
, avatarUrl : String
}
authors : BackendTask (List Author)
```
It makes no difference where that data came from. In fact, let's define it as hardcoded data:
```elm
hardcodedAuthors : BackendTask (List Author)
hardcodedAuthors =
BackendTask.succeed [
{ name = "Dillon Kearns"
, avatarUrl = "/avatars/dillon.jpg"
}
]
```
We could swap that out to get the data from another source at any time. Like this HTTP BackendTask.
```elm
authorsFromCms : BackendTask (List Author)
authorsFromCms =
BackendTask.Http.get (Secrets.succeed "mycms.com/authors")
authorsDecoder
```
Notice that the type signature hasn't changed. The end result will be data that is available when our page loads.
In fact, let's combine our library of authors from 3 different `BackendTask`s.
```elm
authorsFromFile : BackendTask (List Author)
authorsFromFile =
BackendTask.File.jsonFile "data/authors.json"
authorsDecoder
allAuthors : BackendTask (List Author)
allAuthors =
BackendTask.map3 (\authors1 authors2 authors3 ->
List.concat [ authors1, authors2, authors3 ]
)
authorsFromFile
authorsFromCms
hardcodedAuthors
```
So how does the data get there? Let's take a look at the lifecycle of a BackendTask.
## The `BackendTask` Lifecycle
A `BackendTask` is split between two phases:
1. Build step - build up the data for a given page
2. Decode the data - it's available without reading files or making HTTP requests from the build step
That means that when we run `elm-pages build`, then deploy the HTML and JSON output from the build to a CDN, it will not hit `mycms.com/authors` anymore.
So when a user goes to your site, they won't hit your CMS directly. Instead, when they load the page it will include all of the data that we used for that specific page
in the initial load. That's how `elm-pages` can skip the loading spinner for an HTTP data source - it builds the data into the page at build-time.

View File

@ -1,21 +0,0 @@
---
description: Error Handling
---
# Error Handling
1. Have a `FatalError` from data: https://github.com/dillonkearns/elm-pages-v3-beta/blob/49e5e26fd1aecf26c04006464e2252ff459385dd/examples/pokedex/app/Route/ErrorHandling.elm#L46-L51
2. Customize rendering of the InternalError variant in your ErrorPage to your liking: https://github.com/dillonkearns/elm-pages-v3-beta/blob/49e5e26fd1aecf26c04006464e2252ff459385dd/examples/pokedex/app/ErrorPage.elm#L124-L132
3. Custom 500 errors! https://mellow-scone-524810.netlify.app/error-handling
You can do different kinds of errors as well, like 401 or 403 or 404 error pages, but these are expected errors, not "something went really wrong" FatalError's.
```elm
Rendering a 404 looks like this for example:
Request.succeed
(BackendTask.succeed
(Response.errorPage ErrorPage.NotFound)
)
```

View File

@ -0,0 +1,151 @@
# Error Pages
In your server-rendered routes, you can choose to short-circuit rendering your route and instead render an error page. This is useful for things like 404 pages, or 500 pages, or even custom error pages for specific errors. In order to render your route, you must resolve to the `Data` type in your Route module with [Server.Response.render](https://package.elm-lang.org/packages/dillonkearns/elm-pages-v3-beta/latest/Server-Response#render).
For example, let's say you have the following Route module:
```elm
module Route.User.UserId_ exposing (..)
import UserProfile exposing (UserProfile)
type alias Data = { profile : UserProfile }
data :
RouteParams
-> Server.Request.Parser (BackendTask.BackendTask FatalError.FatalError (Server.Response.Response Data ErrorPage.ErrorPage))
data routeParams =
Server.Request.succeed
(UserProfile.find routeParams.userId
|> BackendTask.map
(\maybeProfile ->
case maybeProfile of
Just foundProfile ->
Server.Response.render
{ profile = profile }
Nothing ->
Server.Response.errorPage ErrorPage.NotFound
)
)
```
In this example, we are attempting to lookup a user profile by id. If we find the profile, we render the route. If we don't find the profile, we render a 404 error page. A good rule of thumb is that if you are able to successfully resolve the `Data` for your Route, use `Server.Response.render`. If you are unable to resolve the `Data` for your Route, use [`Server.Response.errorPage`](https://package.elm-lang.org/packages/dillonkearns/elm-pages-v3-beta/latest/Server-Response#errorPage).
## Custom Error Types
Your app must define a module `app/ErrorPage.elm` with a type exposed called `ErrorPage`. However, you can define the `ErrorPage` type with custom error pages and data specific to rendering different error cases.
For example, you might have a generic 404 and 500 page, but in addition to that you might want to define an ErrorPage for viewing a paid resource that the user doesn't have access to. You might define your `ErrorPage` type like this:
```elm
type PlanStatus
= FreeTrialExpired
| ProPlanExpired
| NotLoggedIn
| NotSubscribed
type ErrorPage
= NotFound
| InternalError String
| PaywallAccessError { resource : PaidResource, planStatus : PlanStatus }
```
Then you could render the error page like this:
```elm
data :
RouteParams
-> Server.Request.Parser (BackendTask.BackendTask FatalError.FatalError (Server.Response.Response Data ErrorPage.ErrorPage))
data routeParams =
withProAccess
(\access ->
case access of
Ok () ->
resolvePageData routeParams
Err planStatus ->
Server.Response.errorPage
(ErrorPage.PaywallAccessError
{ resource = routeParams.resource
, planStatus = planStatus
}
)
)
```
This pattern allows us to use `BackendTask`'s to resolve data such as the plan status (this might involve a database request or API call to check for the current user's status), and then pass that data through to be rendered via our `ErrorPage` type. That means that we can resolve data specific to the `ErrorPage` while still short-circuiting our `Route` rendering and **not** resolving our Route's `Data` type.
## Stateful Error Pages
`ErrorPage`'s have access to a self-contained Elm Architecture (Model/view/update), so you can make interactive `ErrorPage`'s.
```elm
type Msg
= Increment
type alias Model =
{ count : Int
}
init : ErrorPage -> ( Model, Effect Msg )
init errorPage =
( { count = 0 }
, Effect.none
)
update : ErrorPage -> Msg -> Model -> ( Model, Effect Msg )
update errorPage msg model =
case msg of
Increment ->
( { model | count = model.count + 1 }, Effect.none )
view : ErrorPage -> Model -> View Msg
view error model =
div []
[ button [ onClick Increment ] []
]
```
## `FatalError`'s
You may not want to explicitly handle every possible error case and resolve it to an `ErrorPage` type for unexpected corner cases. For example, if you depend on an API to render your Route and don't expect it to fail, or can't do anything meaningful except for showing an error page if it fails, you can resolve to a `FatalError`. Note that a pre-rendered static route will fail the build if it resolves to a `FatalError`, resulting in a debugging error message displayed in the console - `FatalError`'s are a great tool for static routes because you can prevent bad data going live to the site and give yourself the opportunity to retry or fix the build when rare edge cases occur.
With server-rendered routes, when your Route module's `data` resolves to a `FatalError`, it will render your `ErrorPage.internalError` page. You can customize how your internal error page is rendered, but the downside is that it will render a generic error page with a String for context without giving you the opportunity to pass through meaningful context (use `Server.Response.errorPage` if you want to pass through meaningful context).
Here's an example of how you can use `FatalError`'s:
```elm
callMyApi : RouteParams -> BackendTask Never (Result Error ApiResponse)
callMyApi = -- ...
data : RouteParams -> Parser (BackendTask FatalError (Response Data ErrorPage))
data routeParams =
Request.succeed
(callMyApi routeParams
|> BackendTask.map (\response ->
case response of
Ok apiResponse ->
renderMyPage apiResponse
Err error ->
BackendTask.fail
(FatalError.fromString "Error accessing API, please try again")
)
)
```
It's important to note that the `String` for `ErrorPage.internalError` could come from propogating a `FatalError`, so it's generally not a good practice to display these error messages to users (though it is a good idea to display them in your dev server's 500 pages, or log them to an error reporting service).
```elm
data : RouteParams -> Parser (BackendTask FatalError (Response Data ErrorPage))
data routeParams =
Request.succeed
(BackendTask.Http.getJson apiUrl apiDecoder
|> BackendTask.allowFatal
|> BackendTask.andThen renderMyPage
)
```
In this case, we're allowing the `FatalError` from the `BackendTask.Http` error to propogate through. This will result in a fairly low-level error message that we should avoid presenting to the user.