HTML Templates and Web Applications with Go Gin

Learning Objectives: By the end of this lesson, you will be able to:

  • Render HTML templates using Go’s html/template package with Gin
  • Implement template inheritance and layout systems
  • Pass data from handlers to templates effectively
  • Create custom template functions and helpers
  • Handle HTML forms with proper validation and CSRF protection
  • Manage user sessions and implement flash messaging
  • Build a complete web application with authentication and dynamic content
  • Optimize static asset serving and caching strategies

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Have you ever wondered how to build traditional web applications with server-side rendering using Go, rather than just APIs? Or perhaps you’ve worked with template engines in other languages and want to understand how Go’s approach differs? Today, we’re exploring how Gin can power full-stack web applications with dynamic HTML generation, user sessions, and interactive forms.

Lesson Overview

  • Duration: 110 minutes
  • Prerequisites: Lessons 1-4 completed, basic HTML/CSS knowledge, understanding of web forms
  • Materials Needed: Go 1.25+ for Gin v1.12, Gin framework, HTML/CSS files, web browser for testing

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Template Rendering - Setting up and using Go’s html/template with Gin
  2. Template Organization - Layouts, partials, and inheritance patterns
  3. Data Passing - Sending complex data from handlers to templates
  4. Template Functions - Creating custom helpers and utilities
  5. Form Handling - Processing HTML forms with validation and security
  6. Session Management - User sessions, authentication, and flash messages
  7. Complete Application - Building a blog or CMS with all features integrated

Success Criteria:

By the end of this session, you should be able to build complete server-side rendered web applications with user authentication, dynamic content, form handling, and professional UI/UX patterns.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

Server-Side Rendering vs. Client-Side Rendering

Server-side rendering (SSR) generates HTML on the server and sends complete pages to the browser. This approach offers better SEO, faster initial page loads, and works without JavaScript. It’s ideal for content-heavy sites, blogs, e-commerce, and applications requiring strong SEO.

Real-World Analogy: Think of SSR like a restaurant where meals are prepared in the kitchen and served ready-to-eat, versus a salad bar (CSR) where customers assemble their own meals. Both have their place, but SSR provides a complete, immediate experience.

Why This Matters: Many applications benefit from SSR’s simplicity, SEO advantages, and reduced client-side complexity. Understanding when to use SSR versus APIs helps you choose the right architecture for your projects.

Go’s html/template Package

Go’s built-in template system provides:

  • XSS Protection: Automatic escaping of user content
  • Type Safety: Compile-time template validation
  • Rich Syntax: Loops, conditionals, and function calls
  • Template Inheritance: Layout and partial template support

Template Organization Patterns

  1. Layout Templates: Base HTML structures shared across pages
  2. Content Templates: Page-specific content that fills layout slots
  3. Partial Templates: Reusable components (headers, footers, forms)
  4. Template Functions: Custom helpers for formatting and logic

Session Management Concepts

  • Session Storage: Where session data is stored (memory, database, Redis)
  • Session Cookies: How browsers identify sessions
  • CSRF Protection: Preventing cross-site request forgery attacks
  • Flash Messages: Temporary messages shown after redirects

Key Terminology

  • Template: A file containing HTML with placeholders for dynamic content
  • Context: Data passed from handlers to templates
  • Layout: Base template that other templates extend
  • Partial: Reusable template component
  • Flash Message: Temporary message stored in session
  • CSRF Token: Security token preventing unauthorized form submissions

4. Detailed Explanation and Examples (25-30 minutes)

Understanding Template Architecture

Gin integrates seamlessly with Go’s html/template package, providing a powerful system for generating dynamic HTML. Templates are parsed once at startup and cached for performance, while data is passed through the template context during rendering.

Template Execution Flow

  1. Template Loading: Templates are parsed and cached at application startup
  2. Route Handling: HTTP request triggers a handler function
  3. Data Preparation: Handler gathers data needed for the template
  4. Template Rendering: Gin combines template with data to generate HTML
  5. Response Sending: Complete HTML is sent to the browser

Template Security Features

Go’s template system automatically escapes content to prevent XSS attacks:

  • HTML content is escaped (< becomes <)
  • JavaScript contexts are protected
  • CSS contexts have appropriate escaping
  • URL contexts prevent injection

Session Management Architecture

Sessions enable stateful interactions in stateless HTTP:

  • Session Creation: Generate unique session ID on login
  • Session Storage: Store session data server-side
  • Session Validation: Verify session ID with each request
  • Session Cleanup: Remove expired sessions periodically

Common Misconceptions

  1. Misconception: “Templates are slower than APIs” Reality: Well-optimized templates can be very fast, especially with caching and CDNs.

  2. Misconception: “You can’t build modern UIs with server-side rendering” Reality: Modern SSR can include progressive enhancement, HTMX, and other techniques for rich interactions.

  3. Misconception: “Session management is automatically secure” Reality: Proper session security requires careful configuration of cookies, CSRF protection, and secure storage.

5. Live Coding Demonstration (20-25 minutes)

Example 1: Basic Template Setup and Rendering

// main.go
package main

import (
    "net/http"
    "html/template"
    "time"
    
    "github.com/gin-gonic/gin"
)

// User represents user data for templates
type User struct {
    ID       int       `json:"id"`
    Username string    `json:"username"`
    Email    string    `json:"email"`
    FullName string    `json:"full_name"`
    JoinedAt time.Time `json:"joined_at"`
}

// PageData represents common data passed to all templates
type PageData struct {
    Title       string
    User        *User
    CurrentYear int
    NavItems    []NavItem
    FlashMsg    string
}

type NavItem struct {
    Name string
    URL  string
    Active bool
}

// Custom template functions
func setupTemplateFunctions() template.FuncMap {
    return template.FuncMap{
        "formatDate": func(t time.Time) string {
            return t.Format("January 2, 2006")
        },
        "formatDateTime": func(t time.Time) string {
            return t.Format("January 2, 2006 at 3:04 PM")
        },
        "add": func(a, b int) int {
            return a + b
        },
        "multiply": func(a, b int) int {
            return a * b
        },
        "truncate": func(s string, length int) string {
            if len(s) <= length {
                return s
            }
            return s[:length] + "..."
        },
        "isEven": func(n int) bool {
            return n%2 == 0
        },
        "dict": func(values ...interface{}) map[string]interface{} {
            if len(values)%2 != 0 {
                return nil
            }
            dict := make(map[string]interface{})
            for i := 0; i < len(values); i += 2 {
                key, ok := values[i].(string)
                if !ok {
                    return nil
                }
                dict[key] = values[i+1]
            }
            return dict
        },
    }
}

func main() {
    r := gin.Default()
    
    // Load templates with custom functions
    r.SetFuncMap(setupTemplateFunctions())
    r.LoadHTMLGlob("templates/**/*")
    
    // Serve static files
    r.Static("/static", "./static")
    r.StaticFile("/favicon.ico", "./static/favicon.ico")
    
    // Home page
    r.GET("/", func(c *gin.Context) {
        data := PageData{
            Title:       "Welcome to Our Site",
            CurrentYear: time.Now().Year(),
            NavItems: []NavItem{
                {Name: "Home", URL: "/", Active: true},
                {Name: "About", URL: "/about", Active: false},
                {Name: "Contact", URL: "/contact", Active: false},
            },
        }
        
        c.HTML(http.StatusOK, "base.html", data)
    })
    
    // User profile page
    r.GET("/profile/:username", func(c *gin.Context) {
        username := c.Param("username")
        
        // Simulate fetching user data
        user := &User{
            ID:       1,
            Username: username,
            Email:    username + "@example.com",
            FullName: "John Doe",
            JoinedAt: time.Now().AddDate(-1, -2, -15),
        }
        
        data := PageData{
            Title: "Profile - " + user.FullName,
            User:  user,
            CurrentYear: time.Now().Year(),
            NavItems: []NavItem{
                {Name: "Home", URL: "/", Active: false},
                {Name: "Profile", URL: "/profile/" + username, Active: true},
            },
        }
        
        c.HTML(http.StatusOK, "base.html", data)
    })
    
    r.Run(":8080")
}

Template Files Structure:

templates/
├── layouts/
│   └── base.html
└── partials/
    ├── header.html
    ├── footer.html
    └── navigation.html

templates/layouts/base.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{.Title}} - My Website</title>
    <link href="/static/css/style.css" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    {{template "header" .}}
    
    <main class="container mt-4">
        {{if .FlashMsg}}
        <div class="alert alert-info alert-dismissible fade show" role="alert">
            {{.FlashMsg}}
            <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
        </div>
        {{end}}
        
    {{if .User}}
        <h1>{{.User.FullName}}</h1>
        <p>Joined {{.User.JoinedAt.Format "January 2, 2006"}}</p>
    {{else}}
        <h1>{{.Title}}</h1>
        <p>Welcome to our site.</p>
    {{end}}
    </main>
    
    {{template "footer" .}}
    
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
    <script src="/static/js/main.js"></script>
</body>
</html>

templates/partials/header.html:

{{define "header"}}
<header class="navbar navbar-expand-lg navbar-dark bg-dark">
    <div class="container">
        <a class="navbar-brand" href="/">My Website</a>
        
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
            <span class="navbar-toggler-icon"></span>
        </button>
        
        <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav me-auto">
                {{range .NavItems}}
                <li class="nav-item">
                    <a class="nav-link {{if .Active}}active{{end}}" href="{{.URL}}">{{.Name}}</a>
                </li>
                {{end}}
            </ul>
            
            <ul class="navbar-nav">
                {{if .User}}
                <li class="nav-item dropdown">
                    <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
                        {{.User.Username}}
                    </a>
                    <ul class="dropdown-menu">
                        <li><a class="dropdown-item" href="/profile/{{.User.Username}}">Profile</a></li>
                        <li><a class="dropdown-item" href="/settings">Settings</a></li>
                        <li><hr class="dropdown-divider"></li>
                        <li><a class="dropdown-item" href="/logout">Logout</a></li>
                    </ul>
                </li>
                {{else}}
                <li class="nav-item">
                    <a class="nav-link" href="/login">Login</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/register">Register</a>
                </li>
                {{end}}
            </ul>
        </div>
    </div>
</header>
{{end}}

templates/partials/footer.html:

{{define "footer"}}<footer class="container">My Website</footer>{{end}}

This first example uses one template name (base.html) with conditional content. With multiple pages, do not parse competing {{define "content"}} overrides into one global template set: later definitions replace earlier ones. Use unique page definitions (or separate cloned template sets) instead. templates/**/* matches exactly one directory beneath templates, not arbitrary nesting.

Example 2: Form Handling with Validation and CSRF Protection

// handlers/auth.go
package handlers

import (
    "crypto/rand"
    "crypto/subtle"
    "encoding/hex"
    "net/http"
    "time"
    
    "github.com/gin-contrib/sessions"
    "github.com/gin-gonic/gin"
    "github.com/go-playground/validator/v10"
)

// LoginForm represents login form data
type LoginForm struct {
    Username string `form:"username" binding:"required,min=3,max=30"`
    Password string `form:"password" binding:"required,min=6"`
    Remember bool   `form:"remember"`
    CSRFToken string `form:"csrf_token" binding:"required"`
}

// RegisterForm represents registration form data
type RegisterForm struct {
    Username        string `form:"username" binding:"required,min=3,max=30,alphanum"`
    Email          string  `form:"email" binding:"required,email"`
    Password       string  `form:"password" binding:"required,min=8"`
    ConfirmPassword string `form:"confirm_password" binding:"required"`
    FullName       string  `form:"full_name" binding:"required,min=2,max=100"`
    Terms          bool    `form:"terms" binding:"required"`
    CSRFToken      string  `form:"csrf_token" binding:"required"`
}

// ValidationError represents form validation errors
type ValidationError struct {
    Field   string `json:"field"`
    Message string `json:"message"`
}

// generateCSRFToken creates a secure random token
func generateCSRFToken() (string, error) {
    bytes := make([]byte, 32)
    if _, err := rand.Read(bytes); err != nil { return "", err }
    return hex.EncodeToString(bytes), nil
}

// validateCSRFToken checks if the provided token matches the session token
func validateCSRFToken(c *gin.Context, providedToken string) bool {
    session := sessions.Default(c)
    sessionToken, ok := session.Get("csrf_token").(string)
    if !ok || len(providedToken) != len(sessionToken) {
        return false
    }
    return subtle.ConstantTimeCompare([]byte(sessionToken), []byte(providedToken)) == 1
}

// ShowLoginForm displays the login form
func ShowLoginForm(c *gin.Context) {
    session := sessions.Default(c)
    
    // Generate CSRF token
    csrfToken, err := generateCSRFToken()
    if err != nil { c.Status(http.StatusInternalServerError); return }
    session.Set("csrf_token", csrfToken)
    if err := session.Save(); err != nil { c.Status(http.StatusInternalServerError); return }
    
    // Get flash messages
    flashMsg := session.Get("flash_message")
    if flashMsg != nil {
        session.Delete("flash_message")
        if err := session.Save(); err != nil { c.Status(http.StatusInternalServerError); return }
    }
    
    data := gin.H{
        "Title":     "Login",
        "CSRFToken": csrfToken,
        "FlashMsg":  flashMsg,
        "NavItems": []gin.H{
            {"Name": "Home", "URL": "/", "Active": false},
            {"Name": "Login", "URL": "/login", "Active": true},
        },
    }
    
    c.HTML(http.StatusOK, "pages/login.html", data)
}

// ProcessLogin handles login form submission
func ProcessLogin(c *gin.Context) {
    var form LoginForm
    session := sessions.Default(c)
    
    // Bind form data
    if err := c.ShouldBind(&form); err != nil {
        // Handle validation errors
        var errors []ValidationError
        
        // Convert validation errors to user-friendly messages
        if validationErrors, ok := err.(validator.ValidationErrors); ok {
            for _, validationError := range validationErrors {
                field := validationError.Field()
                var message string
                
                switch validationError.Tag() {
                case "required":
                    message = field + " is required"
                case "min":
                    message = field + " must be at least " + validationError.Param() + " characters"
                case "max":
                    message = field + " must be at most " + validationError.Param() + " characters"
                default:
                    message = field + " is invalid"
                }
                
                errors = append(errors, ValidationError{
                    Field:   field,
                    Message: message,
                })
            }
        }
        
        // Re-display form with errors
        data := gin.H{
            "Title":        "Login",
            "CSRFToken":    session.Get("csrf_token"),
            "Errors":       errors,
            "FormData":     gin.H{"Username": form.Username, "Remember": form.Remember}, // Never re-render passwords.
            "NavItems": []gin.H{
                {"Name": "Home", "URL": "/", "Active": false},
                {"Name": "Login", "URL": "/login", "Active": true},
            },
        }
        
        c.HTML(http.StatusBadRequest, "pages/login.html", data)
        return
    }
    
    // Validate CSRF token
    if !validateCSRFToken(c, form.CSRFToken) {
        session.Set("flash_message", "Invalid security token. Please try again.")
        if err := session.Save(); err != nil { c.Status(http.StatusInternalServerError); return }
        c.Redirect(http.StatusSeeOther, "/login")
        return
    }
    
    // Simulate user authentication (in real apps, check against database)
    if form.Username == "admin" && form.Password == "password123" {
        // Successful login
        session.Set("user_id", 1)
        session.Set("username", form.Username)
        
        if form.Remember {
            // Set longer session timeout for "remember me"
            session.Options(sessions.Options{
                MaxAge: 30 * 24 * 3600, // 30 days
                HttpOnly: true,
                Secure: true, // Enable in production with HTTPS
            })
        }
        
        session.Set("flash_message", "Welcome back, " + form.Username + "!")
        if err := session.Save(); err != nil { c.Status(http.StatusInternalServerError); return }
        
        c.Redirect(http.StatusSeeOther, "/dashboard")
    } else {
        // Failed login
        session.Set("flash_message", "Invalid username or password")
        if err := session.Save(); err != nil { c.Status(http.StatusInternalServerError); return }
        c.Redirect(http.StatusSeeOther, "/login")
    }
}

// ShowRegisterForm displays the registration form
func ShowRegisterForm(c *gin.Context) {
    session := sessions.Default(c)
    
    csrfToken, err := generateCSRFToken()
    if err != nil { c.Status(http.StatusInternalServerError); return }
    session.Set("csrf_token", csrfToken)
    if err := session.Save(); err != nil { c.Status(http.StatusInternalServerError); return }
    
    data := gin.H{
        "Title":     "Register",
        "CSRFToken": csrfToken,
        "NavItems": []gin.H{
            {"Name": "Home", "URL": "/", "Active": false},
            {"Name": "Register", "URL": "/register", "Active": true},
        },
    }
    
    c.HTML(http.StatusOK, "pages/register.html", data)
}

// ProcessRegister handles registration form submission
func ProcessRegister(c *gin.Context) {
    var form RegisterForm
    session := sessions.Default(c)
    
    if err := c.ShouldBind(&form); err != nil {
        // Handle validation errors (similar to login)
        var errors []ValidationError
        
        // Add custom validation for password confirmation
        if form.Password != form.ConfirmPassword {
            errors = append(errors, ValidationError{
                Field:   "ConfirmPassword",
                Message: "Passwords do not match",
            })
        }
        
        data := gin.H{
            "Title":     "Register",
            "CSRFToken": session.Get("csrf_token"),
            "Errors":    errors,
            "FormData":  gin.H{"Username": form.Username, "Email": form.Email, "FullName": form.FullName}, // Never re-render passwords.
        }
        
        c.HTML(http.StatusBadRequest, "pages/register.html", data)
        return
    }
    
    // Validate CSRF token
    if !validateCSRFToken(c, form.CSRFToken) {
        session.Set("flash_message", "Invalid security token. Please try again.")
        if err := session.Save(); err != nil { c.Status(http.StatusInternalServerError); return }
        c.Redirect(http.StatusSeeOther, "/register")
        return
    }
    
    // Check if user already exists (simulate database check)
    if form.Username == "admin" || form.Email == "admin@example.com" {
        session.Set("flash_message", "Username or email already exists")
        if err := session.Save(); err != nil { c.Status(http.StatusInternalServerError); return }
        c.Redirect(http.StatusSeeOther, "/register")
        return
    }
    
    // Create user (simulate database insertion)
    session.Set("flash_message", "Registration successful! Please log in.")
    if err := session.Save(); err != nil { c.Status(http.StatusInternalServerError); return }
    
    c.Redirect(http.StatusSeeOther, "/login")
}

// Logout handles user logout
func Logout(c *gin.Context) {
    session := sessions.Default(c)
    session.Clear()
    if err := session.Save(); err != nil { c.Status(http.StatusInternalServerError); return }
    
    c.Redirect(http.StatusSeeOther, "/")
}

templates/pages/login.html:

{{define "content"}}
<div class="row justify-content-center">
    <div class="col-md-6">
        <div class="card">
            <div class="card-header">
                <h3 class="card-title mb-0">Login</h3>
            </div>
            <div class="card-body">
                {{if .Errors}}
                <div class="alert alert-danger">
                    <ul class="mb-0">
                        {{range .Errors}}
                        <li>{{.Message}}</li>
                        {{end}}
                    </ul>
                </div>
                {{end}}
                
                <form method="POST" action="/login">
                    <input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
                    
                    <div class="mb-3">
                        <label for="username" class="form-label">Username</label>
                        <input type="text" class="form-control" id="username" name="username" 
                               value="{{if .FormData}}{{.FormData.Username}}{{end}}" required>
                    </div>
                    
                    <div class="mb-3">
                        <label for="password" class="form-label">Password</label>
                        <input type="password" class="form-control" id="password" name="password" required>
                    </div>
                    
                    <div class="mb-3 form-check">
                        <input type="checkbox" class="form-check-input" id="remember" name="remember"
                               {{if .FormData}}{{if .FormData.Remember}}checked{{end}}{{end}}>
                        <label class="form-check-label" for="remember">
                            Remember me
                        </label>
                    </div>
                    
                    <button type="submit" class="btn btn-primary w-100">Login</button>
                </form>
                
                <div class="text-center mt-3">
                    <p><a href="/forgot-password">Forgot your password?</a></p>
                    <p>Don't have an account? <a href="/register">Register here</a></p>
                </div>
            </div>
        </div>
    </div>
</div>
{{end}}

{{template "layouts/base.html" .}}

Example 3: Complete Blog Application with Dynamic Content

// models/blog.go
package models

import (
    "time"
)

// Post represents a blog post
type Post struct {
    ID          int       `json:"id"`
    Title       string    `json:"title"`
    Slug        string    `json:"slug"`
    Content     string    `json:"content"`
    Excerpt     string    `json:"excerpt"`
    AuthorID    int       `json:"author_id"`
    Author      User      `json:"author"`
    Category    string    `json:"category"`
    Tags        []string  `json:"tags"`
    Published   bool      `json:"published"`
    CreatedAt   time.Time `json:"created_at"`
    UpdatedAt   time.Time `json:"updated_at"`
    ViewCount   int       `json:"view_count"`
}

// Comment represents a blog comment
type Comment struct {
    ID        int       `json:"id"`
    PostID    int       `json:"post_id"`
    AuthorID  int       `json:"author_id"`
    Author    User      `json:"author"`
    Content   string    `json:"content"`
    CreatedAt time.Time `json:"created_at"`
}

// handlers/blog.go
package handlers

import (
    "net/http"
    "strconv"
    "strings"
    "time"
    
    "github.com/gin-contrib/sessions"
    "github.com/gin-gonic/gin"
    "github.com/your-app/models"
)

// ShowBlogHome displays the blog homepage with recent posts
func ShowBlogHome(c *gin.Context) {
    // Simulate fetching recent posts
    posts := []models.Post{
        {
            ID:      1,
            Title:   "Getting Started with Go Gin",
            Slug:    "getting-started-with-go-gin",
            Excerpt: "Learn how to build web applications with Go and Gin framework...",
            Author:  models.User{Username: "john_doe", FullName: "John Doe"},
            Category: "Programming",
            Tags:    []string{"go", "gin", "web"},
            Published: true,
            CreatedAt: time.Now().AddDate(0, 0, -2),
            ViewCount: 150,
        },
        {
            ID:      2,
            Title:   "Advanced Template Patterns",
            Slug:    "advanced-template-patterns",
            Excerpt: "Explore advanced techniques for organizing and optimizing templates...",
            Author:  models.User{Username: "jane_smith", FullName: "Jane Smith"},
            Category: "Web Development",
            Tags:    []string{"templates", "html", "design"},
            Published: true,
            CreatedAt: time.Now().AddDate(0, 0, -5),
            ViewCount: 89,
        },
    }
    
    session := sessions.Default(c)
    var currentUser *models.User
    if userID := session.Get("user_id"); userID != nil {
        currentUser = &models.User{
            ID:       userID.(int),
            Username: session.Get("username").(string),
        }
    }
    
    data := gin.H{
        "Title":    "Blog Home",
        "Posts":    posts,
        "User":     currentUser,
        "NavItems": getBlogNavItems("/"),
    }
    
    c.HTML(http.StatusOK, "blog/home.html", data)
}

// ShowPost displays a single blog post with comments
func ShowPost(c *gin.Context) {
    slug := c.Param("slug")
    
    // Simulate fetching post by slug
    post := models.Post{
        ID:      1,
        Title:   "Getting Started with Go Gin",
        Slug:    slug,
        Content: `# Getting Started with Go Gin

Go Gin is a fantastic web framework that makes building web applications and APIs incredibly straightforward. In this comprehensive guide, we'll explore everything you need to know to get started.

## What is Gin?

Gin is a Go HTTP framework with routing, middleware, and HTML rendering. Performance depends on the workload; benchmark your own application rather than assuming a fixed speedup.

## Basic Setup

Here's how to create your first Gin application:

` + "```go" + `
package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run()
}
` + "```" + `

And that's it! You now have a working web server.`,
        Author:    models.User{Username: "john_doe", FullName: "John Doe"},
        Category:  "Programming",
        Tags:      []string{"go", "gin", "web"},
        Published: true,
        CreatedAt: time.Now().AddDate(0, 0, -2),
        ViewCount: 151, // Increment view count
    }
    
    // Simulate fetching comments
    comments := []models.Comment{
        {
            ID:        1,
            PostID:    1,
            Author:    models.User{Username: "alice", FullName: "Alice Brown"},
            Content:   "Great tutorial! This really helped me understand Gin better.",
            CreatedAt: time.Now().AddDate(0, 0, -1),
        },
        {
            ID:        2,
            PostID:    1,
            Author:    models.User{Username: "bob", FullName: "Bob Wilson"},
            Content:   "Thanks for sharing. Looking forward to more Go content!",
            CreatedAt: time.Now().AddHours(-6),
        },
    }
    
    session := sessions.Default(c)
    var currentUser *models.User
    if userID := session.Get("user_id"); userID != nil {
        currentUser = &models.User{
            ID:       userID.(int),
            Username: session.Get("username").(string),
        }
    }
    
    data := gin.H{
        "Title":    post.Title,
        "Post":     post,
        "Comments": comments,
        "User":     currentUser,
        "NavItems": getBlogNavItems("/blog/" + slug),
    }
    
    c.HTML(http.StatusOK, "blog/post.html", data)
}

// getBlogNavItems returns navigation items for blog pages
func getBlogNavItems(currentPath string) []gin.H {
    items := []gin.H{
        {"Name": "Home", "URL": "/", "Active": currentPath == "/"},
        {"Name": "Blog", "URL": "/blog", "Active": strings.HasPrefix(currentPath, "/blog")},
        {"Name": "About", "URL": "/about", "Active": currentPath == "/about"},
        {"Name": "Contact", "URL": "/contact", "Active": currentPath == "/contact"},
    }
    return items
}

templates/blog/home.html:

{{define "content"}}
<div class="row">
    <div class="col-lg-8">
        <h1 class="mb-4">Latest Blog Posts</h1>
        
        {{range .Posts}}
        <article class="card mb-4">
            <div class="card-body">
                <h2 class="card-title">
                    <a href="/blog/{{.Slug}}" class="text-decoration-none">{{.Title}}</a>
                </h2>
                
                <div class="text-muted mb-2">
                    <small>
                        By <strong>{{.Author.FullName}}</strong>
                        on {{formatDate .CreatedAt}}
                        in <span class="badge bg-secondary">{{.Category}}</span>
                    </small>
                </div>
                
                <p class="card-text">{{.Excerpt}}</p>
                
                <div class="d-flex justify-content-between align-items-center">
                    <div>
                        {{range .Tags}}
                        <span class="badge bg-light text-dark me-1">#{{.}}</span>
                        {{end}}
                    </div>
                    <small class="text-muted">{{.ViewCount}} views</small>
                </div>
                
                <a href="/blog/{{.Slug}}" class="btn btn-primary mt-2">Read More</a>
            </div>
        </article>
        {{else}}
        <div class="alert alert-info">
            <h4>No Posts Yet</h4>
            <p>Check back later for new content!</p>
        </div>
        {{end}}
    </div>
    
    <div class="col-lg-4">
        <div class="card">
            <div class="card-header">
                <h5>Categories</h5>
            </div>
            <div class="card-body">
                <a href="/blog/category/programming" class="d-block">Programming (5)</a>
                <a href="/blog/category/web-development" class="d-block">Web Development (3)</a>
                <a href="/blog/category/tutorials" class="d-block">Tutorials (8)</a>
            </div>
        </div>
        
        <div class="card mt-3">
            <div class="card-header">
                <h5>Popular Tags</h5>
            </div>
            <div class="card-body">
                <span class="badge bg-primary me-1 mb-1">#go</span>
                <span class="badge bg-primary me-1 mb-1">#gin</span>
                <span class="badge bg-primary me-1 mb-1">#web</span>
                <span class="badge bg-primary me-1 mb-1">#templates</span>
                <span class="badge bg-primary me-1 mb-1">#html</span>
            </div>
        </div>
    </div>
</div>
{{end}}

{{template "layouts/base.html" .}}

6. Hands-On Practice (30-40 minutes)

Exercise 1: Personal Portfolio Website (15 minutes)

Objective: Create a personal portfolio website with multiple pages and dynamic content

Instructions:

  1. Create a portfolio site with Home, About, Projects, and Contact pages
  2. Use template inheritance with a common layout
  3. Implement a project showcase with dynamic data
  4. Add a contact form with validation
  5. Include navigation with active page highlighting

Starter Code:

type Project struct {
    ID          int      `json:"id"`
    Title       string   `json:"title"`
    Description string   `json:"description"`
    Technologies []string `json:"technologies"`
    URL         string   `json:"url"`
    ImageURL    string   `json:"image_url"`
}

func main() {
    r := gin.Default()
    r.LoadHTMLGlob("templates/**/*")
    r.Static("/static", "./static")
    
    // TODO: Implement portfolio routes
    // - GET / (home page)
    // - GET /about (about page)
    // - GET /projects (projects showcase)
    // - GET /contact (contact form)
    // - POST /contact (form submission)
    
    r.Run(":8080")
}

Exercise 2: Task Management Web App (15 minutes)

Objective: Build a task management application with CRUD operations

Requirements:

  • User authentication (login/register/logout)
  • Task list page with add/edit/delete functionality
  • Task categories and priorities
  • Session-based authentication
  • Flash messages for user feedback
  • Responsive design

Hints:

  • Use sessions to store user authentication state
  • Implement CSRF protection for forms
  • Create reusable form components
  • Add client-side JavaScript for better UX

Exercise 3: Mini CMS (10 minutes)

Objective: Create a simple content management system

Challenge: Build a CMS with:

  • Admin login
  • Create/edit/delete pages
  • Dynamic navigation based on created pages
  • Rich text editor integration
  • File upload for images
  • SEO-friendly URLs

Extension Ideas:

  • Add page templates
  • Implement user roles and permissions
  • Create a media library
  • Add page scheduling and drafts

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Templates not loading or parsing errors Cause: Incorrect template paths or syntax errors in templates Solution: Use absolute paths, check template syntax, and enable debug mode for detailed error messages

  2. Problem: CSRF validation failing Cause: Token mismatch between form and session Solution: Ensure token generation and validation use the same session, and tokens are properly included in forms

  3. Problem: Session data not persisting Cause: Session configuration issues or cookie settings Solution: Configure session store properly and ensure cookies are being sent/received correctly

Debugging Tips

  • Use browser developer tools to inspect form submissions and responses
  • Enable Gin debug mode to see detailed error information
  • Log session data and CSRF tokens to verify they’re working correctly
  • Test forms with different browsers and incognito mode

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Always escape user content to prevent XSS attacks
  • Use CSRF protection for all state-changing forms
  • Implement proper session security (secure cookies, appropriate timeouts)
  • Organize templates logically with layouts and partials
  • Use semantic HTML and accessibility best practices

Next Steps in Your Learning Journey

  • Learn about progressive enhancement and HTMX for dynamic interactions
  • Explore advanced template optimization techniques
  • Study modern CSS frameworks and component libraries
  • Investigate server-side rendering with React/Vue.js
  • Practice building complex web applications with database integration

9. Summary and Review (5 minutes)

Key Takeaways

  1. Go’s template system provides powerful, secure HTML generation with XSS protection
  2. Template organization with layouts and partials creates maintainable codebases
  3. Form handling requires proper validation, CSRF protection, and user feedback
  4. Session management enables stateful web applications with authentication and user data

Quick Review Questions

  1. How does Go’s template system prevent XSS attacks?
  2. What is CSRF protection and why is it important?
  3. How do you pass data from handlers to templates?
  4. What are the benefits of using template layouts and partials?

For Instructors

Teaching Tips

  • Show both successful form submissions and validation errors
  • Demonstrate template debugging techniques
  • Use browser developer tools to show how sessions and cookies work
  • Encourage students to build user-friendly interfaces with good UX patterns
  • Show examples of modern web applications built with server-side rendering

Assessment Ideas

  • Build a complete web application with user authentication
  • Create a custom template function library
  • Implement a complex form with multiple validation rules
  • Design a responsive web interface with proper accessibility

Adaptation Notes

  • For beginners: Focus on basic template rendering before advanced features
  • For advanced students: Introduce performance optimization and caching strategies
  • Online delivery: Provide template examples and form testing tools
  • Time management: The complete blog application can be extended for more advanced features