Blog of Ricardo Martinez

About nothing in particular

Building a blog engine with Go and Postgres
3 Sep 2023

Since a few months I started getting more and more interested in the Go language. It is simple, fast, strongly typed and very memory-efficient thanks to its garbage collection feature. Plus, it offers a very simple and efficient way of dealing with concurrency thanks to goroutines.

I started looking at tutorials and doing simple programming exercises to get familiar with the syntax and the philosophy behind. However, those did not seem to be enough to fully understand the language, so I decided to build a simple project with it. By the time I had come with that idea, I was building my website (yes, this website!)using HTML and CSS, because I also wanted to learn more about how the Frontend shows things so beautiully (and discover in the way that I adore not being a FE developer). So, what cool project could offer both learning some basic frontend stuff, and learning a new server-side programming language? Yes! A(nother) single-user blog engine!

Contents

  1. The stack
  2. Storing the posts
  3. Rendering posts using HTML Templates
  4. Summary

The stack

There are probably unlimited ways to do a blog engine in Go, and most likely hundreds of tutorials. However, almost all of them use HTML templates and a binary storage system. Though, I thought that using a relational database was (from my very subjective and opinionated point of view) a better idea, because it allows storing metadata about the articles and is a flexible way to decouple the rendering engine from the data-side of the application itself. This would be useful, for example to migrate the frontend to a more sophisticated stack, like React.JS (which is also on my to-learn list).

So, I used:

Storing the posts

I tried to stick with DDD patterns as much as I could when building this app, so the first logical step would be to implement the domain entities (or the data types to be shared accross different packages).

In a pkg/domain/posts.go file, we would then have:

package domain

import "time"

type Post struct {
 ID       int
 Added    time.Time
 Modified time.Time
 Author   string
 Tags     []string
 Title    string
 Content  string
 Summary  string
 URLPath  string
 ImgURL   string
}

and then the ORM in a pkg/orm/posts.go:

package orm

import (
 "time"

 "github.com/lib/pq"
)

// gorm.Model definition
type Post struct {
 ID        int `gorm:"primaryKey"`
 CreatedAt time.Time
 UpdatedAt time.Time
 Author    string         `gorm:"default:Ricardo"`
 Tags      pq.StringArray `gorm:"type:text[]"`
 Title     string
 Content   string `gorm:"type:text"`
 URLPath   string `gorm:"column:url_path"`
 Summary   string
 ImgURL    string
}

We can also define custom functions to build a db post object from a domain post object:

func NewPostDB(p domain.Post) *Post {
 return &Post{
  ID:      p.ID,
  Author:  p.Author,
  Tags:    p.Tags,
  Title:   p.Title,
  Content: p.Content,
  URLPath: p.URLPath,
  Summary: p.Summary,
  ImgURL:  p.ImgURL,
 }
}

The post repository

The post repository will be the interface to our persistence infrastracture (the PostgreSQL database in our case), and the application, so that the logic is uncoupled.

We are using Go interfaces for this, which is the closest thing to abstract interfaces in OPP languages like Python or Java. We can define an interface for our PostRepository with all the methods that we need for our blog.

package repository

import (
 "fmt"

 "github.com/rmargar/website/pkg/domain"
 "github.com/rmargar/website/pkg/orm"
 "gorm.io/gorm"
)

type PostRepository interface {
 New(post domain.Post) (*domain.Post, error)
 GetAll() ([]domain.Post, error)
 SearchByTitle(string) ([]domain.Post, error)
 GetOneByUrlPath(string) (*domain.Post, error)
 GetByTag(string) ([]domain.Post, error)
}

And then a PostRepoSqlstruct that will implement the interface from above.


type PostRepoSql struct {
 Db *gorm.DB
}

func (p *PostRepoSql) GetAll() ([]domain.Post, error) {
 var posts []orm.Post
 result := p.Db.Find(&posts)
 if result.Error != nil {
  return orm.NewPosts(posts), result.Error
 }
 return orm.NewPosts(posts), nil
}

func (p *PostRepoSql) SearchByTitle(title string) ([]domain.Post, error) {
 var posts []orm.Post
 result := p.Db.Where("title LIKE ?", "%"+title+"%").Find(&posts)
 if result.Error != nil {
  return orm.NewPosts(posts), result.Error
 }
 return orm.NewPosts(posts), nil
}

func (p *PostRepoSql) GetOneByUrlPath(urlPath string) (*domain.Post, error) {
 var post orm.Post
 result := p.Db.Where("url_path = '" + urlPath + "'").First(&post)
 if result.Error != nil {
  return orm.NewPost(post), result.Error
 }
 return orm.NewPost(post), nil
}

func (p *PostRepoSql) GetByTag(tag string) ([]domain.Post, error) {
 var posts []orm.Post
 result := p.Db.Where(fmt.Sprintf("'%s'=any( tags)", tag)).Find(&posts)
 if result.Error != nil {
  return orm.NewPosts(posts), result.Error
 }
 return orm.NewPosts(posts), nil
}

Now the fun part comes: testing, yay! I decided to go with only integration-test for this part of the application since it is safer and easier, you can check them out in the repo.

Implementing the service layer

Now that we have defined our persistence and the repository for the posts of the blog, we need to write our PostService, which will communicate with the API controllers and the HTML renderer.

Then, in pkg/application/post.go we have:

package application

import (
 "errors"

 "github.com/rmargar/website/pkg/domain"
 "github.com/rmargar/website/pkg/repository"
)

type PostService interface {
 Create(title string, content string, tags []string, summary string, urlPath string) (*domain.Post, error)
 GetOneByTitle(title string) (*domain.Post, error)
 GetAll() ([]domain.Post, error)
 GetByUrlPath(urlPath string) (domain.Post, error)
 GetByTag(tag string) ([]domain.Post, error)
}

type Posts struct {
 postRepo repository.PostRepository
}

func (p *Posts) Create(title string, content string, tags []string, summary string, urlPath string) (*domain.Post, error) {
 post := domain.Post{Title: title, Content: content, Tags: tags, URLPath: urlPath, Summary: summary}
 return p.postRepo.New(post)
}

func (p *Posts) GetByUrlPath(urlPath string) (domain.Post, error) {
 post, err := p.postRepo.GetOneByUrlPath(urlPath)
 return *post, err
}

func (p *Posts) GetOneByTitle(title string) (*domain.Post, error) {
 var post *domain.Post
 foundPosts, err := p.postRepo.SearchByTitle(title)
 if err != nil {
  return post, err
 }

 switch {
 case len(foundPosts) > 1:
  return &foundPosts[0], errors.New("More than one post was found")
 case len(foundPosts) == 0:
  return post, errors.New("No posts found")
 default:
  return &foundPosts[0], nil
 }
}

func (p *Posts) GetAll() ([]domain.Post, error) {
 return p.postRepo.GetAll()
}

func (p *Posts) GetByTag(tag string) ([]domain.Post, error) {
 return p.postRepo.GetByTag(tag)
}

func NewPostService(p repository.PostRepository) *Posts {
 return &Posts{postRepo: p}
}

API to create/update posts

I decided to expose a REST API in order to create/edit posts (using POST and PATCH methods). The API would only expose a single endpoint in api/posts.

Go offers a very powerful built-in package to handle http requests, net/http, however I thought that Chi was a more complete option, and in any case, fully compatible.

Because we don’t want random strangers editing our content, having some sort of authentication is probably a good idea for this API, so I decided to use JWT.

Building the controllers

First, we need to define the serializers in pkg/web/resources/posts.go:

package resources

import (
 "time"

 "github.com/rmargar/website/pkg/domain"
)

type PostPayloadJSONApi struct {
 Title   string   `json:"title" validate:"required"`
 Content string   `json:"content" validate:"required"`
 Tags    []string `json:"tags"`
 URLPath string   `json:"urlPath" validate:"required"`
 Summary string   `json:"summary"`
 ImgURL  string   `json:"imgUrl"`
}

type PostJSONApi struct {
 ID       int       `json:"id"`
 Title    string    `json:"title"`
 Content  string    `json:"content"`
 Tags     []string  `json:"tags"`
 Author   string    `json:"author"`
 Added    time.Time `json:"added"`
 Modified time.Time `json:"modified"`
 Summary  string    `json:"summary"`
 URLPath  string    `json:"urlPath"`
 ImgURL   string    `json:"imgUrl"`
}

type PostCreatedJSONApi struct {
 Message string      `json:"msg"`
 Data    PostJSONApi `json:"data"`
}

func BuildCreatedResponse(post *domain.Post) *PostCreatedJSONApi {
 return &PostCreatedJSONApi{
  Message: "Created",
  Data: PostJSONApi{
   ID:       post.ID,
   Title:    post.Title,
   Content:  post.Content,
   Author:   post.Author,
   Tags:     post.Tags,
   Added:    post.Added,
   Modified: post.Modified,
   Summary:  post.Summary,
   URLPath:  post.URLPath,
   ImgURL:   post.ImgURL,
  },
 }
}

Then, we add our controller to handle POST requests in api/posts such as:


func (p Posts) AddPost(w http.ResponseWriter, r *http.Request) {
 var payload resources.PostPayloadJSONApi

 if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
  representations.WriteBadRequestWithErr(w, err)
  return
 }

 if ok, errors := representations.ValidateInputs(payload); !ok {
  representations.WriteValidationResponse([]representations.ClientError{errors}, w)
  return
 }

 createdPost, err := p.PostService.Create(payload.Title, payload.Content, payload.Tags, payload.Summary, payload.URLPath)

 if err != nil {
  representations.WriteBadRequestWithErr(w, err)
 }

 w.Header().Set("Content-type", "application/json")
 w.WriteHeader(http.StatusCreated)
 response := resources.BuildCreatedResponse(createdPost)
 json.NewEncoder(w).Encode(response)

}

Which can be initialized, together with JWT auth using:

package controllers

import (
 "encoding/json"
 "net/http"

 "github.com/go-chi/chi/v5"
 "github.com/go-chi/jwtauth"
 "github.com/rmargar/website/pkg/application"
 "github.com/rmargar/website/pkg/config"
 "github.com/rmargar/website/pkg/web/representations"
 "github.com/rmargar/website/pkg/web/resources"
)

type Posts struct {
 PostService application.PostService
}

func SetupPosts(r chi.Router, cfg *config.Config, postService application.PostService) {
 tokenAuth := jwtauth.New("HS256", []byte(cfg.JwtSecret), nil)
 postController := Posts{PostService: postService}
 r.Group(func(r chi.Router) {
  r.Use(jwtauth.Verifier(tokenAuth))
  r.Use(jwtauth.Authenticator)
  r.Post("/api/posts", postController.AddPost)
 })
}

Rendering posts using HTML Templates

Now that we have our posts stored in the DB and way to create/edit them safely (thanks to JWT), we can show them to the world!

We can create the following data types:

package html

import (
    "github.com/rmargar/website/pkg/domain"
)

type RenderData struct {
 Posts      []domain.Post
 CurrentURL string
 Tag        string
}

type HTMLPost struct {
 Post        domain.Post
 CurrentURL  string
 ContentHTML string
}

And use goldmark to render Markdown to HTML text:


var md goldmark.Markdown = goldmark.New(
 goldmark.WithExtensions(extension.GFM, extension.Footnote, extension.Typographer),
 goldmark.WithParserOptions(
  parser.WithAutoHeadingID(),
 ),
 goldmark.WithRendererOptions(),
)

func RenderPost(post domain.Post, url string) HTMLPost {
 var buffer bytes.Buffer
 var contentHTML string

 if err := md.Convert([]byte(post.Content), &buffer); err != nil {
  contentHTML = fmt.Sprintf("<p>Error rendering Markdown: <code>%s</code></p>", err.Error())
 } else {
  contentHTML = buffer.String()
 }

 return HTMLPost{
  Post:        post,
  CurrentURL:  url,
  ContentHTML: contentHTML,
 }
}

We can now write a simple template for a post like:

{{ define "title" }}{{ .Title }}{{ end }}

{{ define "head_extra" }}
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
<script>
    hljs.initHighlightingOnLoad();
</script>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/atom-one-dark.min.css">
{{ end }}

{{ define "content" }}
{{ template "partial/heading.tpl" . }}


<div class="row medium-8 large-7 columns">
    <h1 class="blog-header"> {{ .Post.Title }} <br> <small> {{ .Post.Added | format_date }}</small></h1>
    <div class="post-image-container"><img class="thumbnail"
            src="{{- if .Post.ImgURL -}} {{ .Post.ImgURL}} {{- else -}} ../static/assets/jpeg/rmargar.jpeg {{ end }}"></div>
    {{ template "partial/info.tpl" .Post }}

    {{ .ContentHTML | noescape }}

    <hr>

</div>

{{ template "partial/footer.tpl" . }}

{{ end }}

And our package to load a render the HTML templates. For this, we will define an HTML struct that will load in memory the templates for faster execution.

package html

import (
 "fmt"
 "html/template"
 "os"
 "path/filepath"
 "time"

 "github.com/leekchan/gtf"
)

type HTMLConfig struct {
 TemplatesPath string `env:"HTML_TEMPLATES_PATH" env-default:"./templates"`
 DateFormat    string `ev:"HTML_DATEFORMAT" env-default:"2 Jan 2006"`
}

type HTML struct {
 config    HTMLConfig
 Templates map[string]*template.Template
}

// NewHTML creates new HTML instance
func NewHTML(config HTMLConfig) (*HTML, error) {
 instance := &HTML{
  Templates: make(map[string]*template.Template),
  config:    config,
 }

 return instance, instance.InitTemplates()
}

// InitTemplates loads templates in memory
func (r *HTML) InitTemplates() error {

 baseFiles, err := r.GetPartials()
 if err != nil {
  return err
 }

 baseFiles = append(baseFiles, fmt.Sprintf("%s/base.tpl", r.config.TemplatesPath))

 baseTemplate := template.Must(
  template.New("base.tpl").
   Funcs(GetTmplFuncMap(r.config.DateFormat)).
   ParseFiles(baseFiles...),
 )

 for _, tmplName := range []string{"index.tpl", "post.tpl", "tag.tpl"} {
  tmplPath := fmt.Sprintf("%s/%s", r.config.TemplatesPath, tmplName)
  r.Templates[tmplName] = template.Must(template.Must(baseTemplate.Clone()).ParseFiles(tmplPath))

  if err != nil {
   return err
  }
 }

 return nil
}

func (r *HTML) GetPartials() ([]string, error) {
 var partials []string

 walkFn := func(path string, f os.FileInfo, err error) error {
  if nil == err && !f.IsDir() {
   partials = append(partials, path)
  }
  return err
 }
 partialsPath := fmt.Sprintf("%s/partial", r.config.TemplatesPath)

 err := filepath.Walk(partialsPath, walkFn)
 if err != nil {
  return nil, err
 }

 return partials, nil
}

func GetTmplFuncMap(dateFormat string) template.FuncMap {
 funcs := gtf.GtfFuncMap

 funcs["format_date"] = func(value time.Time) string {
  return value.Format(dateFormat)
 }
 funcs["add"] = func(arg int, value int) int {
  return value + arg
 }
 funcs["noescape"] = func(value string) template.HTML {
  /* #nosec G203 -- function is supposed to be non-safe and contain JS */
  return template.HTML(value)
 }

 return funcs
}

Once we have all that, we can set the routers that will serve the HTTP requests to the blog, in pkg/web/controllers/blog.go.


package controllers

import (
 "net/http"

 "github.com/go-chi/chi/v5"
 "github.com/rmargar/website/pkg/application"
 "github.com/rmargar/website/pkg/config"
 "github.com/rmargar/website/pkg/web/html"
)

type Blog struct {
 html    *html.HTML
 service application.PostService
}

func SetupBlog(r chi.Router, cfg *config.Config, postService application.PostService) {
 html, err := html.NewHTML(cfg.HTML)
 if err != nil {
  panic(err)
 }
 blogController := Blog{service: postService, html: html}
 r.Group(func(r chi.Router) {
  r.Get("/blog", blogController.GetBlogIndex)
  r.Get("/blog/{postUrlPath}", blogController.GetPost)
  r.Get("/blog/tag/{tag}", blogController.GetTag)
 })
}

func (b *Blog) GetBlogIndex(w http.ResponseWriter, r *http.Request) {
 t := b.html.Templates["index.tpl"]
 allPosts, _ := b.service.GetAll()
 data := html.RenderData{CurrentURL: r.Host + r.URL.Path, Posts: allPosts}
 t.Execute(w, data)
}

func (b *Blog) GetPost(w http.ResponseWriter, r *http.Request) {
 t := b.html.Templates["post.tpl"]
 urlPath := chi.URLParam(r, "postUrlPath")
 post, _ := b.service.GetByUrlPath(urlPath)
 currentURL := r.Host + r.URL.Path
 data := html.RenderPost(post, currentURL)

 t.Execute(w, data)
}

func (b *Blog) GetTag(w http.ResponseWriter, r *http.Request) {
 t := b.html.Templates["tag.tpl"]
 tag := chi.URLParam(r, "tag")
 allPosts, _ := b.service.GetByTag(tag)
 data := html.RenderData{CurrentURL: r.Host + r.URL.Path, Posts: allPosts, Tag: tag}
 t.Execute(w, data)
}

Summary

Thanks for reading! I actually enjoyed a lot and learnt a lot while building this project. However, I understand that some of the technical decisions are not the best for a production-ready CMS or similar. The idea of storing the posts in a relational database came from a mix of using a technology that gives flexibility and abstraction with no-code, and the fact that I wanted to learn that skill in Go. For a real engine, maybe an object store like S3 or similar would be better.

If you want to check the full code is available in GitHub, or if you prefer, come and say hi from my home page.