Building a Blog in Haskell with Yesod–The Basic Structure

Posted on July 15, 2019 by Riccardo

This is a series about Yesod: a Haskell web framework that follows a similar philosophy to Rails. In fact, it is strongly opinionated and provides a lot of functionality out of the box.

A good read about Yesod is available online for free: Developing web applications with Haskell and Yesod. That’s why this series will be a commentary of the commits from a repo we will use to develop a super simple blog.

In other words, this won’t be good material to learn how to use Yesod. However, it will hopefully give an overview of how the framework works.

Series index:

  • Building a Blog in Haskell with Yesod–The Basic Structure (this post)

Intro

In this post we are going to develop the basic structure of our blog. The flow of the user is simple:

  1. The landing page is a login form:

  1. After a successful login the user is redirected to the posts page. Here, the user is able to create a new post and check all the posts already published:

To keep this article simple, we are not going to connect to the database, neither we will check the login credentials.

Init

Commit 7a5782d47e082a8cfcb4e07bac71d88709179c0c just implements the steps listed in the Yesod quick start guide.

The Landing Page

The stack exec -- yesod add-handler command allows to create a new route and handler. In other words, the logic that takes care of a request at a specific URL.

Commit 354fac670be4b9869b171a0fdd7d15063d094fdb uses that command to add the landing page route

and handler:

Authorization

Yesod provides authentication / authorization support out of the box. That is why the application doesn’t compile:

In particular, we are missing isAuthorized for the new LandingR handler. Let’s fix it by always allowing access:

With that, it’s possible to launch the dev server stack exec -- yesod devel and visit /landing.

The error comes from the fact that the handler is not yet developed.

Landing Somewhere

In the spirit of doing baby steps, let’s first put something on screen for /landing. Commit 5271b5f514782f659c89101ee0c1cd4f8d691ade introduces an Hamlet template which is used to generate HTML:

In particular, #{interpolated} comes from the handler:

In other words, every variable in scope can be used in the template.

If we visited /landing now we would see:

Tweaking the Landing

Commit bb89035c9e4f4b9265bebe378ec38e932bc47bcd does a couple of things:

  1. Puts the LandingR handler at /:
  1. Creates an emptyLayout. This is done to remove all the noise created while initializing the Yesod application so that the code is easier to navigate. We won’t get into details, the important thing is that we can now use emptyLayout instead of defaultLayout.

Also, we get ready to add the login form:

If we visited / now we would see just a “Login” heading.

The Login Form

Commit 8d89a7101a979b06ea324c8d6d9aa0373f49d6ee adds the login form logic.

In particular, we first add the posts route and handler with stack exec -- yesod add-handler. This will be the page to redirect too after a successful login. Also, we add a POST handler to LandingR to handle the form submit:

Then we allow anybody to access PostR. We will limit this resource to logged in users in the future.

We add the login form. Important pieces here:

  • The type-safe url @{LandingR}: the form gets submitted to the POST LandingR handler
  • The interpolated form ^{widget} that comes from the GET LandingR handler

The form gets generated in the GET LandingR handler using generateFormPost. In other words, a form that will be submitted via POST.

Subsequently, POST LandingR handler takes the submitted form parameters and runs the validation with runFormPost. In case of success, the user is redirected to the posts page. Otherwise, the login page is re-rendered with the validation errors.

If we visited / now we would see:

The Posts Page

Since the posts page was created with stack exec -- yesod add-handler, it only has an empty handler:

Commit a176ea0d10b63b9613cbfc5bbf86fbe264461111 fixes it by tweaking the posts page. In particular, it adds a couple of hardcoded posts and a form to create a new one.

The structure is exactly the same as seen above for the login page:

diff --git a/config/routes b/config/routes
index db4ac58..5a01ba2 100644
--- a/config/routes
+++ b/config/routes
@@ -13,4 +13,4 @@
 
 /profile ProfileR GET
 / LandingR GET POST
-/posts PostsR GET
+/posts PostsR GET POST
diff --git a/src/Handler/Posts.hs b/src/Handler/Posts.hs
index 138f43b..8d0b8c4 100644
--- a/src/Handler/Posts.hs
+++ b/src/Handler/Posts.hs
@@ -1,6 +1,35 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE OverloadedStrings #-}
+
 module Handler.Posts where
 
 import Import
 
+data Post =
+  Post { title :: Text, text :: Text }
+  deriving Show
+
+postForm :: Form Post
+postForm =
+  renderDivs $
+    Post <$> areq textField "Title" Nothing <*> areq textField "Text" Nothing
+
 getPostsR :: Handler Html
-getPostsR = error "Not yet implemented: getPostsR"
+getPostsR = do
+  (widget, enctype) <- generateFormPost postForm
+  emptyLayout $ do
+    $(widgetFile "posts")
+
+postPostsR :: Handler Html
+postPostsR = do
+  ((result, widget), enctype) <- runFormPost postForm
+  case result of
+    FormSuccess _ ->
+      redirect PostsR
+    _ ->
+      emptyLayout $ do
+        $(widgetFile "posts")
diff --git a/templates/posts.hamlet b/templates/posts.hamlet
new file mode 100644
index 0000000..2445442
--- /dev/null
+++ b/templates/posts.hamlet
@@ -0,0 +1,15 @@
+<h1>Posts
+
+<div id="new-post">
+  <form method=post action=@{PostsR} enctype=#{enctype}>
+    ^{widget}
+    <button>Post
+
+<ul>
+  <li>
+    <h2>Title 1
+    <p>Text 1
+
+  <li>
+    <h2>Title 2
+    <p>Text 2

If we visited /posts now we would see:

Tweaking the Empty Layout

Commit 77567ed74b12685bbf319a6764f1ff83ac9604b8 adds an important element to the empty layout. We won’t get into details but it is going to be important later when adding CSS.

Adding JavaScript and CSS to Posts

Commit 5fb4b964e0890a042b868738d32a99680c06836e shows how to add JavaScript and CSS to a template.

Up until now, we’ve been using $(widgetFile "NAME") to render Hamlet templates. Turns out the widgetFile looks for other types of templates and puts them together. In fact, by using Julius and Cassius templates we can add respectively JavaScript and CSS.

The following change introduces a button that allows to show / hide the post form with a button:

If we visited / now we would see: