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 = "---"
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
}
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>`
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{}
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)
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):]
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)
}
}
Output
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.
shindakun / atlg
Source repo for the "Attempting to Learn Go" posts I've been putting up over on dev.to
Attempting to Learn Go
Here you can find the code I've been writing for my Attempting to Learn Go posts that I've been writing and posting over on Dev.to.
Post Index
Enjoy this post? |
---|
How about buying me a coffee? |
Top comments (4)
Good tutorial but I wouldn't bother with server side templating anymore in 2019, APIs and SPAs FTW ...
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.
Im sorry but this just isn't true.
Yes maybe it isn't, as always "it depends".