Skip to main content
The renderer package provides HTML template rendering with thread-safe asset management, template caching, and optional minification.

Overview

Key features:
  • Template Caching: Global template cache with modification time tracking
  • Asset Management: Thread-safe asset map for hashed CSS/JS filenames
  • Compression: Optional HTML/CSS/JS minification
  • Custom Functions: Template helpers for common operations
  • File Tracking: Tracks all rendered files for cleanup operations

Creating a Renderer

func New(compress bool, destFs afero.Fs, templateDir string, logger *slog.Logger) *Renderer
Parameters:
  • compress - Enable HTML minification
  • destFs - Destination filesystem (for testing with in-memory FS)
  • templateDir - Path to template directory (e.g., themes/docs/templates)
  • logger - Structured logger
Example:
import (
    "log/slog"
    "github.com/spf13/afero"
    "github.com/Kush-Singh-26/kosh/builder/renderer"
)

logger := slog.Default()
fs := afero.NewOsFs()
r := renderer.New(true, fs, "themes/docs/templates", logger)

// Render a page
data := models.PageData{
    Title: "Getting Started",
    Content: template.HTML("<h1>Hello World</h1>"),
    Meta: map[string]interface{}{"description": "Quick start guide"},
}
r.RenderPage("public/getting-started.html", data)

Renderer Type

type Renderer struct {
    Layout      *template.Template  // Base layout template
    Index       *template.Template  // Homepage template
    Graph       *template.Template  // Graph/network visualization template
    NotFound    *template.Template  // 404 error page template
    Assets      map[string]string   // Asset map (original → hashed filename)
    AssetsMu    sync.RWMutex        // Protects Assets map
    Compress    bool                // Enable minification
    DestFs      afero.Fs            // Destination filesystem
    RenderedMu  sync.RWMutex        // Protects RenderedSet
    RenderedSet map[string]bool     // Tracks rendered files
    logger      *slog.Logger        // Structured logger
}

Core Methods

RenderPage

func (r *Renderer) RenderPage(path string, data models.PageData)
Renders a page using the layout template with automatic asset injection and optional minification. PageData structure:
type PageData struct {
    Title       string                 // Page title
    Content     template.HTML          // Rendered markdown content
    Meta        map[string]interface{} // Frontmatter metadata
    TOC         []TOCEntry             // Table of contents
    Date        time.Time              // Publication date
    Tags        []string               // Post tags
    WordCount   int                    // Word count
    ReadingTime int                    // Estimated reading time (minutes)
    Assets      map[string]string      // Hashed asset map
    Version     string                 // Documentation version
}
Example:
data := models.PageData{
    Title: "Architecture Overview",
    Content: template.HTML(htmlContent),
    Meta: map[string]interface{}{
        "description": "Learn about Kosh architecture",
        "author": "Kosh Team",
    },
    Tags: []string{"architecture", "design"},
    WordCount: 1500,
    ReadingTime: 7,
    Version: "v4.0",
}

r.RenderPage("public/v4.0/architecture.html", data)

Asset Management

SetAssets

func (r *Renderer) SetAssets(assets map[string]string)
Sets the asset map (called after static asset pipeline runs). Example:
assets := map[string]string{
    "/static/css/layout.css": "/static/css/layout.abc123.css",
    "/static/js/search.js":   "/static/js/search.xyz789.js",
}
r.SetAssets(assets)

GetAssets

func (r *Renderer) GetAssets() map[string]string
Retrieves a copy of the asset map (thread-safe). Template usage:
<link rel="stylesheet" href="{{ index .Assets "/static/css/layout.css" }}">
<script src="{{ index .Assets "/static/js/search.js" }}" defer></script>

File Tracking

RegisterFile

func (r *Renderer) RegisterFile(path string)
Registers a file as rendered (called automatically by RenderPage).

GetRenderedFiles

func (r *Renderer) GetRenderedFiles() map[string]bool
Returns all rendered file paths (thread-safe copy).

ClearRenderedFiles

func (r *Renderer) ClearRenderedFiles()
Clears the rendered file set (used in watch mode). Example (watch mode cleanup):
// Before rebuild
oldFiles := r.GetRenderedFiles()
r.ClearRenderedFiles()

// After rebuild
newFiles := r.GetRenderedFiles()
for path := range oldFiles {
    if !newFiles[path] {
        os.Remove(path) // Delete stale file
    }
}

Template Functions

The renderer provides custom template functions:
funcMap := template.FuncMap{
    "lower": strings.ToLower,
    "hasPrefix": strings.HasPrefix,
    "replace": func(from, to, input string) string {
        return strings.ReplaceAll(input, from, to)
    },
    "now": time.Now,
}
Template examples:
<!-- Convert to lowercase -->
<meta name="keywords" content="{{ .Title | lower }}">

<!-- Check URL prefix -->
{{ if hasPrefix .Link "/v4.0/" }}
  <span class="badge">Latest</span>
{{ end }}

<!-- String replacement -->
{{ replace ".md" ".html" .Link }}

<!-- Current timestamp -->
<meta name="build-time" content="{{ now.Format "2006-01-02" }}">

Template Caching

Templates are cached globally with modification time tracking:
type templateCache struct {
    templates map[string]*template.Template
    modTimes  map[string]time.Time
    mu        sync.RWMutex
}
Cache invalidation:
func (tc *templateCache) hasTemplatesChanged() bool {
    tc.mu.RLock()
    defer tc.mu.RUnlock()
    
    for name, modTime := range tc.modTimes {
        info, err := os.Stat(templatePath(name))
        if err != nil || info.ModTime().After(modTime) {
            return true // Template changed
        }
    }
    return false
}
Templates are automatically reloaded if their modification time changes.

Template Structure

Required Templates

layout.html - Base layout with content block:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ .Title }}</title>
    <link rel="stylesheet" href="{{ index .Assets "/static/css/layout.css" }}">
</head>
<body>
    {{ template "content" . }}
    <script src="{{ index .Assets "/static/js/search.js" }}" defer></script>
</body>
</html>

Optional Templates

index.html - Homepage template:
{{ define "content" }}
<div class="hero">
    <h1>{{ .Meta.heroTitle }}</h1>
    <p>{{ .Meta.heroDescription }}</p>
</div>
{{ end }}
404.html - Error page template:
{{ define "content" }}
<div class="error-page">
    <h1>404 - Page Not Found</h1>
    <p>The page you're looking for doesn't exist.</p>
</div>
{{ end }}
graph.html - Network graph visualization (optional).

Compression

When Compress is enabled, HTML is minified using:
import "github.com/Kush-Singh-26/kosh/builder/utils"

if r.Compress {
    mw := utils.Minifier.Writer("text/html", writer)
    defer mw.Close()
    // Write to minified writer
}
Minification features:
  • Remove comments and whitespace
  • Collapse inline CSS/JS
  • Optimize attributes
  • ~20-30% size reduction

Error Handling

The renderer logs errors with structured logging:
r.logger.Error("Failed to render layout", "path", path, "error", err)
r.logger.Warn("Index template not found, falling back to layout", "dir", templateDir)
Template cycle detection:
if strings.Contains(err.Error(), "template") && 
   strings.Contains(err.Error(), "not defined") {
    logger.Error("Possible template cycle detected - check for circular {{ template }} references")
}

Performance Optimization

Buffered Writing

import "github.com/Kush-Singh-26/kosh/builder/utils"

bw := utils.SharedBufioWriterPool.Get(file)
defer func() {
    _ = bw.Flush()
    utils.SharedBufioWriterPool.Put(bw)
}()
Uses object pooling to reduce allocations.

Template Precompilation

Templates are compiled once and cached:
// First request - compile and cache
tmpl, _ := template.New("layout.html").Funcs(funcMap).ParseFiles(layoutPath)
tc.setTemplate("layout", tmpl, modTime)

// Subsequent requests - use cache
if cacheValid {
    return &Renderer{Layout: tc.templates["layout"]}
}

Thread Safety

All public methods are thread-safe:
  • Assets map protected by AssetsMu (RWMutex)
  • RenderedSet protected by RenderedMu (RWMutex)
  • Template cache uses global sync.RWMutex
Concurrent rendering example:
var wg sync.WaitGroup
for _, post := range posts {
    wg.Add(1)
    go func(p Post) {
        defer wg.Done()
        r.RenderPage(p.OutputPath, p.Data) // Thread-safe
    }(post)
}
wg.Wait()

Build docs developers (and LLMs) love