18 min read

Twists of Templates

In this practical installment, we embrace the Standard Go Project Layout, harness the power of Go Templates, and supercharge our board handler with the enchantment of Tailwind CSS
Adrian is cutting some templates

Introduction

Structuring our projects at the outset is vital for defining their future scalability and maintainability. As a Golang developer, beginning with a single file is convenient; without much ceremony, you can have a functioning piece. However, as our needs and project complexities expand, so must our organization's strategy.

Begin with a single file, and it's a breath of fresh air, simple and straightforward. However, as projects mature and requirements compound, that same structure might feel constricting, even chaotic.

In this chapter, we transition from a minimalist approach to a more structured directory system, exploring the reasons behind the introduction of key folder names like cmd, internal, and pkg, and understanding their importance.

In addition to directory structuring, we'll explore Go templates, covering the basics of Go's standard template system and delving into composition and partials. The goal is to provide a solid foundation for effectively using templates in Go.

Let's begin our hands-on journey, transitioning from a single-file setup to organizing features in their rightful places.

Project Layout

When developing in Go, one might come across the Standard Go Project Layout. It's worth noting, however, that despite its name, it isn't an official standard of the Go community. Even the project maintainers themselves acknowledge its unofficial nature.

This layout has sparked a multitude of discussions among Go developers. Some criticize it, while others adopt it enthusiastically. What cannot be denied is the influence it has had.

The structure it suggests addresses a common challenge: the absence of a widely accepted Go project layout. With various approaches in play, though they may not be dramatically different, diving into a new project can be cumbersome.

Adopting a common layout, such as the one this project suggests, streamlines on-boarding. Developers can quickly identify components, understand the project's flow, and become productive faster. This not only reduces the initial friction but also brings a cohesive feel to Go projects across the board.

I favor this approach not only because I've observed many real-world projects employing variations of this layout but also because, interestingly, even before I discovered this recommended structure, my projects naturally followed a pattern closely resembling it. It's as if this layout resonates with a collective intuition or a shared sense of organizational aesthetics.

While the 'standard' layout provides comprehensive guidance for several directories, we won't delve into each one here. The official repository offers in-depth explanations for all of them. However, for the scope of this article, we'll concentrate on three specific directories: cmd, internal, pkg, and finally, assets for storing templates. Let's get started.

The cmd directory

The cmd directory serves as the home for the main applications of a project. Ideally, each sub-directory within cmd corresponds to the name of the executable you're aiming to produce. For example, /cmd/myapp would contain the entry point for generating a myapp binary.

When working with this directory, it's essential to keep things streamlined. It shouldn't be brimming with extensive code. If a piece of your code has the potential for reuse in other projects, then /pkg is in its rightful place. On the other hand, if the code is project-specific or there's a desire to limit its reuse, /internal that is where it should reside. This deliberate placement can provide clear guidelines and safeguard its intended use.

Often, projects utilize the cmd directory to house a concise main function, acting primarily as an entry point. This function typically imports and invokes code from the /internal and /pkg directories.

In scenarios where, in addition to the main web application, there's a need to deliver CLI tools like migration utilities, code generators, or CLI-based clients for app services, the cmd directory becomes particularly handy. Each tool can be neatly placed in its respective sub-directory, ensuring clarity and structured organization.

However, for this project, our primary application's entry point, the main.go launcher, will reside in the root folder. This approach isn't unusual. Should there be a need for additional executables in the future, they'll find their home in dedicated directories under cmd. This arrangement simplifies the utilization of Go's embedded package, making the integration of resources more straightforward and efficient, as we will see later.

The internal directory

The internal directory serves a special purpose within Go projects. It contains application and library code intended for private use, shielding it from being imported by other programs or libraries outside of its parent directory. What makes this directory unique is that its access restrictions are enforced by the Go compiler itself. If you'd like to dive deeper into this compiler behavior, refer to the Go 1.4 release notes.

For added organization within the internal directory, you can employ nested directories to categorize and structure your private code further. This way, you can segregate different modules or functionalities of your application. For example, your primary application code might reside in /internal/app/myapp, while specific utility functions or private libraries could be placed in directories like /internal/repo or /internal/service, among others.

It's worth noting that the above provides just a brief insight. For a comprehensive understanding of the internal directory and its intricacies, refer to the official repository.

The pkg directory

The pkg directory offers a place to store Go libraries and packages intended for external consumption. The key purpose here is to group Go code separately, especially useful when the root directory houses numerous non-Go components. This structure makes it more straightforward to run various Go tools and keeps the root directory decluttered.

However, as your project grows and your root directory becomes increasingly busy, especially when dealing with external resources, the pkg layout can be beneficial.

Historically, the Go source code was once used pkg for its packages, influencing many community projects to adopt the same convention. Even though it was later phased out from the Go core, the convention persisted in many other projects.

Moving our dummy handlers

When structuring a Go web application, the placement and naming of certain components often boil down to a balance between convention and clarity. Why name a directory 'controllers' instead of 'handlers,' especially since Go typically refers to web route functions with the latter name?

Despite some initial resistance, perhaps out of a desire for originality or avoiding common nomenclatures from other platforms, I've gravitated towards using terminology directly from the MVC (Model, View, Controller) framework to name our packages, particularly the ones containing the handlers.

The term controller for our package, for instance, provides immediate clarity and alignment. This nomenclature resonates well, making the organizational intent evident and readily comprehensible, especially for developers familiar with other platforms where MVC is prevalent.

Given this context, let's refactor our code, moving the handlers from main.go to a separate package located in /internal/controller/.

Refactoring the code

In this code snippet, we are modifying the go.mod file of a Go module. The go.mod file is a crucial part of Go's module system, as it defines the module's identity, dependencies, and other configuration settings.

Initially, the go.mod file looks like this:

module hello

go 1.21.0

require github.com/gorilla/mux v1.8.0

Here, the module "hello" defines its name as "hello." However, we want to change the module name to match the structure github.com/username/repo To achieve this, we modify the go.mod file as follows

module github.com/solutioncrafting/plykan  
  
go 1.21.0  
  
require github.com/gorilla/mux v1.8.0

Please remember to replace solutioncrafting/plykan with your actual GitHub username and the repo name e you've created. This way, your Go module will be correctly linked to your specific GitHub repository.

Now create a new directory: /internal/controller/ and within it, a file named boardcontroller.go.

package controller  
  
import (  
	"fmt"  
	"net/http"  
  
	"github.com/gorilla/mux"  
)  
  
func BoardsIndexHandler(w http.ResponseWriter, r *http.Request) {  
	fmt.Fprint(w, "Listing all boards.")  
}  
  
func NewBoardHandler(w http.ResponseWriter, r *http.Request) {  
	fmt.Fprint(w, "Displaying form for new board.")  
}  
  
func CreateBoardHandler(w http.ResponseWriter, r *http.Request) {  
	fmt.Fprint(w, "Creating a new board.")  
}  
  
func ShowBoardHandler(w http.ResponseWriter, r *http.Request) {  
	vars := mux.Vars(r)  
	id := vars["id"]  
	fmt.Fprintf(w, "Showing board with ID: %s.", id)  
}  
  
func EditBoardHandler(w http.ResponseWriter, r *http.Request) {  
	vars := mux.Vars(r)  
	id := vars["id"]  
	fmt.Fprintf(w, "Editing board with ID: %s.", id)  
}  
  
func UpdateBoardHandler(w http.ResponseWriter, r *http.Request) {  
	vars := mux.Vars(r)  
	id := vars["id"]  
	fmt.Fprintf(w, "Updating board with ID: %s.", id)  
}  
  
func DeleteBoardHandler(w http.ResponseWriter, r *http.Request) {  
	vars := mux.Vars(r)  
	id := vars["id"]  
	fmt.Fprintf(w, "Deleting board with ID: %s.", id)  
}  
  
func DeleteConfirmHandler(w http.ResponseWriter, r *http.Request) {  
	vars := mux.Vars(r)  
	id := vars["id"]  
	fmt.Fprintf(w, "Confirm delete for board with ID: %s.", id)  
}

πŸ“ Did you notice something? After relocating our functions to the new controller package, we capitalized their initial letters. This change isn't arbitrary. In Go, the capitalization of the first letter of a function or variable determines its visibility outside the package. By making them start with uppercase letters, we're ensuring these functions are exportable and accessible from our main application, or any other package that might need them.

πŸ“ While restructuring our application, I also opted to drop the "Handler" suffix from all the functions within the controller package. Given that every function in this package is inherently a handler, it felt redundant to continuously state it. This not only simplifies our naming but also makes the code more concise and readable. Remember, context often alleviates the need for verbosity.

Then, in our main.go, we'll import this new package and adjust the function calls:

package main  
  
import (  
	"net/http"  
  
	"github.com/gorilla/mux"  
  
	"github.com/solutioncrafting/plykan.git/internal/controller"  
)  
  
func main() {  
	r := mux.NewRouter()  
  
	// Board routes  
	r.HandleFunc("/boards", controller.BoardsIndexH).Methods("GET")
	r.HandleFunc("/boards/new", controller.NewBoard).Methods("GET")
	r.HandleFunc("/boards", controller.CreateBoard).Methods("POST")
	r.HandleFunc("/boards/{id}", controller.ShowBoard).Methods("GET") 
	r.HandleFunc("/boards/{id}/edit", controller.EditBoard).Methods("GET") 
	r.HandleFunc("/boards/{id}", controller.UpdateBoard).Methods("PUT") 
	r.HandleFunc("/boards/{id}", controller.DeleteBoard).Methods("DELETE")
	r.HandleFunc("/boards/{id}/confirm-delete", controller.DeleteConfirm).Methods("GET")  
  
	http.ListenAndServe(":8080", r)  
}

By restructuring our code in this manner, we achieve several things:

Separation of Concerns: Our main.go is now more concise, focusing only on route configuration and server startup. The actual logic for each endpoint resides separately, leading to better maintainability. In a future article, we will also explore moving the routing definition to its own package to further enhance the organization of our codebase.

Clarity: By organizing our handlers under the controller directory, we signal to developers that these handlers are essentially the controllers in the MVC pattern. This makes it easier to find and understand the application flow.

Scalability: As our application grows, we can easily add more controllers or even group them under sub-directories without cluttering the main application file.

Remember that you can enjoy a more reader-friendly experience by visiting our repository at https://github.com/solutioncrafting/plykan. While this platform may not provide the best code reading experience, GitHub offers a smoother interface.

Go templates

On embedding templates and assets

Given our progression in building Plykan, it's important to have a clear structure for our resources. The recommended directory structure for our templates, based on the standard Go Standard Layout, designates 'assets' as the appropriate directory for these resources.

β”œβ”€β”€ assets/
β”‚   β”œβ”€β”€ templates/
β”‚   β”‚   β”œβ”€β”€ default.tmpl
β”‚   β”‚   β”œβ”€β”€ pages/
β”‚   β”‚   β”‚   β”œβ”€β”€ board.tmpl

This setup will suffice for our current needs. However, as our application grows and we introduce more controllers, we'll need to refine and possibly expand this structure to stay organized.

Currently, we're referencing these templates directly from the file system, making it simpler during the development phase. However, as a hint of what's to come, Go offers a powerful feature that allows us to embed such resources directly into the compiled binary, offering both flexibility and deployment ease. We'll delve deeper into this in future articles.

Web applications demand dynamic content rendering, easy updates, and content customization. This is where Go's standard template system stands out. While many languages depend on external libraries for templating, Go has a built-in solution. This means a reliable, consistently maintained tool is always available without extra installations or configurations.

But Go's templates offer more than just injecting data into HTML. They come with features like conditional logic, loops, and custom functions, giving developers the flexibility to create intricately rendered web pages without compromising speed or adaptability.

Template composition

Web applications often have recurring patterns or sections across different pages: headers, footers, sidebars, and more. Instead of repeatedly defining these sections, Go's templating system supports composition, allowing developers to define and reuse smaller templates within larger ones.

We are building a Kanban board, presentation and user experience play a main role. Go's templating system allows for a neat composition, ensuring modularity and reusability. Let's see how we can leverage this feature for our board.

Imagine a default structure for our web pages:

{{define "default"}}
<html>
  <head>
    <meta charset="UTF-8">
    <title>{{template "head" .}}</title>
    <!-- meta tags, styles, and scripts here -->
  </head>

  <body>
      <!-- some navbar or header content here -->
      <main>
          {{template "main" .}}
      </main>
      <!-- some footer content here -->
  </body>
</html>
{{end}}

./assets/templates/default.tmpl

For the Kanban board page, we'd like to customize the title and content:

{{define "head"}}
  Plykan - Achieve more, stress less
{{end}} 

{{define "main"}}
	<!-- Board content here -->
	<div>
		<h2>To Do</h2>
		<!-- tasks in the "To Do" column will go here -->
	</div> 
	
	<div>
		<h2>In Progress</h2>
		<!-- tasks in the "In Progress" column will go here -->
	</div>
	
	<div>
		<h2>Done</h2>
		<!-- tasks that are "Completed" will go here -->
	</div>	
{{end}}

./assets/templates/pages/board.tmpl

πŸ“ The above representation is a simplified, hard-coded version of a Kanban board. As we delve deeper into the development process, we'll be introducing more dynamics and functionality to this layout. For now, it serves as a foundational structure, giving us an idea of what the final board might look like.

Go templates provide a powerful way to structure web pages into modular components. While these templates are often stored in files with .tmpl extensions, it's essential to understand that a single file can contain several distinct templates, each enclosed within its block. These blocks define different parts of a web page, such as headers, content sections, or footers.

Let's consider a scenario where we have two .tmpl files: default.tmpl and board.tmpl. In default.tmpl, we define a template default that serves as the primary structure of our web page. Within this default template, we reference smaller templates named head (representing the content of an HTML <head> section) and "main" (serving as the primary content holder).

Surprisingly, in board.tmpl, we find templates named head and main. It's important to note that these templates in board.tmpl reference the blocks within that single template file.

The real magic happens when we connect these templates. In our primary template, defaultwe use the {{template "name" .}} syntax to incorporate smaller nested one. The dot . signifies the current data being processed, which is provided when rendering the template. By using this syntax, we seamlessly assemble the various components of our web page. The head template from default.tmpl effortlessly integrates with the <head> section, while the main template fills the main content area. This approach unifies our templates, creating a cohesive web page composed of individual modular components.

At first glance, this template assembly might seem a bit complex, but as we explore how it works and get some hands-on experience, you'll find it becomes a natural way to structure them for your web applications.

We'll start by defining a board data struct. For simplicity, let's assume our board has three columns: ToDo, In Progress, and Done, each containing a slice of tasks. While this initial structure serves as our starting point, it's important to note that our application is a work in progress, and as we develop it incrementally, we'll adapt and refine the data structure as needed.

type Page struct {
    Head    HeadData
    Board   BoardData
}

type HeadData struct {
    Title       string
    Description string
}

type BoardData struct {
    ID            string
    ToDoTasks     []Task
    InProgressTasks []Task
    DoneTasks     []Task
}

type Task struct {
    ID      string
    Name    string
    Detail  string
}

Next, we'll adjust our ShowBoard function in internal/controller/board.go to provide some dummy data

func ShowBoard(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id := vars["id"] // 1

    // 2 - In the future, we will fetch these from the database
    sampleToDoTasks := []Task{
        {ID: "1", Name: "Task 1", Detail: "Detail for task 1"},
    }
    sampleInProgressTasks := []Task{
        {ID: "2", Name: "Task 2", Detail: "Detail for task 2"},
    }
    sampleDoneTasks := []Task{
        {ID: "3", Name: "Task 3", Detail: "Detail for task 3"},
    }

    // 3
    pageData := &Page{
        Head: HeadData{
            Title:       "Plykan - Your Personal Kanban Board",
            Description: "Achieve more, stress less",
        },
        Board: BoardData{
            ID:            id,
            ToDoTasks:     sampleToDoTasks,
            InProgressTasks: sampleInProgressTasks,
            DoneTasks:     sampleDoneTasks,
        },
    }

    // 4
    ts, err := template.ParseFiles(
		"assets/templates/default.tmpl",
		"assets/templates/pages/board.tmpl")

    // 5
    if err != nil {
        http.Error(w, "Unable to parse template", http.StatusInternalServerError)
        return
    }

    // 6
    err = ts.ExecuteTemplate(w, "default", pageData)
    // 7
    if err != nil {
        http.Error(w, "Failed to execute template", http.StatusInternalServerError)
    }
}

./internal/controller/board.go

  1. The code begins by extracting a variable "id" from the request URL. While in a real-world scenario, this ID would typically represent a specific board's identifier, it's important to note that, at this stage, we are working with not real dynamic data. As such, the idhardcoded value extracted from the URL doesn't significantly affect the data displayed on the Kanban board; it serves primarily for illustrative purposes
  2. Next, it initializes dummy data (sampleToDoTasks, sampleInProgressTasks, sampleDoneTasks). These dummy tasks represent the columns (To Do, In Progress, Done) displayed on the Kanban board. In a real application, this data would typically come from a database or some other data source. However, for the purpose of this example, it's hard-coded for now.
  3. The pageData structure is created to organize all the data needed for rendering the web page. It includes HeadData metadata and BoardData for the Kanban board-related information.
  4. Using template.ParseFiles, the Go template engine reads and parses HTML templates, like default.tmpl and board.tmpl, from specified file paths. This process not only captures the template content but also understands template syntax, such as {{template "name" .}}. Importantly, it ensures that all template references within these files can be satisfied, allowing templates to reference and connect with one another during rendering, forming a cohesive web page structure.
  5. If there's an error during template parsing, we return a 500 - Internal Server Error response.
  6. Finally, the ts.ExecuteTemplate function acts like a bridge between our web page's blueprint and the actual content we want to display. It takes the "default" template, which defines the structure of our page, and seamlessly integrates the data from the pageData structure into the designated areas within the template. This process ensures that our web page is assembled correctly, and presented to the user
  7. If there's an error during template execution, it also returns a 500 - Internal Server Error response.

Updating the main.tmpl

It's not uncommon to start with hardcoded content to understand the fundamental structure and rendering process. However, as the application grows, you'll quickly realize the need for dynamic content rendering. This is especially true for a Kanban board, where tasks may frequently change, and the content isn't static.

Consider the previous hardcoded at assets/templates/pages/board.tmpl, this template rigidly displays three columns: "ToDo," "In Progress," and "Done." It serves well as a foundational step, but as we proceed, we want to render lists of tasks within each of these columns based on actual data.

{{define "main"}}
	<div>
		<h2>To Do</h2>
		<!-- tasks in the "To Do" column will go here -->
	</div> 
	
	<div>
		<h2>In Progress</h2>
		<!-- tasks in the "In Progress" column will go here -->
	</div>
	
	<div>
		<h2>Done</h2>
		<!-- tasks that are "Completed" will go here -->
	</div>	
{{end}}

./assets/templates/pages/board.tmpl

Here's how our template will evolve, the range action in Go templates do the trick. In our updated implementation, we utilized range to iterate over and display tasks in each column. In Go templates, the range action allows you to loop over slices or maps, making it easy to render dynamic lists of items.

{{define "main"}}
    <div>
        <h2>To Do</h2>
        {{range .ToDo}}
            <div class="task">{{.}}</div>
        {{end}}
    </div>
    
    <div>
        <h2>In Progress</h2>
        {{range .InProgress}}
            <div class="task">{{.}}</div>
        {{end}}
    </div>
    
    <div>
        <h2>Done</h2>
        {{range .Done}}
            <div class="task">{{.}}</div>
        {{end}}
    </div>
{{end}}

./assets/templates/pages/board.tmpl

With the range action, tasks within each column are no longer static. They're rendered dynamically based on the data we pass to the template. By iterating over each task in .ToDo, .InProgress, and .Done, we can display an up-to-date list in each column without the need for manual updates.

The magic here lies in the dot (.) notation, which serves as a reference to the data we provide when rendering the template. In this case, it's the pageData struct created in the ShowBoard function. So when we use .ToDo, .InProgress, and .Done in the template, they act as 'keys' to extract the respective data frompageData, making sure that the tasks are seamlessly integrated into the Kanban board layout.

This enhanced template allows our Kanban board to showcase currently data, with the potential for future dynamic updates from the database. This sets the stage for further interactivity and dynamic content updates.

Partials

When working with partial templates, the idea is to create a reusable template that can be configured or customized for different use cases. In our case, we want to create a single partial template for Kanban columns that can adapt to different columns (e.g., "To Do", "In Progress," "Done") while maintaining a consistent structure.

Let's add assets/templates/partials/column.tmpl

$ mkdir -p assets/templates/partials
$ touch assets/templates/partials/column.tmpl

To get this new dir structure

Creating a partial template

Begin by creating a single partial template for Kanban columns. This template will serve as a blueprint for all columns and should include the common structure, such as headers and placeholders for tasks:

{{define "kanbanColumn"}}
<div>
  <h2>{{.ColumnName}}</h2>
  {{range .Tasks}}
    <div>{{.}}</div>
  {{end}}
</div>
{{end}}

./assets/templates/partials/column.tmpl

We also need to update the board.tmpl if we want to make it use this new partial

{{define "main"}}
<div>
    {{range $index, $column := .Columns}}
    {{template "kanbanColumn" $column}}
    {{end}}
</div>
{{end}}

./assets/templates/pages/board.tmpl

In the updated code, we've introduced a range loop that iterates over the three columns, allowing us to dynamically render each one. Within the loop, we use {{template "kanbanColumn" $column}} to pass each individual column, represented by the variable $column, to the kanbanColumn template. This enables us to reuse the kanbanColumn template for all columns, making our code more modular and efficient.

Let's run the app to see how it looks

$ go run main.go

Open the browser at http://localhost:8080/boards/111`

πŸ“The `111` at the end of the URL could be any other value since we are not using that identifier to get the board from the database.

You should see something like this

The board doesn't look very appealing, right? It doesn't even look like a board at all as there are no styles applied. Let's add some CSS magic to it.

Entering TailwindCSS

Now that we have our Kanban board structured, it's time to make it visually appealing. We'll use Tailwind CSS, which is known for its simplicity and flexibility. Tailwind provides a wide range of pre-built classes to help us style our web page components without having to write extensive custom CSS.

We also need to update the default.tmplin order to use it.

{{define "default"}}
<html>
  <head>
    {{template "head" .Head}}
    <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
  </head>
  <!-- The rest of the template remains unchanged -->
</html>
{{end}}

./assets/templates/default.tmpl

We're adding the `<link ...>` tag on line 5 to incorporate Tailwind CSS, allowing us to enhance the styling of our board. For simplicity, at least for now, we're fetching it from a content delivery network (CDN) rather than installing it locally.


Now we can sprinkle a bit of CSS on our templates to make our board look like a real board.

{{define "main"}}
<div class="flex m-4 space-x-4">
    {{range $index, $column := .Columns}}
        {{template "kanbanColumn" $column}}
    {{end}}
</div>
{{end}}

./assets/templates/default.tmpl

{{define "kanbanColumn"}}
<div class="kanban-column {{.Color}} p-4 rounded-lg">
    <h2 class="text-{{.Color}}">{{.ColumnName}}</h2>
    {{range .Tasks}}
        <div class="task">{{.}}</div>
    {{end}}
</div>
{{end}}

././assets/templates/partials/column.tmpl

Configuring the Partial Dynamically


To make this partial template adaptable for different columns, we can dynamically configure it when rendering. In your Go code, define a ColumnData struct that contains the necessary information for each column:

type ColumnData struct {
    ColumnName string   // "To Do," "In Progress," "Done"
    Tasks      []string // List of tasks for the column
    Color      string   // Background color class
}

Now, when rendering the template for each column, create an instance of ColumnData with the appropriate values and execute the kanbanColumn partial template.
Therefore, we need to update our pageData in the ShowBoard function of the board controller.

	// In the future, we will fetch these from the database
	sampleToDoTasks := []string{"Task 1", "Task 2"}
	sampleInProgressTasks := []string{"Task 3", "Task 4"}
	sampleDoneTasks := []string{"Task 5", "Task 6"}

	pageData := &Page{
		Head: HeadData{
			Title:       "Plykan - Your Personal Kanban Board",
			Description: "Achieve more, stress less",
		},
		Board: BoardData{
			ID: id,
			// Populate data for each Kanban column
			Columns: []ColumnData{
				{ColumnName: "To Do", Tasks: sampleToDoTasks, Color: "blue-200"},
				{ColumnName: "In Progress", Tasks: sampleInProgressTasks, Color: "green-200"},
				{ColumnName: "Done", Tasks: sampleDoneTasks, Color: "yellow-200"},
			},
		},
	}

	ts, err := template.ParseFiles(
		"assets/templates/default.tmpl",
		"assets/templates/pages/board.tmpl",
		"assets/templates/partials/column.tmpl")

./internal/controller/board.go

We keep our code DRY and reduce redundancy. Any changes or improvements to the column structure can be made in one place, ensuring consistency across columns. It also simplifies the process of adding new columns to our Kanban board in the future.

Let's run the app again and take a look at our new board with sleek styling.

It's starting to look pretty neat, isn't it?

Plykan kanban board with thre nice rounded columns in light blue, light green and yellow

We've made significant progress today, including our initial refactor and the introduction of templates. Now, let's have a sneak peek at what's on the horizon for our upcoming chapter.

Up Next: Logging and Dependency Injection


We'll now take a brief detour from perfecting the visual aspects of our pages to delve into fundamental concepts. This includes exploring leveled logging and introducing dependency injection, unlocking various capabilities. For instance, we'll seamlessly integrate the logger into our handlers and, in the future, apply this approach to streamline the usage of configuration and other dependencies during initialization.

Cheers!

References