Building a blogging website with Golang

| By MBV

After getting fed up with React, SPAs, and Javascript around 2021 I decided to re-write my personal webpage in Rust and wrote an article on how you could build a simple blog, purely using Rust. It ended up becoming one of my most popular articles and for good reason; Rust is exciting, fun to write, and blazingly fast. After a while though, I started to feel frustrated with the development process for adding new features to my site: the feedback loop was simply too long.

I've always been interested in solo entrepreneurship and technology. But, as I'm getting older, I realize that I might have been more interested in trying out new technologies. Great for learning and growing as an engineer, bad for shipping projects, and starting to see that sweet MRR grow. I decided to re-re-write my site once again, this time in Go for multiple reasons:

  • I write Go for a living
  • Simple language, with a decent type system and fast performance
  • Blazingly fast compile times

In this post, I will show how you can create your own personal blog using Go. I'll assume you're familiar with Go, and know how to configure a router/database/server, and so on. Should you not, feel free to grab a clone of my Go starter template Grafto that has lots of things configured for you out of the box.

Foundations

I write all my stuff in markdown; if you're a developer who also wants to start blogging, chances are you also are quite familiar with it. After Go 1.16 where we got the embed package included in the standard library most of our work is already done. We basically only need to have a way of storing some filenames, associating them with an ID or a slug, grabbing the file, and serving it to the user. Pretty simple.

Whether you've created your own setup or grabbed a copy of Grafto, create a new directory in the root of your project called posts and in there, create a file called posts.go. Open it and add the following:

1package posts
2
3// imports omitted
4
5//go:embed *.md
6var Assets embed.FS

Now, any file with the .md extension will get included in the binary that ultimately gets built once we run go run build. We can simply grab the files from our global Assets variable using Assets.ReadFile(name-of-file) to handle any error that might occur or return the file as a string, e.g:

1file, err := Assets.ReadFile("my-post.md")
2if err != nil {
3	return err
4}
5
6return string(file)

It won't be pretty (we'll fix that later), but it gets the basic idea out, which we can build on.

We have a way to include our blog posts in the binary; let's add a way to associate them with a slug. You could use an ID here, but it just looks better to have URLs like: "https://acme.com/my-first-blog-post" compared to "http://acme.com/44a2530b-567d-472f-9495-e2ee64e7ae6d". So, assuming you have a database up and running, add this table:

1create table posts (
2    id uuid primary key,
3    created_at timestamp not null,
4    updated_at timestamp not null,
5    title varchar(255) not null,
6    filename varchar(255) not null,
7    slug varchar(255) not null
8);

Not much exciting going on here. Basically, for the unaware readers, the slug column above will be the title but URL-friendly. So if you have a post with the title "My First Blog Post" the slug equivalent would be "my-first-blog-post" easy.

Lastly, we need to be able to serve this to readers. That flow would typically involve them hitting a landing page showing a list of articles they can choose from, which links to the article.

The implementation of this will depend a bit on your setup, but let us implement the handler to deal with grabbing a specific article using Echo as our router. Assuming you have a route like /posts/:slug, create the following:

 1type ArticleStorage interface {
 2	GetPostBySlug(slug string) (Post, error)
 3}
 4
 5func ArticleHandler(ctx echo.Context, storage ArticleStorage) error {
 6	postSlug := ctx.Param("postSlug")
 7	
 8	postModel, err := storage.GetPostBySlug(postSlug)
 9	if err != nil {
10		return err
11	}
12	
13	postContent, err := posts.Assets.ReadFile(postModel.Filename)
14	if err != nil {
15		return err
16	}
17
18	return ctx.String(http.StatusOK, string(postContent))
19}

I'm putting some decisions on your here in terms of implementing the ArticleStorage. We just need something that grabs the data on the post from the DB, based on the slug.

This is the foundation of what we need...but it's not pretty let's fix that by letting the server do what it was always supposed to do: return HTML.

Enter templ

If you've spent time in the Go ecosystem, chances are you've probably heard about templ. It lets you write HTML templates as Go packages and it's just such a pleasant way of building out a UI. Add some HTMX and alpine.js and you've at least 95% of what you get with SPAs, with the added complexity.

It's good practice to have a base template that wraps around your other templates, so we have a single point for adding things like stylesheets, javascript, metadata, etc. Create a directory in root called views and add the following to a file called base.templ.

 1package views
 2
 3templ base() {
 4	<!DOCTYPE html>
 5	<html lang="en">
 6		<nav>
 7			<a href="/">MBV Labs</a>
 8		</nav>
 9		<body>
10			{ children... }
11			<footer>
12				<aside>
13					<p>Copyright ©2024 </p>
14					<p>All right reserved by MBV Labs</p>
15				</aside>
16			</footer>
17		</body>
18	</html>
19}

For this to work, you'll need to install templ and run templ generate which will produce a file called base_templ.go that we can then import into other templates to wrap around them. For the sake of brevity, we'll only create the template to show the actual article. Create a file called article.templ, and add the following:

 1type ArticlePageData struct {
 2	Title             string
 3	Content           string
 4}
 5
 6templ ArticlePage(data ArticlePageData) {
 7	@layouts.Base() {
 8		<div>
 9			<div>
10				<h1>{ data.Title }</h1>
11			</div>
12			<article>
13				@unsafe(data.Content)
14			</article>
15		</div>
16	}
17}

and run templ generate once again.

We can now go back and update our ArticleHandler handler:

 1func ArticleHandler(ctx echo.Context, storage ArticleStorage) error {
 2	postSlug := ctx.Param("postSlug")
 3	
 4	postModel, err := storage.GetPostBySlug(postSlug)
 5	if err != nil {
 6		return err
 7	}
 8	
 9	postContent, err := posts.Assets.ReadFile(postModel.Filename)
10	if err != nil {
11		return err
12	}
13
14	return views.ArticlePage(
15		views.ArticlePagedata{
16			Title: postModel.Title,
17			Content: postContent,
18		},
19	).Render(
20		ctx.Request().Context(), 
21		ctx.Response().Writer,
22	)
23}

If you run your application now and visit a valid URL, you should see a (rather ugly) page showing the markdown of your article but this time, with some sweet hypertext markup.

Making things (slightly) less ugly

In terms of styling things, throwing some tailwind or vanilla CSS at what we have now will get you a long way. But, we still show raw markdown to the user when they visit our articles. Additionally, we might want to show some nicely formatted code snippets in our articles. Let's fix this now.

For this, we need something that can transform the markdown into HTML components e.g

1## Some sub header

into

1<h2>Some sub header</h2>

Luckily, there already is a create library for this: Goldmark. So let's refactor the posts/posts.go file to parse the content we store using embed.

 1//go:embed *.md
 2var assets embed.FS // unexport assets
 3
 4type Manager struct {
 5	posts embed.FS
 6	markdownParser goldmark.Markdown
 7}
 8
 9func NewManager() Manager {
10	md := goldmark.New(
11		goldmark.WithParserOptions(
12			parser.WithAutoHeadingID(),
13			parser.WithAttribute(),
14		),
15		goldmark.WithRendererOptions(
16			html.WithHardWraps(),
17			html.WithXHTML(),
18			html.WithUnsafe(),
19		),
20	)
21
22	return Manager{
23		posts:           assets,
24		markdownHandler: md,
25	}
26}
27
28func (m *Manager) Parse(name string) (string, error) {
29	source, err := m.posts.ReadFile(name)
30	if err != nil {
31		return "", err
32	}
33
34	// Parse Markdown content
35	var htmlOutput bytes.Buffer
36	if err := m.markdownHandler.Convert(source, &htmlOutput); err != nil {
37		return "", err
38	}
39
40	return htmlOutput.String(), nil
41}

Lastly, update the ArticleHandler to use the Manager:

 1func ArticleHandler(
 2	ctx echo.Context, 
 3	storage ArticleStorage,
 4	postManager posts.Manager
 5) error {
 6	postSlug := ctx.Param("postSlug")
 7	
 8	postModel, err := storage.GetPostBySlug(postSlug)
 9	if err != nil {
10		return err
11	}
12	
13	postContent, err := postManager.Parse(postModel.Filename)
14	if err != nil {
15		return err
16	}
17	
18
19	return views.ArticlePage(
20		views.ArticlePagedata{
21			Title: postModel.Title,
22			Content: postContent,
23		},
24	).Render(
25		ctx.Request().Context(), 
26		ctx.Response().Writer,
27	)
28}

Try and edit your article by adding some code blocks and they will now get nicely formatted. You can add custom themes to the parser so your code snippets will be shown with your favorite theme.