Add Routes

A hello-world site is not very interesting. Let’s write an application to track moods, i.e., a mood tracking app. We will record our moods in plain-text (a CSV file) and view them through an Ema app.

The first step is to think about the various pages and define their corresponding route types. Since our application will have an index page (displaying mood summary) and pages specific to the individual days, we will use an ADT with two constructors:

data Route
  = Route_Index     -- /index.html
  | Route_Date Date -- /date/YYYY-MM-DD.html
  deriving stock (Show, Eq, Ord, Generic)

deriveGeneric ''Route
deriveIsRoute ''Route [t|'[]|]

We must derive IsRoute to enrich our route type with three capabilities:

  • RouteModel: associate a value type (Model type) that will be used for encoding and decoding routes (see next point)
  • routePrism: produce Route Prism (a Prism') that we can use to encode routes to URLs and vice versa.
  • routeUniverse: generate a list of routes to statically generate

Here, we use TemplateHaskell to derive IsRoute generically, instead of hand-writing the instance. We can of course also derive IsRoute manually. In fact, we must do it for the Date sub-route type (because it is not an ADT, like Route above, shaped for Generic deriving):

import Data.Time
import Optics.Core (prism')

-- | Isomorphic to `Data.Time.Calendar.Day`
newtype Date = Date (Integer, Int, Int)
  deriving stock
    (Show, Eq, Ord, Generic)

instance IsRoute Date where
  type RouteModel Date = ()
  routePrism () = Ema.toPrism_ $
    prism'
      ( \(Date (y, m, d)) ->
          formatTime defaultTimeLocale "%Y-%m-%d.html" $
            fromGregorian y m d
      )
      ( fmap (Date . toGregorian)
          . parseTimeM False defaultTimeLocale "%Y-%m-%d.html"
      )
  routeUniverse () = [] -- need model for this
  • We don’t need any special Model type to encode a Date route, thus RouteModel is a unit. But we’ll modify this in next step (to implement routeUniverse).
  • toPrism_ is an Ema function that converts the optics-core Prism' into a coercible Prism_ type that Ema internally uses. A route prism knows how to encode and decode the Date route. Our route Prism' is built using formatTime and parseTimeM.
  • We will implement routeUniverse in the next step of the tutorial

The result is that we can use the function routeUrl to get the URL to our routes. Let’s see this in action in GHCi (run bin/repl in the template repository):

ghci> -- First get hold of the route Prism, which is passed to `siteOutput`
ghci> let rp = fromPrism_ $ routePrism @Route ()
ghci> Ema.routeUrl rp Route_Index
"" -- The 'index.html' is dropped as it is redundant in HTML.
ghci> Ema.routeUrl rp $ Route_Date $ Date (2022, 04, 23)
"date/2022-04-23.html" 

You also can use optics operators to directly operate on route prisms.

-- NOTE: Using `rp` from GHCi session above
ghci> import Optics.Core
ghci> review rp Route_Index
"index.html"
ghci> preview rp "2022-04-23.html"
Nothing
ghci> preview rp "date/2022-04-23.html"
Just (Route_Date (Date (2022,4,23)))

See Route type for details.

Next, we will explain how to define our moods.csv model and render it.

Links to this page
  • IsRoute

    enables us to convert between Route_Blog BlogRoute_Index and /blog/index.html and vice versa (as shown in the tutorial).

  • Hello World
    In Add Routes, you will see how to write more elaborate route types and derive IsRoute for them. IsRoute is what tells Ema that a Haskell type is a route type (with URL encoders and decoders).

    Next, we will explain how to write a simple mood tracker in Ema.

  • Add a Model

    Now we want to associate our Route type from Add Routes with this Model. This can be done as follows: