Merge pull request 'develop' (#14) from develop into main

Reviewed-on: #14
This commit is contained in:
Zeni Kim 2026-05-06 22:03:48 -04:00
commit 4ea9293cb3
7 changed files with 704 additions and 114 deletions

View file

@ -14,6 +14,8 @@ App_HTTPS_HOSTS=example.com, www.example.com
App_REDIRECT_HTTP_TO_HTTPS=false
App_CERT_FILE_PATH=tls/server.crt
App_KEY_FILE_PATH=tls/server.key
LOG_STDOUT_ENABLE=true
LOG_LEVEL=debug
#######################################
###### TEMPLATES ######

View file

@ -9,6 +9,7 @@ import (
"math/rand/v2"
"os"
"strconv"
"time"
"git.smarteching.com/goffee/core"
"git.smarteching.com/goffee/core/template/components"
@ -861,3 +862,140 @@ func getPaginatedPageViews(values [][]components.ContentTableTD, limit int, offs
return values[offset:end] // Slice the array
}
// Custom Templates functions
func TemplatesFunctions(c *core.Context) *core.Response {
// check if template engine is enabled
TemplateEnableStr := os.Getenv("TEMPLATE_ENABLE")
if TemplateEnableStr == "" {
TemplateEnableStr = "false"
}
TemplateEnable, _ := strconv.ParseBool(TemplateEnableStr)
if TemplateEnable {
loggr := c.GetLogger()
loggr.Debug("D e b u g")
loggr.Info("I n f o")
loggr.Warning("W a r n i n g")
loggr.Error("E R R O R")
tmplData := SamplePageData()
return c.Response.Template("custom_templates_functions.html", tmplData)
} else {
message := "{\"message\": \"Error, template not enabled\"}"
return c.Response.Json(message)
}
}
// Author represents the writer of an article to show the custom functions
type Author struct {
Name string
Avatar string
Bio string
}
// Article represents a blog post or news entry used to test template helpers to show the custom functions
type Article struct {
Title string
Slug string
Excerpt string
Body string
Tags []string
PublishedAt time.Time
UpdatedAt time.Time
Author Author
Views int
Price float64
Featured bool
Subtitle string // intentionally left empty on some entries to test coalesce/defaultVal
}
// PageData is the top-level context passed to the HTML template to show the custom functions
type PageData struct {
SiteTitle string
Articles []Article
}
// SamplePageData returns a populated PageData ready to be rendered by the template.
func SamplePageData() PageData {
return PageData{
SiteTitle: "go/template lab",
Articles: []Article{
{
Title: "getting started with go templates",
Slug: "getting-started-go-templates",
Excerpt: "Go's html/template package is both powerful and safe by default. In this article we explore how to extend it with custom FuncMap helpers that bring it closer to the expressiveness of Liquid or Twig, without sacrificing any of the security guarantees.",
Tags: []string{"go", "templates", "web", "backend"},
PublishedAt: time.Now().Add(-3 * 24 * time.Hour),
UpdatedAt: time.Now().Add(-1 * 24 * time.Hour),
Views: 14200,
Price: 0,
Featured: true,
Subtitle: "",
Author: Author{
Name: "marina voss",
Avatar: "MV",
Bio: "Senior backend engineer focused on developer tooling and observability.",
},
},
{
Title: "building a blog engine in go",
Slug: "blog-engine-go",
Excerpt: "We walk through building a minimal but complete blog engine using only the Go standard library: routing with net/http, persistence with database/sql, and rendering with html/template.",
Tags: []string{"go", "blog", "sqlite"},
PublishedAt: time.Now().Add(-10 * 24 * time.Hour),
UpdatedAt: time.Now().Add(-10 * 24 * time.Hour),
Views: 8750,
Price: 9.99,
Featured: false,
Subtitle: "A zero-dependency approach",
Author: Author{
Name: "rafael okonkwo",
Avatar: "RO",
Bio: "Full-stack engineer and open-source contributor. Writes about Go and distributed systems.",
},
},
{
Title: "concurrency patterns you should know",
Slug: "concurrency-patterns-go",
Excerpt: "Goroutines are cheap, but misusing them is expensive. This deep-dive covers fan-out/fan-in, pipelines, semaphores, and error group patterns with real production examples.",
Tags: []string{"go", "concurrency", "goroutines", "advanced"},
PublishedAt: time.Now().Add(-45 * 24 * time.Hour),
UpdatedAt: time.Now().Add(-40 * 24 * time.Hour),
Views: 31400,
Price: 14.99,
Featured: true,
Subtitle: "",
Author: Author{
Name: "selin çelik",
Avatar: "SÇ",
Bio: "Systems programmer. Previously at Cloudflare. Loves writing about the internals of things.",
},
},
{
Title: "understanding go interfaces",
Slug: "understanding-go-interfaces",
Excerpt: "Interfaces in Go are implicit and structural, which makes them both elegant and occasionally surprising. We look at how the runtime dispatches method calls and how to design composable interfaces.",
Tags: []string{"go", "interfaces", "design"},
PublishedAt: time.Now().Add(-2 * time.Hour),
UpdatedAt: time.Now().Add(-1 * time.Hour),
Views: 420,
Price: 0,
Featured: false,
Subtitle: "Implicit, structural, and powerful",
Author: Author{
Name: "marina voss",
Avatar: "MV",
Bio: "Senior backend engineer focused on developer tooling and observability.",
},
},
},
}
}

47
go.mod
View file

@ -7,57 +7,58 @@ replace (
git.smarteching.com/goffee/cup/models => ./models
)
go 1.24.1
go 1.25.0
require (
git.smarteching.com/goffee/core v1.9.5
git.smarteching.com/goffee/core v1.9.6
github.com/google/uuid v1.6.0
github.com/hibiken/asynq v0.25.1
github.com/hibiken/asynq v0.26.0
github.com/joho/godotenv v1.5.1
github.com/julienschmidt/httprouter v1.3.0
gorm.io/gorm v1.30.0
gorm.io/gorm v1.31.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
git.smarteching.com/zeni/go-chart/v2 v2.1.4 // indirect
git.smarteching.com/zeni/go-charts/v2 v2.6.11 // indirect
github.com/SparkPost/gosparkpost v0.2.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-chi/chi/v5 v5.2.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-chi/chi/v5 v5.2.5 // indirect
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
github.com/go-sql-driver/mysql v1.10.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/harranali/mailing v1.2.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.5 // indirect
github.com/jackc/pgx/v5 v5.9.2 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailgun/errors v0.4.0 // indirect
github.com/mailgun/errors v0.5.0 // indirect
github.com/mailgun/mailgun-go/v4 v4.23.0 // indirect
github.com/mattn/go-sqlite3 v1.14.28 // indirect
github.com/mattn/go-sqlite3 v1.14.44 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/redis/go-redis/v9 v9.11.0 // indirect
github.com/redis/go-redis/v9 v9.19.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/sendgrid/sendgrid-go v3.16.1+incompatible // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/cast v1.9.2 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/image v0.29.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
github.com/spf13/cast v1.10.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/image v0.39.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gorm.io/driver/mysql v1.6.0 // indirect
gorm.io/driver/postgres v1.6.0 // indirect
gorm.io/driver/sqlite v1.6.0 // indirect

145
go.sum
View file

@ -1,11 +1,11 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.smarteching.com/goffee/core v1.9.2 h1:SpLAhsTxssPItgkBYLN3UxMH5s+q8qVtbvmRom3WKh8=
git.smarteching.com/goffee/core v1.9.2/go.mod h1:L9a+kL1RVHRHzp+DzCS1apwVLyZAvGE6B94UlyIMhIg=
git.smarteching.com/goffee/core v1.9.5 h1:rq6vI4WSUMGQNzJvhNWmtY2ycC7UszEvXpQ7uUR8sZY=
git.smarteching.com/goffee/core v1.9.5/go.mod h1:ifiBgTOR4zCMzdGsabNrEO792EHny2o149NGe3TSlms=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
git.smarteching.com/goffee/core v1.9.6 h1:GY1EXqbmBEWZAVrl3q22Izb6aXhQzFVQBv2hWhK/So8=
git.smarteching.com/goffee/core v1.9.6/go.mod h1:ifiBgTOR4zCMzdGsabNrEO792EHny2o149NGe3TSlms=
git.smarteching.com/zeni/go-chart/v2 v2.1.4 h1:pF06+F6eqJLIG8uMiTVPR5TygPGMjM/FHMzTxmu5V/Q=
git.smarteching.com/zeni/go-chart/v2 v2.1.4/go.mod h1:b3ueW9h3pGGXyhkormZAvilHaG4+mQti+bMNPdQBeOQ=
git.smarteching.com/zeni/go-charts/v2 v2.6.11 h1:9udzlv3uxGXszpplfkL5IaTUrgkNj++KwhbaN1vVEqI=
git.smarteching.com/zeni/go-charts/v2 v2.6.11/go.mod h1:3OpRPSXg7Qx4zcgsmwsC9ZFB9/wAkGSbnXf1wIbHYCg=
github.com/SparkPost/gosparkpost v0.2.0 h1:yzhHQT7cE+rqzd5tANNC74j+2x3lrPznqPJrxC1yR8s=
github.com/SparkPost/gosparkpost v0.2.0/go.mod h1:S9WKcGeou7cbPpx0kTIgo8Q69WZvUmVeVzbD+djalJ4=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
@ -23,46 +23,37 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE=
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/harranali/mailing v1.2.0 h1:ihIyJwB8hyRVcdk+v465wk1PHMrSrgJqo/kMd+gZClY=
github.com/harranali/mailing v1.2.0/go.mod h1:4a5N3yG98pZKluMpmcYlTtll7bisvOfGQEMIng3VQk4=
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg=
github.com/hibiken/asynq v0.26.0 h1:1Zxr92MlDnb1Zt/QR5g2vSCqUS03i95lUfqx5X7/wrw=
github.com/hibiken/asynq v0.26.0/go.mod h1:Qk4e57bTnWDoyJ67VkchuV6VzSM9IQW2nPvAGuDyw58=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
@ -77,21 +68,21 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailgun/errors v0.4.0 h1:6LFBvod6VIW83CMIOT9sYNp28TCX0NejFPP4dSX++i8=
github.com/mailgun/errors v0.4.0/go.mod h1:xGBaaKdEdQT0/FhwvoXv4oBaqqmVZz9P1XEnvD/onc0=
github.com/mailgun/errors v0.5.0 h1:pLQo8uhAdORsjN69mGixSr0pGs46z/BW/FQXd8HG1VM=
github.com/mailgun/errors v0.5.0/go.mod h1:+2nrgY77E0vDkG4ErehpcpbSkMLkseJzKbrva89WeSs=
github.com/mailgun/mailgun-go/v4 v4.23.0 h1:jPEMJzzin2s7lvehcfv/0UkyBu18GvcURPr2+xtZRbk=
github.com/mailgun/mailgun-go/v4 v4.23.0/go.mod h1:imTtizoFtpfZqPqGP8vltVBB6q9yWcv6llBhfFeElZU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -103,10 +94,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc=
github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k=
github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
@ -114,81 +103,55 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBUpXyTcTXy/Rl9N9aZ/Qw=
github.com/sendgrid/sendgrid-go v3.16.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1GjJohAA0p6hVEaDtHWWs=
github.com/sendgrid/sendgrid-go v3.16.1+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=

13
main.go
View file

@ -58,6 +58,19 @@ func main() {
app.SetGormConfig(config.GetGormConfig())
app.SetCacheConfig(config.GetCacheConfig())
app.Bootstrap()
// Set log level from environment variable (debug, info, warning, error)
if levelStr := env.GetVar("LOG_LEVEL"); levelStr != "" {
switch levelStr {
case "debug":
logger.ResolveLogger().SetLevel(logger.DEBUG)
case "info":
logger.ResolveLogger().SetLevel(logger.INFO)
case "warning":
logger.ResolveLogger().SetLevel(logger.WARNING)
case "error":
logger.ResolveLogger().SetLevel(logger.ERROR)
}
}
app.RegisterTemplates(resources)
registerGlobalHooks()
registerRoutes()

View file

@ -60,4 +60,6 @@ func registerRoutes() {
controller.Get("/appsession", controllers.AppSession)
controller.Post("/appsession", controllers.AppSession)
controller.Get("/templatesfunc", controllers.TemplatesFunctions)
}

View file

@ -0,0 +1,471 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{.SiteTitle}} — template lab</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=JetBrains+Mono:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0d0d0f;
--surface: #141416;
--border: #222226;
--muted: #3a3a40;
--subtle: #6b6b75;
--body: #c8c8d0;
--heading: #f0f0f4;
--accent: #e8c547;
--accent2: #5b8cff;
--danger: #ff6b6b;
--mono: 'JetBrains Mono', monospace;
--serif: 'Playfair Display', Georgia, serif;
--sans: 'DM Sans', sans-serif;
}
html { background: var(--bg); color: var(--body); font-family: var(--sans); font-size: 16px; line-height: 1.6; }
body { max-width: 1100px; margin: 0 auto; padding: 0 2rem 6rem; }
/* ── Header ── */
header {
border-bottom: 1px solid var(--border);
padding: 3rem 0 2rem;
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 1rem;
}
.site-title {
font-family: var(--mono);
font-size: .75rem;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--accent);
}
.site-headline {
font-family: var(--serif);
font-size: clamp(2rem, 5vw, 3.5rem);
color: var(--heading);
line-height: 1.1;
margin-top: .4rem;
}
.site-headline em { color: var(--accent); font-style: italic; }
.helper-count {
font-family: var(--mono);
font-size: .7rem;
color: var(--subtle);
text-align: right;
white-space: nowrap;
}
/* ── Section label ── */
.section-label {
display: flex;
align-items: center;
gap: 1rem;
margin: 4rem 0 1.5rem;
}
.section-label span {
font-family: var(--mono);
font-size: .65rem;
letter-spacing: .15em;
text-transform: uppercase;
color: var(--accent);
white-space: nowrap;
}
.section-label::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
/* ── Helper badge ── */
.badge {
display: inline-block;
font-family: var(--mono);
font-size: .6rem;
letter-spacing: .08em;
padding: .15em .5em;
border-radius: 3px;
border: 1px solid var(--muted);
color: var(--subtle);
vertical-align: middle;
margin-left: .4rem;
}
.badge-accent { border-color: var(--accent); color: var(--accent); }
.badge-blue { border-color: var(--accent2); color: var(--accent2); }
/* ── Demo block ── */
.demo-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1px;
background: var(--border);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
.demo-cell {
background: var(--surface);
padding: 1.5rem;
}
.demo-cell h3 {
font-family: var(--mono);
font-size: .7rem;
letter-spacing: .1em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: .75rem;
}
.demo-cell .result {
font-size: 1rem;
color: var(--heading);
}
.demo-cell .note {
margin-top: .5rem;
font-size: .75rem;
color: var(--subtle);
font-family: var(--mono);
}
/* ── Article cards ── */
.article-list { display: flex; flex-direction: column; gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.article-card { background: var(--surface); padding: 2rem; display: grid; grid-template-columns: 1fr auto; gap: 1rem; align-items: start; transition: background .15s; }
.article-card:hover { background: #18181c; }
.card-meta { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; margin-bottom: .75rem; }
.tag {
font-family: var(--mono);
font-size: .6rem;
letter-spacing: .08em;
text-transform: uppercase;
padding: .2em .55em;
background: var(--muted);
border-radius: 3px;
color: var(--body);
}
.tag-featured { background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); }
.card-title { font-family: var(--serif); font-size: 1.35rem; color: var(--heading); line-height: 1.25; margin-bottom: .5rem; }
.card-subtitle { font-size: .85rem; color: var(--accent2); margin-bottom: .6rem; font-style: italic; }
.card-excerpt { font-size: .875rem; color: var(--body); line-height: 1.65; }
.card-author { display: flex; align-items: center; gap: .6rem; margin-top: 1.25rem; }
.avatar {
width: 32px; height: 32px; border-radius: 50%;
background: var(--muted);
display: flex; align-items: center; justify-content: center;
font-family: var(--mono); font-size: .55rem; color: var(--accent);
flex-shrink: 0;
}
.author-name { font-size: .8rem; color: var(--heading); }
.author-date { font-size: .72rem; color: var(--subtle); font-family: var(--mono); }
.card-aside { text-align: right; }
.views { font-family: var(--mono); font-size: .75rem; color: var(--subtle); white-space: nowrap; }
.views strong { color: var(--body); display: block; font-size: 1rem; }
.price-badge {
display: inline-block; margin-top: .75rem;
font-family: var(--mono); font-size: .7rem;
padding: .3em .7em; border-radius: 4px;
background: color-mix(in srgb, var(--accent2) 15%, transparent);
color: var(--accent2); border: 1px solid var(--accent2);
}
.price-free {
background: color-mix(in srgb, #4caf50 12%, transparent);
color: #6fcf97; border-color: #4caf50;
}
/* ── Logic demo table ── */
.logic-table { width: 100%; border-collapse: collapse; font-size: .85rem; }
.logic-table th {
font-family: var(--mono); font-size: .65rem; letter-spacing: .1em; text-transform: uppercase;
color: var(--subtle); text-align: left; padding: .6rem 1rem;
border-bottom: 1px solid var(--border);
}
.logic-table td { padding: .75rem 1rem; border-bottom: 1px solid var(--border); color: var(--body); }
.logic-table tr:last-child td { border-bottom: none; }
.logic-table tr:hover td { background: var(--surface); }
.val { font-family: var(--mono); font-size: .8rem; color: var(--accent); }
/* ── Footer ── */
footer {
margin-top: 5rem; padding-top: 2rem; border-top: 1px solid var(--border);
font-family: var(--mono); font-size: .65rem; color: var(--muted);
display: flex; justify-content: space-between; flex-wrap: wrap; gap: .5rem;
}
</style>
</head>
<body>
<!-- ════════════════════════════════════════════
HEADER
════════════════════════════════════════════ -->
<header>
<div>
<p class="site-title">{{.SiteTitle}}</p>
<h1 class="site-headline">Template <em>helpers</em><br>in action</h1>
</div>
<p class="helper-count">22 helpers registered<br>across 6 groups</p>
</header>
<!-- ════════════════════════════════════════════
STRING HELPERS
════════════════════════════════════════════ -->
<div class="section-label"><span>String helpers</span></div>
{{$first := first .Articles}}
<div class="demo-grid">
<div class="demo-cell">
<h3>capitalize <span class="badge">capitalize</span></h3>
<p class="result">{{capitalize $first.Title}}</p>
<p class="note">input: "{{$first.Title}}"</p>
</div>
<div class="demo-cell">
<h3>truncate <span class="badge">truncate</span></h3>
<p class="result">{{$first.Excerpt | truncate 80}}</p>
<p class="note">capped at 80 chars</p>
</div>
<div class="demo-cell">
<h3>prepend <span class="badge">prepend</span></h3>
<p class="result">{{prepend $first.Slug "/articles/"}}</p>
<p class="note">prepended "/articles/" to slug</p>
</div>
<div class="demo-cell">
<h3>strAppend <span class="badge">strAppend</span></h3>
<p class="result">{{strAppend $first.Slug ".html"}}</p>
<p class="note">appended ".html" to slug</p>
</div>
<div class="demo-cell">
<h3>split → join <span class="badge">split</span> <span class="badge">join</span></h3>
{{$parts := split "go,templates,funcmap" ","}}
<p class="result">{{join $parts " · "}}</p>
<p class="note">split on "," then joined with " · "</p>
</div>
</div>
<!-- ════════════════════════════════════════════
NUMBER & DATE HELPERS
════════════════════════════════════════════ -->
<div class="section-label"><span>Number &amp; Date helpers</span></div>
<div class="demo-grid">
<div class="demo-cell">
<h3>fmtNumber — int <span class="badge">fmtNumber</span></h3>
<p class="result">{{fmtNumber $first.Views}}</p>
<p class="note">raw value: {{$first.Views}}</p>
</div>
{{$paid := index .Articles 2}}
<div class="demo-cell">
<h3>fmtNumber — float <span class="badge">fmtNumber</span></h3>
<p class="result">$ {{fmtNumber $paid.Price}}</p>
<p class="note">raw value: {{$paid.Price}}</p>
</div>
<div class="demo-cell">
<h3>fmtDate "short" <span class="badge">fmtDate</span></h3>
<p class="result">{{fmtDate $first.PublishedAt "short"}}</p>
<p class="note">layout: "02 Jan 2006"</p>
</div>
<div class="demo-cell">
<h3>fmtDate "long" <span class="badge">fmtDate</span></h3>
<p class="result">{{fmtDate $first.PublishedAt "long"}}</p>
<p class="note">layout: "02 January 2006"</p>
</div>
<div class="demo-cell">
<h3>fmtDate "iso" <span class="badge">fmtDate</span></h3>
<p class="result">{{fmtDate $first.PublishedAt "iso"}}</p>
<p class="note">layout: "2006-01-02"</p>
</div>
<div class="demo-cell">
<h3>timeAgo <span class="badge">timeAgo</span></h3>
{{range .Articles}}
<p class="result" style="margin-bottom:.3rem">{{timeAgo .PublishedAt}} <span class="note" style="display:inline">— {{fmtDate .PublishedAt "short"}}</span></p>
{{end}}
</div>
</div>
<!-- ════════════════════════════════════════════
COLLECTION HELPERS
════════════════════════════════════════════ -->
<div class="section-label"><span>Collection helpers</span></div>
<div class="demo-grid">
<div class="demo-cell">
<h3>first <span class="badge">first</span></h3>
{{with first .Articles}}
<p class="result">{{capitalize .Title}}</p>
<p class="note">first article in the list</p>
{{end}}
</div>
<div class="demo-cell">
<h3>last <span class="badge">last</span></h3>
{{with last .Articles}}
<p class="result">{{capitalize .Title}}</p>
<p class="note">last article in the list</p>
{{end}}
</div>
<div class="demo-cell">
<h3>sliceOf 02 <span class="badge">sliceOf</span></h3>
{{range sliceOf .Articles 0 2}}
<p class="result" style="margin-bottom:.25rem">— {{capitalize .Title}}</p>
{{end}}
<p class="note">only first 2 of {{len .Articles}} articles</p>
</div>
<div class="demo-cell">
<h3>contains — slice <span class="badge">contains</span></h3>
{{if contains $first.Tags "go"}}
<p class="result" style="color:var(--accent)">✓ has tag "go"</p>
{{else}}
<p class="result" style="color:var(--danger)">✗ missing tag "go"</p>
{{end}}
<p class="note">tags: {{join $first.Tags ", "}}</p>
</div>
<div class="demo-cell">
<h3>contains — string <span class="badge">contains</span></h3>
{{if contains $first.Excerpt "FuncMap"}}
<p class="result" style="color:var(--accent)">✓ excerpt mentions "FuncMap"</p>
{{else}}
<p class="result" style="color:var(--danger)">✗ not found</p>
{{end}}
<p class="note">substring search on .Excerpt</p>
</div>
<div class="demo-cell">
<h3>join <span class="badge">join</span></h3>
<p class="result">{{join $first.Tags " / "}}</p>
<p class="note">tags joined with " / "</p>
</div>
</div>
<!-- ════════════════════════════════════════════
LOGIC HELPERS
════════════════════════════════════════════ -->
<div class="section-label"><span>Logic helpers</span></div>
<table class="logic-table">
<thead>
<tr>
<th>Helper</th>
<th>Article</th>
<th>Input</th>
<th>Output</th>
</tr>
</thead>
<tbody>
{{range .Articles}}
<tr>
<td><span class="badge badge-accent">defaultVal</span></td>
<td>{{.Title | capitalize | truncate 30}}</td>
<td class="val">.Subtitle ({{if .Subtitle}}"{{.Subtitle}}"{{else}}empty{{end}})</td>
<td class="val">{{defaultVal "No subtitle" .Subtitle}}</td>
</tr>
<tr>
<td><span class="badge badge-blue">ternary</span></td>
<td>{{capitalize .Title | truncate 30}}</td>
<td class="val">.Featured = {{.Featured}}</td>
<td class="val">{{ternary "⭐ featured" "regular" .Featured}}</td>
</tr>
<tr>
<td><span class="badge badge-accent">coalesce</span></td>
<td>{{capitalize .Title | truncate 30}}</td>
<td class="val">.Subtitle → .Title</td>
<td class="val">{{coalesce .Subtitle .Title}}</td>
</tr>
{{end}}
</tbody>
</table>
<!-- ════════════════════════════════════════════
FULL ARTICLE CARDS (all helpers combined)
════════════════════════════════════════════ -->
<div class="section-label"><span>Full article cards — all helpers combined</span></div>
<div class="article-list">
{{range .Articles}}
<div class="article-card">
<div>
<div class="card-meta">
{{if .Featured}}<span class="tag tag-featured">featured</span>{{end}}
{{range .Tags}}<span class="tag">{{.}}</span>{{end}}
</div>
<h2 class="card-title">{{capitalize .Title}}</h2>
{{with coalesce .Subtitle ""}}
<p class="card-subtitle">{{.}}</p>
{{end}}
<p class="card-excerpt">{{.Excerpt | truncate 160}}</p>
<a style="font-family:var(--mono);font-size:.7rem;color:var(--accent2);text-decoration:none;margin-top:.75rem;display:inline-block;"
href="{{prepend .Slug "/articles/"}}">
{{prepend .Slug "/articles/"}} →
</a>
<div class="card-author">
<div class="avatar">{{.Author.Avatar}}</div>
<div>
<div class="author-name">{{capitalize .Author.Name}}</div>
<div class="author-date">
{{fmtDate .PublishedAt "short"}} · {{timeAgo .PublishedAt}}
</div>
</div>
</div>
</div>
<div class="card-aside">
<div class="views">
<strong>{{fmtNumber .Views}}</strong>
views
</div>
{{if gt .Price 0.0}}
<span class="price-badge">$ {{fmtNumber .Price}}</span>
{{else}}
<span class="price-badge price-free">free</span>
{{end}}
</div>
</div>
{{end}}
</div>
<!-- ════════════════════════════════════════════
FOOTER
════════════════════════════════════════════ -->
<footer>
<span>{{.SiteTitle}} / template sandbox</span>
<span>{{len .Articles}} articles · {{fmtDate (first .Articles).PublishedAt "iso"}} → {{fmtDate (last .Articles).PublishedAt "iso"}}</span>
</footer>
</body>
</html>