Building a Blog with Yesod - Part 1

Posted on 26 April 2020

So taking part of this journey? Here we are, let's go back to the car.

A view on things

I assume your browser is running on localhost:3000 and you are wondering what to do next. Play around a bit and you may get confirmed in the assumption that this template-website is by far well thought out. Try to login, upload something, write a comment.

In Firefox do a

Ctrl Shift c

for Chromium (chrome)

Crtl Shift i

and check out the html/css structure of the site. Same command again will get you back in normal mode.

Using Geany we may have a look at the default-layout.hamlet and default-layout.julius files in the templates folder.

In the Yesodbook you will now want to read the shakespearean-templates chapter if not already done.

Take your time. As the concept here is straightforward (each of the 3 commonly used "weblanguages", Html, Css and Javascript, have a correspondence in Yesod) the amount of code and the structure of the template we are using asks for a slow pacing.

And this is a very important point here -> we are not able to understand the whole thing in once. At least I am not able to, and I am so bold to assume that you don't either. So let's try some kind of cherry-picking and slowly grope our way forward.

Go to

[https://git.onepigayear.de/]

and clone the repo. Yuck! We haven't installed git? Ok, here we go

apt update
apt upgrade
apt install git

Now (from your blog-directory) do a

git clone https://git.onepigayear.de/ye/yesodblog.git

and you will have all the code at your hands. You may want to learn more about git. As you have already noticed, I host an instance of git(ea) by myself. And I use it nearly every day.

[https://git-scm.com/book/en/v2/Getting-Started-About-Version-Control]

Read at least that first page, and try to make use of your git installation. There are a lot of tutorials out there.

So, now let's go back to the yesodblog folder where 3 subfolders are of main interest,

  • templates where each Handler find their related .hamlet, .lucius and .julius files,
  • src/Handler which hold the Haskell functionality for our sites in Handlers,
  • config where we define routes and types.

Compare the folder structures.

The plan here is that we build our blogpost by inserting/changing step by step code from the repo you already cloned. But first, let's get our hands on the front-end, will say on the visual part of things. Our template uses Bootstrap in version 3 for lifting things from our shoulders. That doesen't mean you can't overwrite it. Compare the default-layout files from the yesodblog repo and the template. Have in mind that changes here are sidewide (default).

For example in default-layout.hamlet I have taken out some parts, but left all the nav, containerstructure and footer. So it will be the html-structure of all (sub-)sites.

The default-layout.wrapper file is an abstraction over the html functionality of our site. You will see that I have taken out the script src for bootstrap.min.js which now is downloaded and located in the static/js folder. Why? Mmh, maybe because of having more control and maybe because this will be probably not an extreemly high traffic site.

See /src/Foundation.hs around line 160, where you find this change made known to Yesod. First time when I saw the Foundation.hs file, I thought of stepping my whole idea of learning Haskell in the bin. But cool -> as you will find out, this is a very good example file of how to structure Haskell code. More you get familiar with Yesod, more you will appreciate Foundation.hs and more you will get familiar with looking at, and beginning to understand code.

As an example try to replicate the lifting of bootstrap.min.js and also the other 2 relevant files. If you like to compare things deep, stop stack exec -- yesod devel with

Crtl c

and restart it with

yesod devel -p 4000

In another terminal from the yesodblog-folder you should now be able to start another instance of the devel-server with

yesod devel -q 5000

Uups, seems we have forgotten something. But what the heck, ah oh -> well, sorry for that! Go back to the previous Blogpost and there straight to the Postgresql section. Prepare a database for user yesodblog with password yesodblog and database yesodblog should do the trick. Aren't we some lucky folks?

Open both sites in different tabs of your browser. Template will be on localhost:4000, where the yesodblog will be on localhost:3000. This port-thing might be a bit confusing, but as far as I know has somthing to do with otherwise overlapping https-ports.

Comparing scr/Handler of your template with the corresponding yesodblog folder, will give some more insight.

  • About.hs for example is a simple Handler which declares which title and files from the template-folder to use for the About (sub-)site.

  • PostDetails.hs is a Handler-file which declares title and template-files of the various blogposts and query the corresponding data from the (postgres)database and inserts it in respective blogposts.

  • Profile.hs declares title, template-files of the post-entry form, structure of the post-entry and send that to the database.

If you remember the output from the last command in the previous article executed was something like:

Registering library for blogpost-0.0.0..
Success! Waiting for next file change.
Type help for available commands. Press enter to force a rebuild.
Starting devel application
Migrating: CREATe TABLE "user"("id" SERIAL8  PRIMARY KEY UNIQUE,"ident" VARCHAR NOT      NULL,"password" VARCHAR NULL)
31/Mar/2020:10:13:38 +0200 [Debug#SQL] CREATe TABLE "user"("id" SERIAL8  PRIMARY KEY UNIQUE,"ident" VARCHAR NOT NULL,"password" VARCHAR NULL); []
Migrating: CREATe TABLE "email"("id" SERIAL8  PRIMARY KEY UNIQUE,"email" VARCHAR NOT NULL,"user_id" INT8 NULL,"verkey" VARCHAR NULL)
Migrating: CREATe TABLE "comment"("id" SERIAL8  PRIMARY KEY UNIQUE,"message" VARCHAR NOT NULL,"user_id" INT8 NULL)
Migrating: ALTER TABLE "user" ADD CONSTRAINT "unique_user" UNIQUE("ident") 
Migrating: ALTER TABLE "email" ADD CONSTRAINT "unique_email" UNIQUE("email")
Migrating: ALTER TABLE "email" ADD CONSTRAINT "email_user_id_fkey" FOREIGN KEY("user_id") REFERENCES "user"("id")
Migrating: ALTER TABLE "comment" ADD CONSTRAINT "comment_user_id_fkey" FOREIGN KEY("user_id") REFERENCES "user"("id")
Devel application launched: http://localhost:3000
31/Mar/2020:10:13:38 +0200 [Debug#SQL] CREATe TABLE "email"("id" SERIAL8  PRIMARY KEY UNIQUE,"email" VARCHAR NOT NULL,"user_id" INT8 NULL,"verkey" VARCHAR NULL); []
31/Mar/2020:10:13:38 +0200 [Debug#SQL] CREATe TABLE "comment"("id" SERIAL8  PRIMARY KEY UNIQUE,"message" VARCHAR NOT NULL,"user_id" INT8 NULL); []
31/Mar/2020:10:13:38 +0200 [Debug#SQL] ALTER TABLE "user" ADD CONSTRAINT "unique_user" UNIQUE("ident"); []
31/Mar/2020:10:13:38 +0200 [Debug#SQL] ALTER TABLE "email" ADD CONSTRAINT "unique_email" UNIQUE("email"); []
31/Mar/2020:10:13:38 +0200 [Debug#SQL] ALTER TABLE "email" ADD CONSTRAINT "email_user_id_fkey" FOREIGN KEY("user_id") REFERENCES "user"("id"); []
31/Mar/2020:10:13:38 +0200 [Debug#SQL] ALTER TABLE "comment" ADD CONSTRAINT "comment_user_id_fkey" FOREIGN KEY("user_id") REFERENCES "user"("id"); []
31/Mar/2020:10:14:02 +0200 [Debug#SQL] SELECT "id", "message", "user_id" FROM "comment" ORDER BY "id"; []

Which for learning reasons we should check in our database.

sudo su - postgres
psql blogpost
\d+

will give us something like:

Schema |      Name      |   Typ   | Eigentümer |   Größe    | Beschreibung 
--------+----------------+---------+------------+------------+--------------
public | comment        | Tabelle | blogpost   | 8192 bytes | 
public | comment_id_seq | Sequenz | blogpost   | 8192 bytes | 
public | email          | Tabelle | blogpost   | 8192 bytes | 
public | email_id_seq   | Sequenz | blogpost   | 8192 bytes | 
public | user           | Tabelle | blogpost   | 8192 bytes | 
public | user_id_seq    | Sequenz | blogpost   | 8192 bytes | 

Have a look at the Name column. Comment, email and user are guiding us to config/models.persistentmodels file where they are defined. Compare that file to corresponding config/model from yesodblog. (.persistantmodels has come up recently, older versions of the template haven't had that file-extension but it touches somehow the concept behind models.persistentmodels for which you get an in depht explanation in the Persistant chapter of the Yesodbook.)

Yesodblog config/models let you not only find out that comments are not available and that there is some kind of auth-system, but also, by looking at Handler/Profile.hs, that the left column value has the type defined in the right column. E.g. title -> Text, article -> Markdown, aso.

Therefore to rebuild our template to become a Blog we will start with config/models.persistentmodels. Let's cut out the comment section entirely, safe the file and have a look at the terminal where we started yesod devel for our template. I haven't talked so far about what happens when Ghc - do you remember, your new best friend, gives us an error. In this case here, the first error from Ghc is:

/home/ye/cava/blog/blogpost/src/Handler/Home.hs:72:30: error:
Not in scope: type constructor or class ‘Comment’
Perhaps you meant ‘Content’ (imported from Import
   |
72 | getAllComments :: DB [Entity Comment]
   |                              ^^^^^^^

which will translate among other things to: Dear friend, have a look in line 72 of your src/Handler/Home.hs file - which we directly do. If we delete line 72 and 73 which declare getAllComments and save the file, Ghc will guide us to the now obsolete file src/Handler/Comments.hs which we, without realy knowing what happens inside, delete.

Phiew... if you are a person who hates to delete files, please feel free to save them to your stash in order to examinate it later on. For now let's focus on Ghc again

/home/ye/cava/blog/blogpost/src/Handler/Home.hs:30:28: error:
Variable not in scope: getAllComments
    :: ReaderT SqlBackend (HandlerFor App) (t2 (Entity t1))
   |
30 |     allComments <- runDB $ getAllComments
   |                            ^^^^^^^^^^^^^^

which we (line 30) delete also. Ghc still telling us that "getAllComments is not in scope", and points to line 35, which is

$(widgetFile "homepage")

Ok, let's put the gloves on, pop up the hood and have a look in templates/homepage.hamlet where we find allComments around line 124. Maybe at this point we should fall back to our old strategy of comparing template with yesodblog and see the solution right away: cut out the whole section. The next Ghc error will be about some other remainings of getAllComments. Let's get rid of that line to. Now Ghc guides us to the src/Application.hs file where the Handlers for our App are imported. We should get rid of

import Handler.Comment

what leads to

/home/ye/cava/blog/blogpost/src/Application.hs:50:1: error:
Variable not in scope: postCommentR :: HandlerFor App res0
   |
50 | mkYesodDispatch "App" resourcesApp
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

which I find a bit cryptic, or better, not easy to understand. Maybe we have a look in our config/routes file. Here we leave only the routes

/static StaticR Static appStatic
/auth   AuthR   Auth   getAuth
/favicon.ico FaviconR GET
/robots.txt RobotsR GET
/ HomeR GET POST
/profile ProfileR GET

Now we will be guided to src/Foundation.hs where we take out CommentR from the isAuthorized section. We are near friend, Ghc is whispering.

Next we assume that line 35 and 49, which both guide back to templates/homepage.hamlet are misleading. By cutting out

let (commentFormId, commentTextareaId, commentListId) = commentIds

from src/Handler/Home.hs line 32, we get a more understandable error which tells us that

commentFormId
commentTextareaId
CommentR
commentListId
commentTextareaId

are not in scope. But where do we have that in templates/homepage.hamlet? Mmh, is the error maybe from another homepage-file, maybe templates/homepage.julius? which we open and see a bunch of javascript code that somehow is related to the comment-errors we get, ah sorry gotten, because it already hiked in your stash.

But still we have

/home/ye/cava/blog/blogpost/src/Handler/Home.hs:34:11: error:
Variable not in scope: commentTextareaId
   |
34 |         $(widgetFile "homepage")
   |           ^^^^^^^^^^^^^^^^^^^^^

People from a certain part of the world now might say: Mama Mia, come facciamo?

But we as high-skilled aspiring Haskellers have an idea: in templates/homepage.lucius we learn the hard way, that it is also possible to address our Haskell-code directly in the .julius file. Great, we have made it!

Don't forget to delete

##{commentTextareaId} {
width: 400px;
height: 100px;
}

Wow, what a long stage was that. Was it worth the effort? Yes, our application compiles again and we have had a nice first encounter with Ghc. By having such thing more often in the near future, we should now really leave all contact difficulties apart and accept the generously helping compiler.

At this point I would like to recommend having a look at

[https://yannesposito.com/Scratch/en/blog/Yesod-tutorial-for-newbies/]

from Yann Esposito (2012, Yesod version 1.2) and

[https://www.youtube.com/watch?v=SadfV-qbVg8]

from Max Tagher (2015, Yesod version 1.4)

In Yann Espositos tutorial you will see a clear and dense explanation of how things are connected together. Do not try to install it using cabal to avoid making things more complicated than we have to for now. Just read it and compare to our current Yesodblog version. You will immediately identify the cornerstones.

By following Max Tagher through his video tutorial you will deepen insight and observe what the actual blueprint for our work here is. And for sure, there are some more examples out there. You might already have seen

[https://www.yesodweb.com/book/blog-example-advanced]

and

[https://github.com/yesodweb/yesodweb.com/blob/master/src/Blog.hs]

as to name only a few.

Thanks for reading so far; it is still early, sun is still low and the next hill to climb on our way casts his shadow on:

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