Make sure that you have have followed the previous section in order to have the template repo checked out and running locally. Here, our goal is to replace the source code of the template repo and write a basic site from scratch.

  • Follow the template repo’s README and have it open in Visual Studio Code while running the dev server. Your website should be viewable at http://localhost:9001/
  • Open src/Main.hs
  • Delete everything in it, and replace it with the following
module Main where

import qualified Ema

main :: IO ()
main = do
  let speaker :: Text = "Ema"
  Ema.runEmaPure $ \_ ->
    encodeUtf8 $ "<b>Hello</b>, from " <> speaker

This is the minimum amount of code necessary to run an Ema site. Notice that as you replace and save this file, your browser (which is at http://locahost:9001) will hot reload to display “Hello, Ema”. Congratulations, you just created your first website!

Expanding on Hello World

Okay, but that’s just one page. But we want to add a second page. And might as well add more content than “Hello, Ema”. Let’s do that next. The first step is define the route type that corresponds to our site’s pages. Add the following:

data Route
  = Index  -- Corresponds to /
  | About  -- Corresponds to /about
  deriving (Bounded, Enum, Show)

Next, let’s define a model. A model will hold the state of our website used to render its HTML. Let’s put the speaker variable in it, as that’s all we are using:

data Model = Model { speaker :: Text }

We should now tell Ema how to convert our Route to actual file paths (which is used to determine the URL too). Let’s do that by making an instance of the Ema class:

import Ema (Ema (..))

instance Ema Model Route where
  encodeRoute _model = \case
    Index -> "index.html"  -- To /
    About -> "about.html"  -- To /about
  decodeRoute _model = \case
    "index.html" -> Just Index  -- From /
    "about.html" -> Just About  -- From /about
    _ -> Nothing                -- Everything else, are bad routes

Now, we write the main entry point:

import Control.Concurrent (threadDelay)
import qualified Data.LVar as LVar

main :: IO ()
main = do
  Ema.runEma render $ \_act model -> do
    LVar.set model $ Model "Ema"
    liftIO $ threadDelay maxBound

The runEma function is explained here, but in brief: it takes a render function (see below) as well as an IO action that allows us to create and update the model lvar. Note that threadDelay maxBound here? That is because our IO action must not exit; in the dev server mode of real-world websites, you would continue to monitor the external world (such as Markdown files) and update the model, to facilitate hot reload of data used by your site.

On final piece of the puzzle is to write the aforementioned render function:

import Data.Some (Some)
import qualified Ema
import qualified Ema.CLI
import Text.Blaze.Html5 ((!))
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A
import qualified Text.Blaze.Html.Renderer.Utf8 as RU

render :: Some Ema.CLI.Action -> Model -> Route -> Ema.Asset LByteString
render _emaAction model r = Ema.AssetGenerated Ema.Html . RU.renderHtml $
  H.html $ do
    H.head $ do 
      H.title "Basic site"
      H.base ! A.href "/" -- This is important.
    H.body $ do
      H.div ! A.class_ "container" $ do
        case r of
          Index -> do
            H.toHtml $
              "You are on the index page. The name is " <> speaker model
            routeElem About "Go to About"
          About -> do
            "You are on the about page. "
            routeElem Index "Go to Index"
    routeElem targetRoute w =
        ! A.style "text-decoration: underline" 
        ! A.href (H.toValue $ Ema.routeUrl model targetRoute) $ w

If everything compiles, you should see the site update in the web browser. A couple of quick points about the render function:

  • It should return the raw HTML as a ByteString. Here, we use blaze-html as HTML DSL. You can also use your own HTML templates of course.
  • It uses Ema.routeUrl function to create a URL out of our Route type. This function uses the Ema typeclass, so it uses the encodeRoute function defined further above.

On final note, you will note that nothing is actually generated so far. This is because Ema has been running in the dev server mode, which is quite useful during development. To actually generate the files, you can use the gen command when running the CLI:

mkdir ~/output
nix run . -- gen $(pwd)/output


  • Discover how to manage static files like images and PDFs (hint: see AssetStatic in Ema.Asset)
  • What happens if you throw an exception or use error in the render function?

Next, checkout the Guide series for information on specific topics.

Links to this page