Copyright © 2023, Ricardo
Martinez
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!
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:
PostgreSQL, a.k.a the coolest open-source relational database out there. It offers handy features like ARRAY
and TEXT
datatypes, which would allow us to store tags and posts of up to 65.5 kB, which is probably enough for every post I could imagine myself writing (this one here is 2kB).
GORM as a ORM, in order to easy transform from domain to DB entities. Although the Go community stands contrary to using an ORM (because it goes against the language philosophy of functional programming), I found it easier to handle connections since I come from Python and SQLAlchemy.
HTML templates for the posts, super-powered by CSS. For this, I used the html/template package.
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 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 PostRepoSql
struct 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.
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}
}
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.
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)
})
}
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)
}
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.