Building a Blog with Yesod - Part 2

Posted on 1 May 2020

In the last post we have learned to listen to the compiler while we have taken some parts out of the template. Now, in a second terminal (one is still running yesod devel) go to your template folder and exec

yesod add-handler

We will be asked for the name of route (without trailing R):

PostDetails

Enter route pattern (ex: /entry/#EntryId):

/posts/#Slug

and we should enter space-separated list of methods (ex: GET POST): Which we do by entering

GET

Yesod has now created for us a Handler entry in the

  • blogpost.cabal file, added an import in the
  • src/Application.hs , added a route in
  • config/routes file and has created for us the
  • src/Handler/PostDetails.hs file.

The file looks like this:

module Handler.PostDetails where
import Import 
getPostDetailsR :: Slug -> Handler Html
getPostDetailsR slug = error "Not yet implemented: getPostDetailsR"

Very good. That's the usual workflow for adding a new Handler to the template. But let's look a bit closer and try to understand more of this new file.

module Handler.PostDetails where

tells us Handler.PostDetails is a module of our template with the characteristics described above. The chapter "Routing and Handlers" from the Yesodbook is going in more detail here, but cool if you don't digest it completely.

The second line

import Import

distributes the functionality of the template to our new file.

getPostDetailsR :: Slug -> Handler Html

is telling us that getPostDetailsR takes a type Slug and returns a type Handler Html. You don't have to understand Handler Html completely to use it but to give a very simple and imprecise simplification, it is a gearbox that transforms to html content. Be aware that Handler Html may be the most used one at least in our application.

So from time to time you may want to come back to the explanation in the book and will understand better.

getPostDetailsR slug = error "Not yet implemented: getPostDetailsR"

Somehow we have to tell the compiler that we are still not using getPostDetailsR. We may also do this by writing

getPostDetailsR slug = undefined

As yesod devel gives us

/home/ye/cava/blog/blogpost/src/Foundation.hs:63:1: error:
Not in scope: type constructor or class ‘Slug’
    |
 63 | mkYesodData "App" $(parseRoutesFile "config/routes")
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

let's try to get closer to our initial type Slug. Slugs making our url's more pretty and readable. Without a Slug the url of this blogpost will look like:

blog.onepigayear.de/posts/3

So using Slugs in our application is a nice and helpful thing. Honestly I struggled quite a while to implement them. It seems that there is a module Slug on hackage.haskell.org witch is now deprecated and some talk about implementing them in someway somewhere else, and so on. I ended up by combining the cookbook receipt with code from McKay Broderick found on Github to something that works.

Oh by the way, have you already found the yesod cookbook?

[https://github.com/yesodweb/yesod-cookbook/blob/master/Cookbook.md]

By comparing yesodblog src/Handler/PostDetails.hs with it's template counterpart, let's go through the differences. First we have

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude #-}

"A language pragma directs the Haskell compiler to enable an extension or modification of the Haskell language" - from the HaskellWiki

(Later on try to take one of them out and recompile. You will see that Ghc most of the time knows which pragma to use when.)

import Helpers.Slug (Slug)

Is Helpers.Slug a module, you might ask? It is a Haskell file that you find in the src/Helpers folder in yesodblog and may copy it over.

getPostDetailsR slug = do
    Entity _ BlogPost {blogPostTitle, blogPostArticle, ..} <-
        runDB $ getBy404 $ UniqueSlug slug

GetPostDetailsR slug let Entity _ BlogPost represent a call to the database by using a certain slug to give the whole construct of a blogpost which is defined in src/Handler/Profile.hs -- that we will look at later. This might not be the whole truth, but you should get the general point that we query the database here.

Without using Slugs the code would be much more straightforward, like

getPostDetailsR :: BlogPostId -> Handler Html
getPostDetailsR blogPostId = do
     blogPost <- runDB $ get404 blogPostId

But ok, we wanted them, now we have them.

Far in the distance we might have seen the shade rips of a few horses, do we?

With,

defaultLayout $ do
    setTitle $ toHtml blogPostTitle
    $(widgetFile "postDetails/post")
    $(widgetFile "pu")

by using our corresponding template-files, we shift everything into the capability range of a webbrowser.

I hope that this description of functionality is not too inaccurate, but for now I think, we only want to have a somehow walkable shortcut through the bush.

So, by having modified our src/Handler/PostDetails.hs we still get the same Ghc error and it points to config/routes. Most of the time we do well using our guide, step by step but for now let's first change our config/models.p... according to the yesodblog code.

Wow, Ghc now gives us something like

/home/ye/cava/blog/blogpost/src/Model.hs:22:7: error:
• Not in scope: type constructor or class ‘Markdown’
• In the untyped splice:
$(persistFileWith
    lowerCaseSettings "config/models.persistentmodels")  
   |
22 |     $(persistFileWith lowerCaseSettings "config/models.persistentmodels")
   |       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/home/ye/cava/blog/blogpost/src/Model.hs:22:7: error:
• Not in scope: type constructor or class ‘Slug’
• In the untyped splice:
$(persistFileWith
    lowerCaseSettings "config/models.persistentmodels")
   |
22 |     $(persistFileWith lowerCaseSettings "config/models.persistentmodels")
   |       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

which guide us to the scr/Models.hs file where you directly see (still comparing) that imports of Text.Markdown and Helpers.Slug are missing. To be exact here, add please also import HashDB and copy the instance of HashDBUser. Take care to not mix up the routes for your config/models.p....

Next step is described by Ghc precisly. Ok, we only have one .cabal file I think. Let's add the package mtl. By having a short look we find out that we also have to add Helpers.Slug, which we have already copied a long time ago.

The next message from Ghc is less clear but means to add Yesod.Markdown, Yesod.Text.Markdown, Markdown and Yesod.Auth.HashDB (witch all have nothing to do with slugs but will be of later use) to blogpost.cabal.

Now we dig down even deeper by copying - yesod-markdown-0.12.6.2@sha256:c0be0f1d1e2df073935bd1f6386d814c686be1a14cf6538de1ca09ac882b393c,1737 or similar, that may depend on the resolver version you use, from our terminal output to the extra-deps section in the stack.yaml file. Which should look like so

#
extra-deps:
- yesod-markdown-0.12.6.2@sha256:c0be0f1d1e2df073935bd1f6386d814c686be1a14cf6538de1ca09ac882b393c,1737

Let us take the recommendation for a glass of water serious, stack is busy for a while to get dependencies in order. And hey, we have done quite a good part of the way.

But still we receive these messages from Ghc. So bad that we can't write back the usual way, or send an emoji. Ghc is there a bit stoic and guides us directly to

Authentication

Yesod makes it possible to use different Authentication systems. See the according chapter in the book please, and did you notice we have made it up to the advanced section?

The cookbook give us an example of how to use Auth.HashDB which I choosed because it saves the login credentials hashed in our database, and as this is a personal blog, there is maybe only one person that insert blogposts. In a later post, by looking at another Yesod project, we will learn to use HashDB in a more complex scenario.

[https://github.com/yesodweb/yesod-cookbook/blob/master/cookbook/Using-HashDB-In-a-Scaffolded-Site.md]

Ok, config/models.p... is already ok, only differs in using a name -> Text instead of email -> Text. (You may notice the UniqueUser -> name part.) Let's change it.

src/Model.hs is adapted too, so we finally get to our src/Foundation.hs where we modify

-- Authentication related stuff
instance YesodAuth App where

to

    type AuthId App = UserId
    -- Where to send a user after successful login
    loginDest _ = HomeR
    -- Where to send a user after logout
    logoutDest _ = HomeR
    -- Override the above two destinations when a Referer: header is present
    redirectToReferer _ = True
    authenticate :: (MonadHandler m, HandlerSite m ~ App)
        => Creds App
        -> m (AuthenticationResult App)
    authenticate creds =
        liftHandler $
        runDB $ do
            x <- getBy $ UniqueUser $ credsIdent creds
            case x of
                Just (Entity uid _) -> return $ Authenticated uid
                Nothing -> return $ UserError InvalidLogin
    -- You can add other plugins like Google Email, email or OAuth here
    authPlugins :: App -> [AuthPlugin App]
    authPlugins _ = [authHashDBWithForm myform (Just . UniqueUser)]

...

myform :: Route App -> Widget
myform action = $(widgetFile "auth")    

...

stFile :: QuasiQuoter
stFile = quoteFile st

If we are already here, we should adapt also

isAuthorized (PostDetailsR _) _ = return Authorized

which returns

/home/ye/cava/blog/blogpost/src/Foundation.hs:63:1: error:
Not in scope: type constructor or class ‘Slug’
    |
 63 | mkYesodData "App" $(parseRoutesFile "config/routes")
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/home/ye/cava/blog/blogpost/src/Foundation.hs:262:11: error: 
Not in scope: type constructor or class ‘QuasiQuoter’
    |
262 | stFile :: QuasiQuoter
    |           ^^^^^^^^^^^

and make us think something is wrong in config/routes. Mmh, maybe we should give this

import Helpers.Slug

a try. Yeah, first error is gone, so we may add also

import Yesod.Auth.HashDB (authHashDBWithForm)
import Yesod.Auth.Message (AuthMessage(InvalidLogin))

and

{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}

but still we have

/home/ye/cava/blog/blogpost/src/Foundation.hs:262:11: error:
Not in scope: type constructor or class ‘QuasiQuoter’
    |
262 | stFile :: QuasiQuoter
    |           ^^^^^^^^^^^

By putting this

Yesod Not in scope: type constructor or class ‘QuasiQuoter’

in a search-engine, we might get guided to

[https://stackoverflow.com/questions/43344823/not-in-scope-type-constructor-or-class]

which answers our question in a way that we again have to import, so

import Language.Haskell.TH.Quote (QuasiQuoter, quoteFile)

will do the trick.

The next message from Ghc is quite clear. By using

myform :: Route App -> Widget
myform action = $(widgetFile "auth")

for easy customizing our login-form, we have to have a widgetFile, that in this case is named "auth". Copy it over and study it a bit.

With

import Text.Shakespeare.Text (st)

we respond to the next Ghc message.

So, at this point we may increment our step size and copy the whole src/Handler/Profile.hs over. Which will be followed by doing the same with the templates/posts folder and the templates/pu file. But still...

a bunch of errors and warnings. Shall we give up and forget the whole thing? Sounds not very attractive to me, so another deep breath please and

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NamedFieldPuns #-}

and

import Helpers.Slug (Slug)  

into the src/Handler/PostDetails.hs file and a quick copy of the templates/posts folder result in

/home/ye/cava/blog/blogpost/src/Handler/PostDetails.hs:18:11: error:
Variable not in scope: formatDateStr :: UTCTime -> a0
   |
18 |         $(widgetFile "postDetails/post")
   |           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

In src/Import.hs we have to declare how time is to be handled. Adjust the dateTimeFormat to your needs. More Explanation here:

[https://github.com/yesodweb/yesod/wiki/Formatting-dates]

-- date formatting for templates
dateTimeFormat :: String
dateTimeFormat = "%e %B %Y"

...

formatDateStr :: UTCTime -> String
formatDateStr = formatTime defaultTimeLocale dateTimeFormat
-- end date formatting

Now, let's replace src/Handler/Home.hs with the according code from the yesodblog and the next Ghc message will be about trouble with the original widgetFile.

Looking at the yesodblog template/homepage.hamlet shows that we want to show on our landing-page a list of the blogposts that we want to write in the future. It's a blog here, yeah. So according to we change the contents of it.

One more change to our config/routes has to be done. As we declare the Handler postProfileR in src/Handler/Profile.hs we should also let our config/routes know what's going on here, and ...

Phew!

Database migration: manual intervention required.
The unsafe actions are prefixed by '***' below:
ALTER TABLE "user" ADD COLUMN "name" VARCHAR NOT NULL;
*** ALTER TABLE "user" DROP COLUMN "ident";
ALTER TABLE "user" DROP CONSTRAINT "unique_user";
ALTER TABLE "user" ADD CONSTRAINT "unique_user" UNIQUE("name");
CREATe TABLE "blog_post"("id" SERIAL8  PRIMARY KEY UNIQUE,"title" VARCHAR NOT NULL,"article" VARCHAR NOT NULL,"slug" VARCHAR NOT NULL,"posted" TIMESTAMP WITH TIME ZONE NOT NULL);
ALTER TABLE "blog_post" ADD CONSTRAINT "unique_slug" UNIQUE("slug");

Oh no, we won't get overrun by this one. Looking closer, cleaning the glasses, somehow it seems quite understandable. Let's try

sudo su - postgres
psql blogpost
ALTER TABLE "user" ADD COLUMN "name" VARCHAR NOT NULL;

Mmh,

ERROR:  Column »name« contains NULL values 

We learn here

[https://yuji.wordpress.com/2010/02/18/postgresql-error-column-contains-null-values/]

to maybe do

ALTER TABLE "user" ADD COLUMN "name" VARCHAR NOT NULL default 0;

Seems ok for now. But mmh, again an error. So, while we still don't have saved anything in our database, we may also let it be set up completely new from persistent. The chapter of the book is also called persistent and is a must read.

DROP TABLE blog_post;
DROP TABLE comment;
DROP TABLE email; 

and

DROP TABLE "user";

Now persistent will handle the thing for us and on localhost:4000, or what you have it running at this time, are some candles burning.

That was an awful long stage. Some steep ramps and these mysterious horses in the distance, but we have done it and implemented at least in great part: Slugs, Markdown article support, a time handling and the basics of an authentication-system with a custom layout. Well done!!

Thanks for reading so far. The next article will be about inserting hashed login credentials in our database and make authentication working.

[https://blog.onepigayear.de/posts/building-a-blog-with-yesod-part-3]