DEV Community

Cover image for Attempting to Learn Go - Building Dev Log Part 02
Steve Layton
Steve Layton

Posted on

Attempting to Learn Go - Building Dev Log Part 02

Building Out A Dev Blog

When last we met we had just read in a markdown file and parsed its contents. Not too exciting but a necessary first step toward building a static site generator. This time around we're going to continue working toward that goal! First, we'll tweak our struct a bit, introduce a new function for some basic testing the loaded markdown, and then output some real live HTML. As usual, if you have not read the previous post I recommend it, we're going to just jump in here with the new code.


From The Top

We'll get started with our expanded set of imports. We're adding in html/template, BlackFriday, and BlueMonday. If you've read the previous post on sending an email you should be familiar with text/template, html/template is basically the same thing with added functions for dealing with HTML.

package main

import (
  "bytes"
  "fmt"
  "html/template"
  "io/ioutil"
  "os"
  "strings"

  "github.com/microcosm-cc/bluemonday"
  "github.com/russross/blackfriday"
  yaml "gopkg.in/yaml.v2"
)

const delim = "---"
Enter fullscreen mode Exit fullscreen mode

Structural Changes

We need to export the items inside our struct so the template can use that data that's fed into it. Note, that we're also setting up our PostBody to have a type of template.HTML. We'll get into that in a little bit.

type post struct {
  Title       string
  Published   bool
  Description string
  Tags        []string
  CoverImage  string
  Series      string
  PostBody    template.HTML
}
Enter fullscreen mode Exit fullscreen mode

HTML Template

Since we're stepping through this slowly, this version is only going to use a single built-in template. This is far from final but it's enough to show what we want to accomplish in this post. We'll move to load templates from files within a few iterations of the code.

var templ = `<!DOCTYPE html>
<html lang="en">
  <head>
    <title>{{.Title}}</title>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="HandheldFriendly" content="True">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="referrer" content="no-referrer-when-downgrade" />
    <meta name="description" content="{{.Description}}" />
  </head>
  <body>
    <div class="post">
      <h1>{{.Title}}</h1>
      {{.PostBody}}
    </div>
  </body>
</html>`
Enter fullscreen mode Exit fullscreen mode

If Is Nil Panic

The loadFile() function hasn't changed at all so far so we'll skip over that to isNil(). You may recall if you read the previous post that, as it was written, it was to easy to trigger a panic if the front matter wasn't set. isNil() is the start of a larger effort to ensure that our incoming data is set. We'll be extending it to return some proper errors and not just bomb and stop the execution. As it stands right now we're only using it down in main() but, not really since it's presently commented out. (cough)

func loadFile(s string) (b []byte, err error) {
  f, err := ioutil.ReadFile(s)
  if err != nil {
    return nil, err
  }
  return f, nil
}

func isNil(i interface{}) bool {
  if i != nil {
    return false
  }
  return true
}

func main() {
  f, err := loadFile("test.md")
  if err != nil {
    panic(err)
  }

  b := bytes.Split(f, []byte(delim))
  if len(b) < 3 || len(b[0]) != 0 {
    panic(fmt.Errorf("Front matter is damaged"))
  }

  m := make(map[string]interface{})
  err = yaml.Unmarshal([]byte(b[1]), &m)
  if err != nil {
    msg := fmt.Sprintf("error: %v\ninput:\n%s", err, b[1])
    panic(msg)
  }

  p := &post{}
Enter fullscreen mode Exit fullscreen mode

Is Nil Not Used

Here you can see the beginnings of where we'll use our isNil() code. I'm not very happy with it but, I was short on time so it's still in the works. I think that's alright for now since we're still working with a "known-good" test file. I'll have to give this part a closer look soon.

  // if isNil(m["title"]) {
  // in final we wouldn't actually panic just reject the file and continue on
  //   panic(err)
  // } else {
  p.Title = m["title"].(string)
  p.Published = m["published"].(bool)
  p.Description = m["description"].(string)

  // TODO: Strip space after comma prior to parse?
  tmp := m["tags"].(string)
  p.Tags = strings.Split(tmp, ", ")

  p.CoverImage = m["cover_image"].(string)
  p.Series = m["series"].(string)
Enter fullscreen mode Exit fullscreen mode

Post Body

Last time, I posed the question of how we could continue to use the file we already loaded to get the post body. Well, this is one way! We have the file data in f which is a []byte so we can take the length of our YAML header, b[1], and add it to the length of two times the front matter delimiter (---). We use that as the start and go until the end of f which gives us what we need, f[x:]. This code is probably too fragile for "production" but for now we are going to live dangerously, the split check and the fact that the YAML needs to parse correctly should at least give us some coverage for now - we'll need to revisit this in the future I think.

  pBody := f[len(b[1])+(len(delim)*2):]
Enter fullscreen mode Exit fullscreen mode

pBody in hand now we need to pass it into blackfriday. For now, we'll just have blackfriday's default markdown parser do its thing and return us a []byte which we turn around and pass into bluemonday. I'm doing this almost exactly as suggested in the blackfriday readme, making sure to use the policy to preserve fenced code blocks. bluemonday is used for HTML sanitization. Using this should help ensure anything that is unintentionally nefarious in our posts gets cleaned up. Finally, we put everything together and dump to standard out. So right now if we happen to be on the server we can run go run main.go > /var/www/dev.shindakun.net/index.html and have at least a one-page static website. Not super exciting but we're making steady progress.

  out := blackfriday.Run(pBody)

  bm := bluemonday.UGCPolicy()
  bm.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code")
  p.PostBody = template.HTML(bm.SanitizeBytes(out))

  t := template.Must(template.New("msg").Parse(templ))

  err = t.Execute(os.Stdout, p)
  if err != nil {
    panic(err)
  }
}
Enter fullscreen mode Exit fullscreen mode

Output

Image of homepage

That's right! The fruits of our labor are currently (as of the time of writing this) live over on dev.shindakun.net.


Next Time

We have a good bit of the basics out of the way but, we have a long way left to go. Next time around, I hope to have, loading and outputting of multiple files mostly implemented. That should put us in a good place to take a step back and get some more error handling in place. I should probably start thinking about adding tests too.


You can find the code for this and most of the other Attempting to Learn Go posts in the repo on GitHub.




Top comments (4)

Collapse
 
leob profile image
leob

Good tutorial but I wouldn't bother with server side templating anymore in 2019, APIs and SPAs FTW ...

Collapse
 
shindakun profile image
Steve Layton

We'll probably do an API based blog once we get through the static site generator I think. That will probably just return pure markdown with all the heavy lifting done in the browser. Not sure when I will get started on that though.

Collapse
 
quii profile image
Chris James

Im sorry but this just isn't true.

Collapse
 
leob profile image
leob

Yes maybe it isn't, as always "it depends".