fmt.Fprintf
, but in this chapter you'll learn that Go's standard library has some tools to generate HTML in a simpler and more maintainable way. You'll also learn more effective ways of testing this kind of code that you may not have run in to before.fs.FS
(a file-system), and return a slice of Post
for each markdown file it encountered.Post
Body
field in Post
is a string containing markdown so that should be converted to HTML.<html>
and a <head>
containing links to CSS stylesheets and whatever else we may want.io.Writer
. This means the caller of our code has the flexibility to:io.Writer
the user can generate some HTML from a Post
<h1>
. This feels like the smallest first step that can move us forward a bit.io.Writer
also makes testing simple, in this case we're writing to a bytes.Buffer
which we can then later inspect the contents.Render
function. Try and follow the compiler messages yourself and get to a state where you can run the test and see that it fails with a clear message.Post
.<head>
content and whatever page furniture we need.Package template (html/template) implements data-driven templates for generating HTML output safe against code injection. It provides the same interface as package text/template and should be used instead of text/template whenever the output is HTML.
text/template
to clean up your code.<h1>{{.Title}}</h1><p>{{.Description}}</p>Tags: <ul>{{range .Tags}}<li>{{.}}</li>{{end}}</ul>
Execute
method on it, passing in our data, in this case the Post
.{{.Description}}
with the content of p.Description
. Templates also give you some programming primitives like range
to loop over values, and if
. You can find more details in the text/template documentation.html/template
has definitely been an improvement, but having it as a string constant in our code isn't great:blog.gohtml
, paste our template into the file.Package embed provides access to files embedded in the running Go program.Go source files that import "embed" can use the //go:embed directive to initialize a variable of type string, []byte, or FS with the contents of files read from the package directory or subdirectories at compile time.
ApprovalTests allows for easy testing of larger objects, strings and anything else that can be saved to a file (images, sounds, CSV, etc...)
"github.com/approvals/go-approval-tests"
to your project and edit the test to the followingrenderer_test.TestRender.it_converts_a_single_post_into_HTML.received.txt
renderer_test.TestRender.it_converts_a_single_post_into_HTML.approved.txt
html
element, along with a head
, perhaps some nav
too. Usually there's an idea of a footer too.top.gohtml
with the followingbottom.gohtml
io.Writer
BenchmarkRender-8 362124 3131 ns/op
. The old NS per op were 53812 ns/op
, so this is a decent improvement! As we add other methods to render, say an Index page, it should simplify the code as we don't need to duplicate the template parsing.Body
. If you recall, that should be markdown that the author has written, so it'll need converting to HTML.Post
's title field as a part of the path of the URL, but we don't really want spaces in the URL so we're replacing them with hyphens.RenderIndex
method to our PostRenderer
that again takes an io.Writer
and a slice of Post
.href
attributes. We need a way to do a string replace of spaces with hyphens. We can't just loop through the []Post
and replace them in-memory because we still want the spaces displayed to the user in the anchors.template.FuncMap
into your template, which allows you to define functions that can be called within your template. In this case we've made a sanitiseTitle
function which we then call inside our template with {{sanitiseTitle .Title}}
.Post
and then call that in the templateSanitisedTitle
method to Post
. This would simplify the template and we could easily unit test this logic separately if we wish. This is probably the easiest solution, although not necessarily the simplest.PostViewModel
with exactly the data we needPost
, it instead takes a view model.[]Post
to []PostView
, generating the SanitizedTitle
. A way to keep this clean would be to have a func NewPostView(p Post) PostView
which would encapsulate the mapping.Post
templates/index.gohtml
) and load it once, when we construct our renderer.templ
we now have to call ExecuteTemplate
and specify which template we wish to render as appropriate, but hopefully you'll agree the code we've arrived at looks great.HTML encapsulates a known safe HTML document fragment. It should not be used for HTML from a third-party, or HTML with unclosed tags or comments. The outputs of a sound HTML sanitiser and a template escaped by this package are fine for use with HTML.Use of this type presents a security risk: the encapsulated content should come from a trusted source, as it will be included verbatim in the template output.
postViewModel
), because I still viewed this as internal implementation detail to rendering. I have no need to test this separately and I don't want it polluting my API.Body
into HTMLBody
and then I use that field in the template to render the HTML.text/template
to generate other kinds of data from a template. If you find yourself needing to transform data into some kind of structured output, the techniques laid out in this chapter can be useful.