Tutorials

Build a Blog With Go Templates

This article was written by an external contributor.

Aniket Bhattacharyea

Mathematics postgraduate who has a passion for computers and software.

Website

Go templates are a robust feature used to generate text or HTML outputs based on data in a Go program. You can customize how the data is displayed by passing an object to a template. Templates are often used to generate web pages, emails, and other text-based outputs. A very popular real-life use of a Go template is in the kubectl command line tool, where you can pass a template to the --template flag to customize the output to your needs.

Templates overview

In Go, there are two packages that provide the templating functionality: the text/template and html/template packages. Both offer the exact same set of interfaces; the only difference is that the latter automatically secures the HTML output against various attacks. This makes html/template a better choice for generating HTML output, and is why this article will use the html/template package.

A template is simply a string with some special commands called actions, which are enclosed in double curly braces. The actions are used to access or evaluate data, or to control the template’s structure.

Here’s an example template:

tmpl := "Hello {{.Name}}!"

The above template has a single action that prints the value of the Name field of the data object passed to the template. The . character in the action refers to the data object passed to the template, and .Name accesses the Name field of the object.

To render the template, you must parse the template with the Parse function and use the Execute function, which takes a writer and the data object as arguments and writes the output to the writer.

// Defining the data to pass
type User struct {
    Name string
}

user := User{"James"}
t, err := template.New("test").Parse(tmpl)
if err != nil {
    panic(err)
}
err = t.Execute(os.Stdout, user)
if err != nil {
    panic(err)
}

The above outputs Hello James! to the console.

Apart from accessing data, you can use actions like if to conditionally render content, and range to iterate over a collection. You can also define your own functions and use them in the template. A complete overview of templates can be found here.

Building a Blog With Templates

To follow along with the tutorial, you need to have Go installed and set up on your system. You’ll also want to install GoLand.

You can find the code for this tutorial on GitHub. Feel free to clone and explore the code, or to follow along with the tutorial to create the application from scratch.

Creating a new project

[To the top]

Start GoLand and click on the New project button. Choose the Go option and provide a name, such as “GoBlog”.

Click on Create to create the project. Create a file main.go at the root of the project with the following content:

package main

import (
    "database/sql"
    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
    _ "github.com/mattn/go-sqlite3"
)

You’ll notice the modules are highlighted in red. This is because these modules have not been downloaded yet.

GoLand can automatically download them for you. Simply hover over any highlighted entries and click on Sync dependencies of GoBlog in the popup that appears.

After a few seconds, the modules will be downloaded, and the red highlighting will disappear.

Please note that if you have Auto-format on save enabled, as soon as the file is saved (for example, when the window loses focus), GoLand will format the code and remove the unused dependencies. You can disable autosaving when switching to a different application or a built-in terminal in Preference / Settings | Tools | Actions on Save | Configure autosave options… | Autosave.

You can also add imports manually. The Sync Dependencies action downloads the packages to cache memory, and when you add imports manually, you get autocomplete suggestions.

As the first step of coding the server, declare the following two global variables in main.go:

var router *chi.Mux
var db *sql.DB

While you can use the standard library to set up the REST server, this article will use the chi router for this purpose – chi is an easy-to-use, lightweight, and superfast router that comes bundled with many features.

router will store the chi router instance, and db will store the database object.

Define the Article struct, which will have a title, content, and ID:

type Article struct {
    ID      int           `json:"id"`
    Title   string        `json:"title"`
    Content template.HTML `json:"content"`
}

Note that for Content, you’re using template.HTML instead of string. Because the content will be rich text and rendered as HTML, you need to use template.HTML to prevent the HTML from being escaped when the templates are rendered.

Setting up the database

[To the top]

Create the file db.go, which will house the functionalities related to the database. Start by importing the necessary module:

package main

import (
    "database/sql"
)

In this article, you’ll use the database/sql package to interact with the database. However, the templating system is unaffected by your choice of database package; you can use it with any other database package that you like.

This tutorial will use the SQLite database to store the data, but you can use any other SQL database, such as MySQL or PostgreSQL. A complete list of databases supported by database/sql can be found here.

Define the connect function, which will create the initial connection to the database. You will be using a local file named data.sqlite to store the data. If the file doesn’t exist, it will be created automatically.

func connect() (*sql.DB, error) {
    var err error
    db, err = sql.Open("sqlite3", "./data.sqlite")
    if err != nil {
        return nil, err
    }

    sqlStmt := `
    create table if not exists articles (id integer not null primary key autoincrement, title text, content text);
    `

    _, err = db.Exec(sqlStmt)
    if err != nil {
        return nil, err
    }

    return db, nil
}

You’ll notice the SQL statement is highlighted in GoLand.

GoLand has a built-in database plugin that can connect to different databases and lets you query, create, and manage tables, all without leaving the IDE. To use the plugin, you need to configure a data source. Simply click on the highlighted SQL statement, and in the context actions menu (the yellow bulb icon), click Configure data source. Alternatively, you can click in highlighted area and press Alt+Enter (⌥ ↩) to see available intention actions.

In the dialog that appears, click on + to add a new data source, and select SQLite as the database type. Provide a name for the database, such as “Database”, and use data.sqlite as the file name. If the appropriate database driver is not installed, you’ll be prompted to install it. Once ready, click on OK to save the data source.

GoLand will create the file if it does not exist, connect to it, and open a database console. This is where you can write the SQL queries. You can also see the newly created database in the Database tool window on the right side.

Return to the code, place the cursor on the create table SQL statement, and press Ctrl+Enter (⌘↩). Select the console from the popup menu, and GoLand will run the query in the console.

You will see from the logs that the table has been successfully created; this will also be reflected in the database tool window.

Now let’s write the rest of the database functions. The dbCreateArticle() function will create a new article in the database from an Article struct:

func dbCreateArticle(article *Article) error {
    query, err := db.Prepare("insert into articles(title,content) values (?,?)")
    defer query.Close()

    if err != nil {
        return err
    }
    _, err = query.Exec(article.Title, article.Content)

    if err != nil {
        return err
    }

    return nil
}

Here, you have a prepared statement that will be used to insert the article into the database. The ? placeholders will be replaced by the values of the Title and Content fields of the Article struct. You’ll notice GoLand correctly identifies the embedded SQL statement and highlights it.

Like before, you can run the query by placing the cursor on the SQL statement and pressing Ctrl+Enter (⌘↩). This time, you’ll be prompted to provide values for the placeholder. Enter the values and click on Execute.

Note that you need to add quotes to the values, since these will be substituted verbatim.

You can double-click the table name in the database tool window to see all the rows in the database. The newly created article should also show up here.

The dbGetAllArticles() function will return all the articles in the database as a slice of Article structs:

func dbGetAllArticles() ([]*Article, error) {
    query, err := db.Prepare("select id, title, content from articles")
    defer query.Close()

    if err != nil {
        return nil, err
    }
    result, err := query.Query()

    if err != nil {
        return nil, err
    }
    articles := make([]*Article, 0)
    for result.Next() {
        data := new(Article)
        err := result.Scan(
            &data.ID,
            &data.Title,
            &data.Content,
        )
        if err != nil {
            return nil, err
        }
        articles = append(articles, data)
    }

    return articles, nil
}

The dbGetArticle() function will return a single article from the database based on the ID:

func dbGetArticle(articleID string) (*Article, error) {
    query, err := db.Prepare("select id, title, content from articles where id = ?")
    defer query.Close()

    if err != nil {
        return nil, err
    }
    result := query.QueryRow(articleID)
    data := new(Article)
    err = result.Scan(&data.ID, &data.Title, &data.Content)

    if err != nil {
        return nil, err
    }

    return data, nil
}

The final pieces of the puzzle are the dbUpdateArticle() and dbDeleteArticle() functions, which will update and delete an article from the database, respectively:

func dbUpdateArticle(id string, article *Article) error {
    query, err := db.Prepare("update articles set (title, content) = (?,?) where id=?")
    defer query.Close()

    if err != nil {
        return err
    }
    _, err = query.Exec(article.Title, article.Content, id)

    if err != nil {
        return err
    }

    return nil
}

func dbDeleteArticle(id string) error {
    query, err := db.Prepare("delete from articles where id=?")
    defer query.Close()

    if err != nil {
        return err
    }
    _, err = query.Exec(id)

    if err != nil {
        return err
    }

    return nil
}

Creating the routes

[To the top]

With the database functions complete, return to main.go to write the rest of the server. Start by writing the catch() function, which simply panics in case of an error:

func catch(err error) {
    if err != nil {
        fmt.Println(err)
        panic(err)
    }
}

The main() function is where you will set up the routes and the middlewares. Add the following code in main.go:

func main() {
    router = chi.NewRouter()
    router.Use(middleware.Recoverer)

    var err error
    db, err = connect()
    catch(err)

    router.Use(ChangeMethod)
    router.Get("/", GetAllArticles)
    router.Route("/articles", func(r chi.Router) {
        r.Get("/", NewArticle)
        r.Post("/", CreateArticle)
        r.Route("/{articleID}", func(r chi.Router) {
            r.Use(ArticleCtx)
            r.Get("/", GetArticle) // GET /articles/1234
            r.Put("/", UpdateArticle)    // PUT /articles/1234
            r.Delete("/", DeleteArticle) // DELETE /articles/1234
            r.Get("/edit", EditArticle) // GET /articles/1234/edit
        })
    })

    err = http.ListenAndServe(":8005", router)
    catch(err)
}

Note the use of the Recoverer middleware. When the catch() function panics, this middleware will recover the server, log the error with a stack trace, and send a 500 Internal Server Error response to the client.

The above code sets up the following routes:

  • GET /: Displays all the articles in the database.
  • GET /articles: Displays the form to create a new article.
  • POST /articles: Creates a new article in the database.
  • GET /articles/{articleID}: Displays a single article.
  • PUT /articles/{articleID}: Updates an article in the database.
  • DELETE /articles/{articleID}: Deletes an article from the database.
  • GET /articles/{articleID}/edit: Displays the form to edit an article.

In addition to the routes, the code sets up two middlewares:

  • ChangeMethod: This middleware will change the request method to PUT or DELETE if the request method is POST and the form field _method is set to PUT or DELETE, respectively. This is required because HTML forms only support GET and POST methods.
  • ArticleCtx: This middleware will fetch the article from the database and store it in the request context. It will be used by the routes under the /articles/{articleID} path.

Let’s write the ChangeMethod function first. As mentioned before, it will look for the _method form element and change the request method accordingly:

func ChangeMethod(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Method == http.MethodPost {
            switch method := r.PostFormValue("_method"); method {
            case http.MethodPut:
                fallthrough
            case http.MethodPatch:
                fallthrough
            case http.MethodDelete:
                r.Method = method
            default:
            }
        }
        next.ServeHTTP(w, r)
    })
}

The ArticleCtx middleware will access the article ID from the URL parameters and fetch the article from the database. If the article is found, it will be stored in the request context. If the article is not found, the middleware will return a 404 status code:

func ArticleCtx(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        articleID := chi.URLParam(r, "articleID")
        article, err := dbGetArticle(articleID)
        if err != nil {
            fmt.Println(err)
            http.Error(w, http.StatusText(404), 404)
            return
        }
        ctx := context.WithValue(r.Context(), "article", article)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

The GetAllArticles() function will use the dbGetAllArticles() function to fetch all the articles from the database, and display them on a webpage by rendering a template. For now, let’s leave the rendering part empty:

func GetAllArticles(w http.ResponseWriter, r *http.Request) {
    articles, err := dbGetAllArticles()
    catch(err)
    fmt.Println(articles)

    //TODO: Render template
}

The NewArticle() function will simply display the form to create a new article. It doesn’t need to interact with the database, and as such, it’s pretty bare bones:

func NewArticle(w http.ResponseWriter, r *http.Request) {
    //TODO: Render template
}

The CreateArticle() function will be called when the form to create a new article has been submitted. It will extract the Title and Content fields from the form, and use the dbCreateArticle() function to create a new article in the database. After that, it will redirect the user to the / page:

func CreateArticle(w http.ResponseWriter, r *http.Request) {
    title := r.FormValue("title")
    content := r.FormValue("content")
    article := &Article{
        Title:   title,
        Content: template.HTML(content),
    }

    err := dbCreateArticle(article)
    catch(err)
    http.Redirect(w, r, "/", http.StatusFound)
}

The GetArticle() function will display a single article. Thanks to the ArticleCtx middleware, you don’t need to fetch the article here – you can simply get it from the request context:

func GetArticle(w http.ResponseWriter, r *http.Request) {
    article := r.Context().Value("article").(*Article)
    fmt.Println(article)
    //TODO: Render template
}

The EditArticle function will display the form to edit an article:

func EditArticle(w http.ResponseWriter, r *http.Request) {
    article := r.Context().Value("article").(*Article)
    fmt.Println(article)
    // TODO: Render template
}

The UpdateArticle() function will be called when the form to edit an article has been submitted. It will extract the Title and Content fields from the form, and use the dbUpdateArticle() function to update the article in the database. After that, it will redirect the user to the /articles/{articleID} page:

func UpdateArticle(w http.ResponseWriter, r *http.Request) {
    article := r.Context().Value("article").(*Article)

    title := r.FormValue("title")
    content := r.FormValue("content")
    newArticle := &Article{
        Title:   title,
        Content: template.HTML(content),
    }

    err := dbUpdateArticle(strconv.Itoa(article.ID), newArticle)
    catch(err)
    http.Redirect(w, r, fmt.Sprintf("/articles/%d", article.ID), http.StatusFound)
}

Finally, the DeleteArticle() function will delete the article from the database and redirect the user to the / page:

func DeleteArticle(w http.ResponseWriter, r *http.Request) {
    article := r.Context().Value("article").(*Article)
    err := dbDeleteArticle(strconv.Itoa(article.ID))
    catch(err)

    http.Redirect(w, r, "/", http.StatusFound)
}

Rendering templates

[To the top]

The routes are now ready; the next step is to render the templates. Create a templates directory with an index.html file within it, which will be rendered in the GetAllArticles() function. Add the following code to the file:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>All articles</title>
</head>
<body>
{{if eq (len .) 0}}
Nothing to see here
{{end}}
{{range .}}
<div>
    <a href="/articles/{{.ID}}">{{.Title}}</a>
</div>
{{end}}
<p>
  <a href="/articles">Create new article</a>

</p>
</body>
</html>

This template utilizes the if and range template functions. The if function will check if the number of articles equals zero. If it is, it will display the Nothing to see here message; otherwise, it will display the list of articles. The range function will loop through the list of articles and display them. The . variable refers to the slice of all articles passed to the template. Within the body of the range function, the . variable refers to a single article. The ID and Title fields of the article are accessed using the dot notation.

Let’s update the GetAllArticles() function to render the template:

func GetAllArticles(w http.ResponseWriter, r *http.Request) {
    articles, err := dbGetAllArticles()
    catch(err)

    t, _ := template.ParseFiles("templates/index.html")
    err = t.Execute(w, articles)
    catch(err)
}

Let’s see how the app looks so far. Right click on the project name in the Project sidebar, then go to the Run menu item and click go build GoBlog. This will build the project and run it.

Open your browser and navigate to http://localhost:8005. You should see the article that you created previously.

Let’s now create the form to create a new article. Since the Content field is a rich text, you’ll need a rich text editor. This article will use the TinyMCE editor. You’ll need to sign up for a free account to get an API key. Once you have the API key, create a new.html file in the templates directory and add the following code to it:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Create a new article</title>
    <script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
    <script>
        tinymce.init({
            selector: '#mytextarea',
        });
    </script>
</head>
<body>
<h1>Create a new article</h1>
<form method="post" action="/articles">
    <input id="title" type="text" name="title" placeholder="Enter the title">
    <textarea id="mytextarea" name="content"></textarea>
    <button id="submit" type="submit">Create</button>
</form>
</body>
</html>

You’ll need to replace no-api-key in the above code with your TinyMCE API key.

Note that this file simply displays a form. It’s not strictly a template, but rather a simple HTML file, so you don’t need to parse it using the template.ParseFiles() function. You can simply use the http.ServeFile() function to serve it. Add the following line to the NewArticle() function:

http.ServeFile(w, r, "templates/new.html")

You can visit http://localhost:8005/articles to see the form in action.

Make sure you restart the server if it is running. To do this, use the restart button next to the configurations menu.

Whenever you change Go files, you need to restart the server. This is not explicitly mentioned in the tutorial.

Nested templates

[To the top]

You may have noticed that the two templates you’ve created so far share a large chunk of code. The basic structure of the HTML page is the same in both templates – the only difference is the title, content, and scripts of the page. You can use nested templates to avoid repeating the same code in multiple templates. Create a base.html file in the templates directory and add the following code to it:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>{{ template "title" . }}</title>
  {{ template "scripts" }}
</head>
<body>
{{ template "body" . }}
</body>
</html>

This template uses three other templates: title, scripts, and body. You’ll define different versions of these templates, which will be substituted in base.html. These templates will be used to display the title, scripts, and the body of the page. Also, note that the title and body templates pass the current object using the . variable, enabling the nested templates to access it if needed.

Replace the contents of index.html with the following code:

{{define "title"}}All articles{{end}}
{{define "scripts"}}{{end}}
{{define "body"}}
{{if eq (len .) 0}}
Nothing to see here
{{end}}
{{range .}}
<div>
    <a href="/articles/{{.ID}}">{{.Title}}</a>
</div>
{{end}}
<p>
    <a href="/articles">Create new article</a>

</p>
{{end}}

Using the define function, you can define the nested templates. Here, you’ve simply extracted the title, body, and scripts (which are empty because this page did not load any scripts) into their own templates.

To make this work, you need to amend the GetAllArticles() function to load the base.html template in addition to index.html:

t, _ := template.ParseFiles("templates/base.html", "templates/index.html")

Note the order of the templates. base.html must come before index.html.

Replace new.html with the following code:

{{define "title"}}Create new article{{end}}
{{define "scripts"}}
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
<script>
    tinymce.init({
        selector: '#mytextarea',
    });
</script>
{{end}}
{{define "body"}}
<form method="post" action="/articles">
    <input type="text" name="title" placeholder="Enter the title">
    <textarea id="mytextarea" name="content"></textarea>
    <button id="submit" type="submit">Create</button>
</form>
{{end}}

Since new.html is now a template, http.ServeFile won’t work anymore. You need to parse the template and execute it:

func NewArticle(w http.ResponseWriter, r *http.Request) {
    t, _ := template.ParseFiles("templates/base.html", "templates/new.html")
    err := t.Execute(w, nil)
    catch(err)
}

At this point, you can create a new article using the form.

After clicking on Create, you’ll be redirected to the root URL, and the newly created article will appear in the list.

Observe that the rich text is stored as HTML in the database.

Create a file named article.html, which will display a single article:

{{define "title"}}{{.Title}}{{end}}
{{define "scripts"}}{{end}}
{{define "body"}}
<h1>{{.Title}} </h1>
<div>
  {{.Content}}
</div>
<div>
  <a href="/articles/{{.ID}}/edit">Edit</a>
  <form action="/articles/{{.ID}}" method="post">
    <input type="hidden" name="_method" value="DELETE">
    <button type="submit">Delete</button>
  </form>
</div>
{{end}}

This page has a link to the edit page and a form to delete the article. Note the hidden _method element. As discussed before, the presence of this element will convert the request to a DELETE request courtesy of the ChangeMethod middleware.

Modify the GetArticle function to render the template:

func GetArticle(w http.ResponseWriter, r *http.Request) {
    article := r.Context().Value("article").(*Article)
    t, _ := template.ParseFiles("templates/base.html", "templates/article.html")
    err := t.Execute(w, article)
    catch(err)
}

Clicking on the title of an article on the home page will now display the article.

Image uploading

[To the top]

TinyMCE supports image uploading by default. You can drag and drop images into the editor, and they’ll be converted into base64 strings and stored as part of the Content field.

The image will show up on the article page, as well.

However, it’s not considered good practice to store images as base64 strings, so let’s add support for image uploading.

Add two new routes to the router:

func main() {
    ...
    router.Use(ChangeMethod)
    router.Get("/", GetAllArticles)
    router.Post("/upload", UploadHandler) // Add this
    router.Get("/images/*", ServeImages) // Add this
    router.Route("/articles", func(r chi.Router) {
        ...
    })

    ...
}

The /upload route will handle the image upload and store them as files in the images directory, and the /images/* route will serve the images.

Let’s write the UploadHandler function:

func UploadHandler(w http.ResponseWriter, r *http.Request) {
    const MAX_UPLOAD_SIZE = 10 << 20 // Set the max upload size to 10 MB
    r.Body = http.MaxBytesReader(w, r.Body, MAX_UPLOAD_SIZE)
    if err := r.ParseMultipartForm(MAX_UPLOAD_SIZE); err != nil {
        http.Error(w, "The uploaded file is too big. Please choose a file that's less than 10MB in size", http.StatusBadRequest)
        return
    }

    file, fileHeader, err := r.FormFile("file")
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    defer file.Close()

    // Create the uploads folder if it doesn't already exist
    err = os.MkdirAll("./images", os.ModePerm)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // Create a new file in the uploads directory
    filename := fmt.Sprintf("/images/%d%s", time.Now().UnixNano(), filepath.Ext(fileHeader.Filename))
    dst, err := os.Create("." + filename)
    if err != nil {
        fmt.Println(err)
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    defer dst.Close()

    // Copy the uploaded file to  the specified destination
    _, err = io.Copy(dst, file)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    fmt.Println(filename)
    response, _ := json.Marshal(map[string]string{"location": filename})
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    w.Write(response)
}

This function creates a new file in the images directory and copies the incoming file to this new file. It then returns the file’s location as a JSON response, which TinyMCE uses to link the image.

The ServeImages function is pretty straightforward:

func ServeImages(w http.ResponseWriter, r *http.Request) {
    fmt.Println(r.URL)
    fs := http.StripPrefix("/images/", http.FileServer(http.Dir("./images")))
    fs.ServeHTTP(w, r)
}

It simply serves the files in the images directory as static files.

The final step is to let TinyMCE know about the /upload route. In new.html, modify the tinymce.init call with the following:

tinymce.init({
    selector: '#mytextarea',
    plugins: 'image',
    toolbar: 'undo redo | blocks | image | ' +
        'bold italic backcolor | alignleft aligncenter ' +
        'alignright alignjustify | bullist numlist outdent indent | ' +
        'removeformat | help',
    images_upload_url: "/upload",
    relative_urls : false,
    remove_script_host : false,
    convert_urls : true,
});

The plugins option loads the image upload plugin, and the toolbar option adds the image upload button to the toolbar. The images_upload_url option specifies the route to which the image will be uploaded. The relative_urls, remove_script_host, and convert_urls options are used to convert the relative URLs returned by the /upload route to absolute URLs.

The new article page will now show the image upload button in the toolbar.

Click on the Upload tab and upload any image you want.

It will be uploaded and linked to the article.

Finally, create the template edit.html with the following code:

{{define "title"}}Create new article{{end}}
{{define "scripts"}}
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
<script>
    tinymce.init({
        selector: '#mytextarea',
        plugins: 'image',
        toolbar: 'undo redo | blocks | image | ' +
            'bold italic backcolor | alignleft aligncenter ' +
            'alignright alignjustify | bullist numlist outdent indent | ' +
            'removeformat | help',
        images_upload_url: "/upload",
        relative_urls : false,
        remove_script_host : false,
        convert_urls : true,
    });
</script>
{{end}}
{{define "body"}}
<form method="post" action="/articles/{{.ID}}">
  <input type="text" name="title" value="{{.Title}}">
  <textarea id="mytextarea" name="content">{{.Content}}</textarea>
  <input type="hidden" name="_method" value="PUT">
  <button id="submit" type="submit" onclick="submitForm()">Edit</button>
</form>
{{end}}

It’s very similar to the new article form, except it populates the form with the article’s title and content. The action attribute of the form is set to /articles/{id}, and the _method field is set to PUT to indicate that the form is used to edit an article.

Render this template in the EditArticle function:

func EditArticle(w http.ResponseWriter, r *http.Request) {
    article := r.Context().Value("article").(*Article)

    t, _ := template.ParseFiles("templates/base.html", "templates/edit.html")
    err := t.Execute(w, article)
    catch(err)
}

You can now click on the Edit button on the article page to edit the article.

After editing, you’ll be redirected to the same page, now displaying the updated article.

You can also click on the Delete button to delete the article.

Conclusion

[To the top]

If you’d like to review all of the code for this project in one place, you can do so here. Templates in Go offer robust functionalities related to customizing output formats. The html/template package in particular sees extensive use in web development due to its ability to output secure HTML. In this article, you learned how to use the html/template package to create a simple blog application.

Discover more