Building a Blog in Go: Rendering Markdown as HTML

Converting Markdown to HTML

At this point my blog is reading files from disk and rendering their markdown content as plain text. My next step is going to be converting the markdown to proper HTML using some sort of markdown library.

After looking at a few library options, I decided to use goldmark. The main reason for this choice is that it is designed to be extensible. This means I can add custom markdown syntax to my posts and update the parser to handle them correctly, and it sounded like a fun thing to try out since I very commonly add <aside> blocks to my write-ups that will have additional optional information, like links to related resources or common bugs users encounter and how to address them.

I first installed the goldmark library.

go get github.com/yuin/goldmark

Then I updated my code to convert the markdown into HTML. This is done using the Convert function provided by goldmark. Finally, I copied the HTML to the http.ResponseWriter.

func PostHandler(sl SlugReader) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		slug := r.PathValue("slug")
		postMarkdown, err := sl.Read(slug)
		var buf bytes.Buffer
		err = goldmark.Convert([]byte(postMarkdown), &buf)
		if err != nil {
			http.Error(w, "Error converting markdown", http.StatusInternalServerError)
			return
		}
		if err != nil {
			// TODO: Handle different errors in the future
			http.Error(w, "Post not found", http.StatusNotFound)
			return
		}
		io.Copy(w, &buf)
	}
}

Now if I restart my Go server and visit a page like /posts/io-reader I will see the markdown being rendered as HTML.

Syntax Highlighting

Next I wanted to make my code look a little better. Most notably, I would like to use syntax highlighting. This is another reason I opted to use goldmark - it has a highlighting extension already built, so I just needed to install it and use it.

go get github.com/yuin/goldmark-highlighting/v2

Auto-imports for this library didn’t always work for me, but the import I used is shown below.

import (
	"bytes"
	"io"
	"log"
	"net/http"
	"os"

	"github.com/yuin/goldmark"
	highlighting "github.com/yuin/goldmark-highlighting/v2"
)

With the import in place I updated my PostHandler function to use the extension. To do this, I needed to stop using the goldmark’s Convert function, and instead use the New function. This allows me to create a new markdown converter with my own custom options.

func PostHandler(sl SlugReader) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		slug := r.PathValue("slug")
		postMarkdown, err := sl.Read(slug)
		if err != nil {
			// TODO: Handle different errors in the future
			http.Error(w, "Post not found", http.StatusNotFound)
			return
		}
		mdRenderer := goldmark.New(
			goldmark.WithExtensions(
				highlighting.Highlighting,
			),
    )
    var buf bytes.Buffer
		err = mdRenderer.Convert([]byte(postMarkdown), &buf)
		if err != nil {
			http.Error(w, "Error converting markdown", http.StatusInternalServerError)
			return
		}
		io.Copy(w, &buf)
	}
}

The highlighting library supports multiple themes, and I tend to use the Dracula theme, so I opted to use that for my Go blog as well.

mdRenderer := goldmark.New(
  goldmark.WithExtensions(
    highlighting.NewHighlighting(
      highlighting.WithStyle("dracula"),
    ),
  ),
)

This is far from perfect, but it is a great start.

Using a Layout

The next thing I want to focus on is adding a layout to the application. That way I can add links to other pages, add some custom styling, and more. To do this I opted to use Go’s html/template library.

I started by creating a template file.

code post.gohtml

I plan to provide each post with at least the following information:

I also intend to use Tailwind CSS to style everything, so I’m going to include it via their CDN. I also know from past experience that I will want their Typography plugin, which makes it really easy to style generated HTML like I will have from the markdown.

Putting this all together, plus a little copilot magic to speed things up, I generated the following HTML:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="https://cdn.tailwindcss.com?plugins=typography"></script>
  <title>{{.Title}} | Jon's Blog</title>
</head>
<body>
  <nav class="flex items-center justify-between bg-gray-800 p-6 mb-4">
    <div class="flex items-center flex-shrink-0 text-white mr-6">
      <span class="font-semibold text-xl tracking-tight">Jon's Blog</span>
    </div>
    <div class="block">
      <ul class="flex space-x-4">
        <li><a href="#" class="text-gray-300 hover:bg-gray-700 px-3 py-2 rounded">Home</a></li>
        <li><a href="#" class="text-gray-300 hover:bg-gray-700 px-3 py-2 rounded">About</a></li>
        <li><a href="#" class="text-gray-300 hover:bg-gray-700 px-3 py-2 rounded">Blog</a></li>
        <li><a href="#" class="text-gray-300 hover:bg-gray-700 px-3 py-2 rounded">Contact</a></li>
      </ul>
    </div>
  </nav>
  <div class="container mx-auto">
    <h1 class="text-4xl font-bold text-center">{{.Title}}</h1>
    <div class="text-center mt-4">
      <p class="text-gray-500">Author: {{.Author}}</p>
    </div>
    <div class="prose max-w-full">
      {{.Content}}
    </div>
  </div>
</body>
</html>

I then updated my PostHandler to use this new template:

func PostHandler(sl SlugReader) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
    // ...

    tpl, err := template.ParseFiles("post.gohtml")
		if err != nil {
			http.Error(w, "Error parsing template", http.StatusInternalServerError)
			return
		}
		err = tpl.Execute(w, PostData{
			Title:   "My First Post",
			Content: template.HTML(buf.String()),
			Author:  "Jon Calhoun",
		})
		// io.Copy(w, &buf)
	}
}

This gets my blog looking better, but the title and author are hardcoded. The first # in my markdown also conflicts a bit with the title I am rendering in my HTML. To fix both of these problems I am going to add code to parse frontmatter from my blog posts, which will contain various pieces of metadata we might need to render the post. That will have to wait until the next part in this series though!

See the source code so far here: https://github.com/joncalhoun/jonblog/tree/p2

Learn Web Development with Go!

Sign up for my mailing list and I'll send you a FREE sample from my course - Web Development with Go. The sample includes 19 screencasts and the first few chapters from the book.

You will also receive emails from me about Go coding techniques, upcoming courses (including FREE ones), and course discounts.

Avatar of Jon Calhoun
Written by
Jon Calhoun

Jon Calhoun is a full stack web developer who teaches about Go, web development, algorithms, and anything programming. If you haven't already, you should totally check out his Go courses.

Previously, Jon worked at several statups including co-founding EasyPost, a shipping API used by several fortune 500 companies. Prior to that Jon worked at Google, competed at world finals in programming competitions, and has been programming since he was a child.

More in this series

This post is part of the series, Exercise: Building a Blog in Go.

Spread the word

Did you find this page helpful? Let others know about it!

Sharing helps me continue to create both free and premium Go resources.

Want to discuss the article?

See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!

You can reach me via email or via twitter.

Recent Articles All Articles Mini-Series Progress Updates Tags About Me Go Courses

©2024 Jonathan Calhoun. All rights reserved.