Building a blog in Go Part 3

Let's recap what we have done so far in Parts 1 and 2. We have a server that parses markdown files, renders HTML templates, and displays each post. We are converting the markdown into HTML and showing that to the user. There is still plenty of room for improvement.

As it stands, our server starts up but you would have no idea. Let's add some code to the main.go file that shows what port our server is running on.

Add this line right above the call to the listenAndServe() function at the bottom of the main.go

1fmt.Printf("starting server on port :%d\n", cfg.port)

Now, when we launch our server, we will actually see the port that it is running on.

Adding some style

This blog is in need of styling. I am about as far as it gets from being a decent designerβ€”I have zero creative ability and I am color blind to boot. Don't get your hopes up that this site is going to be anything special. Open up the index.tmpl and add the following code:

 1<!DOCTYPE html>
 2<html lang="en">
 3<head>
 4  <meta charset="UTF-8">
 5  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 6  <title>Sample Blog</title>
 7   <style>
 8   body {
 9    font-family: 'Segoe UI', Arial, sans-serif;
10    background: #18181b;
11    margin: 0;
12    padding: 2rem;
13    color: #f3f4f6;
14    min-height: 100vh;
15    display: flex;
16    align-items: center;
17    justify-content: center;
18  }
19  .container {
20    display: flex;
21    flex-direction: column;
22    max-width: 600px;
23    width: 100%;
24    padding: 2.5rem 2rem;
25  }
26  h1 {
27    font-size: 2.5rem;
28    font-weight: 700;
29    margin-bottom: 1.5rem;
30    color: #fafafa;
31  }
32  p {
33    font-size: 1.25rem;
34    color: #e5e7eb;
35    margin-bottom: 2rem;
36  }
37  ul {
38    list-style: none;
39    padding: 0;
40    margin: 0 0 1rem 0;
41  }
42  li {
43    margin-bottom: 0.75rem;
44  }
45  a {
46    display: inline-block;
47    margin-right: 1rem;
48    margin-bottom: 0.5rem;
49    text-decoration: none;
50    color: #60a5fa;
51    font-size: 1.1rem;
52    transition: color 0.2s, border-bottom 0.2s;
53    border-bottom: 2px solid transparent;
54    font-weight: 500;
55    letter-spacing: 0.02em;
56  }
57  a:hover {
58    color: #38bdf8;
59    border-bottom: 2px solid #38bdf8;
60  }
61  </style>
62</head>
63<body>
64  <div class="container">
65    <h1>Hi πŸ‘‹</h1>
66    <p>Welcome to our blog! Here you will find a collection of posts for all things interesting. I hope you enjoy!</p>
67    {{ range .Urls }}
68    <ul>
69      <li><a href="/posts/{{.}}">{{.}}</a></li>
70    </ul> 
71  {{ end }}
72  </div>
73</body>
74</html>

Go ahead and look at the changes. You should see something that looks a little bit better.

We have some issues now. Can you spot them? It is quite a bit more obvious now that we have a dark theme. The issue is that we are not rendering a template when we go to a blog post; instead, we just get some unstyled HTML. The reason for this is because we are simply outputting HTML from the markdown content. It is not part of our template set and is not getting the styles we added.

There are a few ways to solve this, but the easiest is to just create a structure that will likely be used for future projects.

In the terminal create two more .tmpl files. We will then create a template folder and move all of our templates into that folder.

1touch base.tmpl post.tmpl
2mkdir templates
3mv *.tmpl templates/ 
4mv templates/index.tmpl templates/home.tmpl

Now add the following content to each of the templates (note the filename of each at the top).

 1<!-- base.tmpl -->
 2{{define "base"}}
 3<!DOCTYPE html>
 4<html lang="en">
 5<head>
 6  <meta charset="UTF-8">
 7  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 8  <title>Sample Blog</title>
 9   <style>
10  </style>
11</head>
12<body>
13  <div class="container">
14    {{template "content" .}}
15  </div>
16</body>
17</html>
18{{end}}
19
 1<!-- home.tmpl -->
 2{{define "content"}}
 3  <h1>Sample Blog</h1>
 4  <p>Welcome to our blog! Here you will find a collection of posts for all things interesting. I hope you enjoy!</p>
 5    {{ range .Urls }}
 6      <ul>
 7        <li><a href="/posts/{{.}}">{{.}}</a></li>
 8      </ul>
 9    {{end}}
10{{end}}
1<!-- post.tmpl -->
2{{define "content"}}
3    <article class="post">
4      <div class="post-body">
5        {{.Content}}
6      </div>
7    </article>
8{{end}}

Your folder structure and template layout should look like this.

 1project-root/
 2β”œβ”€β”€ main.go
 3β”œβ”€β”€ content/
 4β”‚   └── hotdogs.md
 5    └── tacos.md
 6    └── icecream.md
 7└── templates/
 8  β”œβ”€β”€ base.tmpl
 9  β”œβ”€β”€ home.tmpl
10  └── post.tmpl

We have made some big changes here...

We have modified our template structure to contain a base template, a home template, and a post template. The home and base templates will render together as a template set, as will the post and base templates. A couple of key items to note:

  • {{define "content"}} defines a template called content
  • {{template "content" .}} calls a defined template called content and passes the context or dot operator. All of this is a fancy way to say data. We are going to pass a struct into the template.
  • If you do pass a struct, the fields need to be exported (start with an uppercase letter)
  • The call to {{.Content}} is a call to the Content field in the struct (it is poorly named considering our template names)
  • Template parsing order matters! You want to parse the base template first, as that template calls on other templates.
  • log.Fatal() will exit if an error is thrown. Seems appropriate considering all this app does is render markdown content.

Moving on...

We need to make some modifications to our main.go file, as we have three templates and each needs to be parsed in a specific way. Go to main.go and make these changes:

 1// create a home template set by parsing both base and home
 2 homeTmpl, err := template.ParseFiles(
 3  "templates/base.tmpl",
 4  "templates/home.tmpl",
 5 )
 6 if err != nil {
 7  log.Fatalf("failed to parse home templates: %v", err)
 8 }
 9
10 // create a post template set by parsing both base and post
11 postTmpl, err := template.ParseFiles(
12  "templates/base.tmpl",
13  "templates/post.tmpl",
14 )
15 if err != nil {
16  log.Fatalf("failed to parse post templates: %v", err)
17 }

We are creating two template sets: our home template set, which generates a template out of both the home.tmpl and base.tmpl files, and a post template set generated from post.tmpl and base.tmpl. This ensures that when we render our markdown content in the post template, we have our styling applied from the base template.

Now, in our / route handler, make these changes:

1 // handler for default / route
2 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
3  if err := homeTmpl.ExecuteTemplate(w, "base", homePageData); err != nil {
4   http.Error(w, err.Error(), http.StatusInternalServerError)
5  }
6 })

Let's create a simple helper that helps create a title for our post pages. This simply takes in the slug, strips any file extensions and dashes, and puts it in title case. We will use this to pass into our template's title field.

1func slugToTitle(slug string) string {
2 slug = strings.TrimSuffix(slug, ".md") // remove the suffix
3 slug = strings.ReplaceAll(slug, "-", " ") // remove dashes with spaces
4 caser := cases.Title(language.AmericanEnglish) // create a caser
5 return caser.String(slug) // convert to upper case
6}

Note that the cases package is not part of the standard library. You will need to add the package with this command:

1go get golang.org/x/text/cases

And make this change in our /posts/{slug} handler:

 1// handler for /posts/slug route
 2 http.HandleFunc("/posts/{slug}", func(w http.ResponseWriter, r *http.Request) {
 3  slug := r.PathValue("slug")
 4  if slug == "" {
 5   http.Error(w, "Post Not Found", http.StatusNotFound)
 6   return
 7  }
 8
 9  data, found := markdownCache[slug]
10  if !found {
11   http.Error(w, "Post Not Found", http.StatusNotFound)
12   return
13  }
14
15  var buf bytes.Buffer
16  if err := goldmark.Convert(data, &buf); err != nil {
17   log.Printf("error converting markdown for %q: %v", slug, err)
18   http.Error(w, "Something went wrong rendering this post", http.StatusInternalServerError)
19   return
20  }
21
22  p := PostPageData{
23   Title:   slugToTitle(slug),
24   Content: template.HTML(buf.String()),
25  }
26
27  // set the content type
28  w.Header().Set("Content-Type", "text/html")
29  if err := postTmpl.ExecuteTemplate(w, "base", p); err != nil {
30   fmt.Printf("Error rendering template %v\n", err)
31   return
32  }
33 })

That should be all the changes. We should have a working solution that now has our styles in both our home page and our posts. I hope these posts give you an idea of what you can do here. Some suggestions for improvement:

  • We have no tests; we should be testing the handlers and ensuring they render our content
  • There are a ton of neat features with Markdown parsing, such as Frontmatter and code highlighting. If we added Frontmatter to these posts, it would likely break things.
  • If you wanted things to be more flexible regarding styling, you could add a configuration file in JSON or TOML and read the configuration into a struct, then direct it to various style sheets or HTML layouts.

I have one last post where we will do a little extra credit. We will add testing, refactor our code a little bit, and add Frontmatter support.