diff --git a/internal/routes/town.go b/internal/routes/town.go
index 6a2d243..ad61ee1 100644
--- a/internal/routes/town.go
+++ b/internal/routes/town.go
@@ -3,11 +3,8 @@ package routes
import (
"dk/internal/middleware"
"dk/internal/router"
- "dk/internal/template"
"dk/internal/template/components"
- "fmt"
-
- "github.com/valyala/fasthttp"
+ "dk/internal/towns"
)
func RegisterTownRoutes(r *router.Router) {
@@ -19,21 +16,8 @@ func RegisterTownRoutes(r *router.Router) {
}
func showTown(ctx router.Ctx, _ []string) {
- tmpl, err := template.Cache.Load("town/town.html")
- if err != nil {
- ctx.SetStatusCode(fasthttp.StatusInternalServerError)
- fmt.Fprintf(ctx, "Template error: %v", err)
- return
- }
-
- content := tmpl.RenderNamed(map[string]any{
- "town": ctx.UserValue("town"),
+ town := ctx.UserValue("town").(*towns.Town)
+ components.RenderPageTemplate(ctx, town.Name, "town/town.html", map[string]any{
+ "town": town,
})
-
- pageData := components.NewPageData("Town - Dragon Knight", content)
- if err := components.RenderPage(ctx, pageData, nil); err != nil {
- ctx.SetStatusCode(fasthttp.StatusInternalServerError)
- fmt.Fprintf(ctx, "Template error: %v", err)
- return
- }
}
diff --git a/internal/template/components/components.go b/internal/template/components/page.go
similarity index 61%
rename from internal/template/components/components.go
rename to internal/template/components/page.go
index 535db86..d295936 100644
--- a/internal/template/components/components.go
+++ b/internal/template/components/page.go
@@ -3,29 +3,15 @@ package components
import (
"fmt"
"maps"
+ "strings"
"dk/internal/auth"
- "dk/internal/csrf"
"dk/internal/middleware"
"dk/internal/router"
"dk/internal/template"
-)
-// GenerateTopNav generates the top navigation HTML based on authentication status
-func GenerateTopNav(ctx router.Ctx) string {
- if middleware.IsAuthenticated(ctx) {
- csrfField := csrf.HiddenField(ctx, auth.Manager)
- return fmt.Sprintf(`
-
`, csrfField)
- } else {
- return `
-
-
`
- }
-}
+ "github.com/valyala/fasthttp"
+)
// PageData holds common page template data
type PageData struct {
@@ -100,3 +86,39 @@ func NewPageData(title, content string) PageData {
Build: "dev",
}
}
+
+// PageTitle returns a proper title for a rendered page. If an empty string
+// is given, returns "Dragon Knight". If the provided title already has " - Dragon Knight"
+// at the end, returns title as-is. Appends " - Dragon Knight" to title otherwise.
+func PageTitle(title string) string {
+ if title == "" {
+ return "Dragon Knight"
+ }
+
+ if strings.HasSuffix(" - Dragon Knight", title) {
+ return title
+ }
+
+ return title + " - Dragon Knight"
+}
+
+// RenderPageTemplate is a simplified helper that renders a template within the page layout.
+// It loads the template, renders it with the provided data, and then renders the full page.
+// Returns true if successful, false if an error occurred (error is written to response).
+func RenderPageTemplate(ctx router.Ctx, title, templateName string, data map[string]any) bool {
+ content, err := template.RenderNamed(templateName, data)
+ if err != nil {
+ ctx.SetStatusCode(fasthttp.StatusInternalServerError)
+ fmt.Fprintf(ctx, "Template error: %v", err)
+ return false
+ }
+
+ pageData := NewPageData(PageTitle(title), content)
+ if err := RenderPage(ctx, pageData, nil); err != nil {
+ ctx.SetStatusCode(fasthttp.StatusInternalServerError)
+ fmt.Fprintf(ctx, "Template error: %v", err)
+ return false
+ }
+
+ return true
+}
diff --git a/internal/template/components/topnav.go b/internal/template/components/topnav.go
new file mode 100644
index 0000000..a5128d3
--- /dev/null
+++ b/internal/template/components/topnav.go
@@ -0,0 +1,24 @@
+package components
+
+import (
+ "dk/internal/auth"
+ "dk/internal/csrf"
+ "dk/internal/middleware"
+ "dk/internal/router"
+ "fmt"
+)
+
+// GenerateTopNav generates the top navigation HTML based on authentication status
+func GenerateTopNav(ctx router.Ctx) string {
+ if middleware.IsAuthenticated(ctx) {
+ return fmt.Sprintf(`
+
`, csrf.HiddenField(ctx, auth.Manager))
+ } else {
+ return `
+
+
`
+ }
+}
diff --git a/internal/template/template.go b/internal/template/template.go
index 9c0dd2b..4fc0560 100644
--- a/internal/template/template.go
+++ b/internal/template/template.go
@@ -23,7 +23,7 @@ type TemplateCache struct {
type RenderOptions struct {
ResolveIncludes bool
- Blocks map[string]string
+ Blocks map[string]string
}
type Template struct {
@@ -261,7 +261,7 @@ func (t *Template) getStructField(obj any, fieldName string) any {
if obj == nil {
return nil
}
-
+
rv := reflect.ValueOf(obj)
if rv.Kind() == reflect.Ptr {
if rv.IsNil() {
@@ -269,16 +269,16 @@ func (t *Template) getStructField(obj any, fieldName string) any {
}
rv = rv.Elem()
}
-
+
if rv.Kind() != reflect.Struct {
return nil
}
-
+
field := rv.FieldByName(fieldName)
if !field.IsValid() {
return nil
}
-
+
return field.Interface()
}
@@ -314,29 +314,29 @@ func (t *Template) WriteToWithOptions(ctx *fasthttp.RequestCtx, data any, opts R
// processIncludes handles {include "template.html"} directives
func (t *Template) processIncludes(content string, data map[string]any, opts RenderOptions) string {
result := content
-
+
for {
start := strings.Index(result, "{include ")
if start == -1 {
break
}
-
+
end := strings.Index(result[start:], "}")
if end == -1 {
break
}
end += start
-
- directive := result[start+9:end] // Skip "{include "
+
+ directive := result[start+9 : end] // Skip "{include "
templateName := strings.Trim(directive, "\" ")
-
+
if includedTemplate, err := t.cache.Load(templateName); err == nil {
var includedContent string
if data != nil {
// Create new options to pass blocks to included template
includeOpts := RenderOptions{
ResolveIncludes: opts.ResolveIncludes,
- Blocks: opts.Blocks,
+ Blocks: opts.Blocks,
}
includedContent = includedTemplate.RenderNamedWithOptions(includeOpts, data)
} else {
@@ -348,7 +348,7 @@ func (t *Template) processIncludes(content string, data map[string]any, opts Ren
result = result[:start] + result[end+1:]
}
}
-
+
return result
}
@@ -357,16 +357,16 @@ func (t *Template) processYield(content string, opts RenderOptions) string {
if opts.Blocks == nil {
return strings.ReplaceAll(content, "{yield}", "")
}
-
+
result := content
for blockName, blockContent := range opts.Blocks {
yieldPlaceholder := fmt.Sprintf("{yield %s}", blockName)
result = strings.ReplaceAll(result, yieldPlaceholder, blockContent)
}
-
+
// Replace any remaining {yield} with empty string
result = strings.ReplaceAll(result, "{yield}", "")
-
+
return result
}
@@ -375,23 +375,23 @@ func (t *Template) processBlocks(content string, opts *RenderOptions) string {
if opts.Blocks == nil {
opts.Blocks = make(map[string]string)
}
-
+
result := content
-
+
for {
start := strings.Index(result, "{block ")
if start == -1 {
break
}
-
+
nameEnd := strings.Index(result[start:], "}")
if nameEnd == -1 {
break
}
nameEnd += start
-
+
blockName := strings.Trim(result[start+7:nameEnd], "\" ")
-
+
contentStart := nameEnd + 1
endTag := "{/block}"
contentEnd := strings.Index(result[contentStart:], endTag)
@@ -399,13 +399,38 @@ func (t *Template) processBlocks(content string, opts *RenderOptions) string {
break
}
contentEnd += contentStart
-
+
blockContent := result[contentStart:contentEnd]
opts.Blocks[blockName] = blockContent
-
+
// Remove the block definition from the template
result = result[:start] + result[contentEnd+len(endTag):]
}
-
+
return result
}
+
+// RenderToContext is a simplified helper that renders a template and writes it to the request context
+// with error handling. Returns true if successful, false if an error occurred (error is written to response).
+func RenderToContext(ctx *fasthttp.RequestCtx, templateName string, data map[string]any) bool {
+ tmpl, err := Cache.Load(templateName)
+ if err != nil {
+ ctx.SetStatusCode(fasthttp.StatusInternalServerError)
+ fmt.Fprintf(ctx, "Template error: %v", err)
+ return false
+ }
+
+ tmpl.WriteTo(ctx, data)
+ return true
+}
+
+// RenderNamed is a simplified helper that loads and renders a template with the given data,
+// returning the rendered content or an error.
+func RenderNamed(templateName string, data map[string]any) (string, error) {
+ tmpl, err := Cache.Load(templateName)
+ if err != nil {
+ return "", fmt.Errorf("failed to load template %s: %w", templateName, err)
+ }
+
+ return tmpl.RenderNamed(data), nil
+}