go
58 lines · 8 steps
Per-tenant HTML templates with Gin's renderer
A thread-safe registry gives each tenant its own template set while satisfying Gin's HTMLRender interface.
Explained by
highlit
1package theme
2
3import (
4 "fmt"
5 "html/template"
6 "path/filepath"
7 "sync"
8
9 "github.com/gin-gonic/gin/render"
10)
11
12type Registry struct {
13 mu sync.RWMutex
14 tenants map[string]*template.Template
15 funcs template.FuncMap
16 baseGlob string
17}
18
19func NewRegistry(baseGlob string, funcs template.FuncMap) *Registry {
20 return &Registry{
21 tenants: make(map[string]*template.Template),
22 funcs: funcs,
23 baseGlob: baseGlob,
24 }
25}
26
27func (r *Registry) Load(tenant, dir string) error {
28 tmpl := template.New(tenant).Funcs(r.funcs)
29
30 if _, err := tmpl.ParseGlob(r.baseGlob); err != nil {
31 return fmt.Errorf("parse base templates: %w", err)
32 }
33 if _, err := tmpl.ParseGlob(filepath.Join(dir, "*.html")); err != nil {
34 return fmt.Errorf("parse tenant %q templates: %w", tenant, err)
35 }
36
37 r.mu.Lock()
38 r.tenants[tenant] = tmpl
39 r.mu.Unlock()
40 return nil
41}
42
43func (r *Registry) Instance(name string, data any) render.Render {
44 tenant, _ := data.(map[string]any)["tenant"].(string)
45
46 r.mu.RLock()
47 tmpl, ok := r.tenants[tenant]
48 if !ok {
49 tmpl = r.tenants["default"]
50 }
51 r.mu.RUnlock()
52
53 return render.HTML{
54 Template: tmpl,
55 Name: name,
56 Data: data,
57 }
58}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Implementing Gin's HTMLRender interface lets you swap in fully custom, per-request template selection.
- 2An RWMutex lets many requests read the template map concurrently while loads take exclusive access.
- 3Layering ParseGlob calls composes shared base templates with tenant-specific overrides in one template set.
Related explainers
java
public class SlidingLogRateLimiter { private final int maxRequests; private final long windowMillis;
How a sliding-log rate limiter works
rate-limiting
concurrency
sliding-window
Advanced
8 steps
python
import time import threading from enum import Enum from functools import wraps
Building a circuit breaker in Python
circuit-breaker
resilience
decorators
Advanced
7 steps
typescript
import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'; import { BehaviorSubject, Observable, throwError } from 'rxjs'; import { catchError, filter, take, switchMap } from 'rxjs/operators';
Refreshing auth tokens in an Angular interceptor
http-interceptor
token-refresh
rxjs
Advanced
8 steps
go
package auth import ( "net/http"
Setting and reading secure session cookies in Go
cookies
session-management
security
Intermediate
6 steps
php
<?php namespace App\Http\Middleware;
Idempotent requests with Laravel middleware
idempotency
middleware
caching
Advanced
8 steps
go
package middleware import ( "net/http"
A maintenance-mode gate in Gin
middleware
atomic-flag
concurrency
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/per-tenant-html-templates-with-gin-s-renderer-explained-go-6293/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.