diff --git a/cmd/muxt/testdata/err_path_method_collision.txt b/cmd/muxt/testdata/err_path_method_collision.txt
new file mode 100644
index 0000000..19dd600
--- /dev/null
+++ b/cmd/muxt/testdata/err_path_method_collision.txt
@@ -0,0 +1,35 @@
+! muxt generate --use-receiver-type=T
+
+stderr 'TemplateRoutePaths method name collision: handlers list and List both produce method List'
+
+-- template.gohtml --
+{{define "GET /items list(ctx)" }}
+
+{{end}}
+
+{{define "GET /items/{id} List(ctx, id)" }}
+
+{{end}}
+
+-- go.mod --
+module example.com
+
+go 1.22
+-- template.go --
+package server
+
+import (
+ "context"
+ "embed"
+ "html/template"
+)
+
+//go:embed *.gohtml
+var formHTML embed.FS
+
+var templates = template.Must(template.ParseFS(formHTML, "*"))
+
+type T struct{}
+
+func (T) list(_ context.Context) []string { return nil }
+func (T) List(_ context.Context, _ string) string { return "" }
diff --git a/cmd/muxt/testdata/err_path_method_unexportable.txt b/cmd/muxt/testdata/err_path_method_unexportable.txt
new file mode 100644
index 0000000..20a8af3
--- /dev/null
+++ b/cmd/muxt/testdata/err_path_method_unexportable.txt
@@ -0,0 +1,30 @@
+! muxt generate --use-receiver-type=T
+
+stderr 'cannot export identifier "_list" for TemplateRoutePaths method'
+
+-- template.gohtml --
+{{define "GET /items _list(ctx)" }}
+
+{{end}}
+
+-- go.mod --
+module example.com
+
+go 1.22
+-- template.go --
+package server
+
+import (
+ "context"
+ "embed"
+ "html/template"
+)
+
+//go:embed *.gohtml
+var formHTML embed.FS
+
+var templates = template.Must(template.ParseFS(formHTML, "*"))
+
+type T struct{}
+
+func (T) _list(_ context.Context) []string { return nil }
diff --git a/cmd/muxt/testdata/reference_path_unexported_method.txt b/cmd/muxt/testdata/reference_path_unexported_method.txt
new file mode 100644
index 0000000..f2480cb
--- /dev/null
+++ b/cmd/muxt/testdata/reference_path_unexported_method.txt
@@ -0,0 +1,61 @@
+muxt generate --use-receiver-type=T
+muxt check
+
+cat template_routes.go
+stdout 'func \(routePaths TemplateRoutePaths\) ListItems\(\) string'
+
+exec go test -cover
+
+-- template.gohtml --
+{{define "GET /items listItems(ctx)" }}
+ Items
+{{end}}
+
+-- go.mod --
+module example.com
+
+go 1.22
+-- template.go --
+package server
+
+import (
+ "context"
+ "embed"
+ "html/template"
+)
+
+//go:embed *.gohtml
+var formHTML embed.FS
+
+var templates = template.Must(template.ParseFS(formHTML, "*"))
+
+type T struct{}
+
+func (T) listItems(_ context.Context) string { return "items" }
+-- template_test.go --
+package server
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func Test(t *testing.T) {
+ mux := http.NewServeMux()
+
+ TemplateRoutes(mux, T{})
+
+ req := httptest.NewRequest(http.MethodGet, "/items", nil)
+ rec := httptest.NewRecorder()
+ mux.ServeHTTP(rec, req)
+ res := rec.Result()
+ if res.StatusCode != http.StatusOK {
+ t.Error("expected OK")
+ }
+
+ path := TemplateRoutePaths{}.ListItems()
+ if path != "/items" {
+ t.Errorf("TemplateRoutePaths{}.ListItems() = %q, want %q", path, "/items")
+ }
+}
diff --git a/docs/reference/known-issues.md b/docs/reference/known-issues.md
index 36a6950..e050d21 100644
--- a/docs/reference/known-issues.md
+++ b/docs/reference/known-issues.md
@@ -98,6 +98,21 @@ If functions are added after `ParseFS`, type checking won't recognize them.
**Workaround:** Pass data explicitly and ensure subtemplates don't assume specific types.
+## TemplateRoutePaths Method Name Collision
+
+**Issue:** `TemplateRoutePaths` methods are always exported so they can be called from templates. This has two consequences:
+
+1. **Collision:** If a receiver has both an exported and unexported handler method that differ only in the first letter's case (e.g., `List` and `list`), generation fails because both would produce the same `TemplateRoutePaths.List()` method.
+2. **Unexportable identifiers:** Handler methods starting with `_` (e.g., `_list`) cannot be exported, so generation fails.
+
+**Errors:**
+- `TemplateRoutePaths method name collision: handlers list and List both produce method List`
+- `cannot export identifier "_list" for TemplateRoutePaths method: first character '_' has no uppercase form`
+
+**Fix:** Rename the handler method to start with a letter.
+
+**Test files:** `err_path_method_collision.txt`, `err_path_method_unexportable.txt`
+
## Reporting Issues
Found a limitation not listed here? [Open an issue](https://github.com/typelate/muxt/issues/new) with:
diff --git a/docs/skills/muxt_debug-generation-errors/SKILL.md b/docs/skills/muxt_debug-generation-errors/SKILL.md
index 63b2637..385f427 100644
--- a/docs/skills/muxt_debug-generation-errors/SKILL.md
+++ b/docs/skills/muxt_debug-generation-errors/SKILL.md
@@ -232,6 +232,26 @@ type Article struct {
**Test files:** `err_check_with_unused_template.txt`, `err_check_with_dead_code_outside_define.txt`
+### Path Method Name Collision
+
+**Error:** `TemplateRoutePaths method name collision: handlers list and List both produce method List`
+
+**Cause:** Two handler methods differ only in the case of the first letter (e.g., `list` and `List`). Since `TemplateRoutePaths` methods are always exported for template access, both would produce the same method name.
+
+**Fix:** Rename one of the handler methods so they don't collide after capitalization.
+
+**Test file:** `err_path_method_collision.txt`
+
+### Unexportable Path Method Identifier
+
+**Error:** `cannot export identifier "_list" for TemplateRoutePaths method: first character '_' has no uppercase form`
+
+**Cause:** The handler method starts with a character that has no uppercase form (e.g., `_`). `TemplateRoutePaths` methods must be exported for template access.
+
+**Fix:** Rename the handler method to start with a letter.
+
+**Test file:** `err_path_method_unexportable.txt`
+
### CLI Errors
**Error:** `unknown command` or `unknown flag`
@@ -273,3 +293,5 @@ type Article struct {
| Unknown flag | `err_unknown_flag.txt` |
| Invalid identifier flag | `err_invalid_identifier_flag.txt` |
| Invalid output filename | `err_invalid_output_filename.txt` |
+| Path method name collision | `err_path_method_collision.txt` |
+| Unexportable path method | `err_path_method_unexportable.txt` |
diff --git a/internal/generate/template_route_path.go b/internal/generate/template_route_path.go
index 3ffac21..3753789 100644
--- a/internal/generate/template_route_path.go
+++ b/internal/generate/template_route_path.go
@@ -9,6 +9,8 @@ import (
"go/types"
"strconv"
"strings"
+ "unicode"
+ "unicode/utf8"
"github.com/typelate/muxt/internal/astgen"
"github.com/typelate/muxt/internal/muxt"
@@ -27,7 +29,16 @@ func routePathTypeAndMethods(imports *File, config RoutesFileConfiguration, defs
},
},
}
+ seen := make(map[string]string, len(defs))
for _, t := range defs {
+ exported, err := exportIdentifier(t.Identifier())
+ if err != nil {
+ return nil, err
+ }
+ if prev, ok := seen[exported]; ok {
+ return nil, fmt.Errorf("TemplateRoutePaths method name collision: handlers %s and %s both produce method %s", prev, t.Identifier(), exported)
+ }
+ seen[exported] = t.Identifier()
decl, err := routePathFunc(imports, config, &t)
if err != nil {
return nil, err
@@ -49,8 +60,13 @@ func routePathFunc(file *File, config RoutesFileConfiguration, def *muxt.Definit
textMarshalerUnderlying := textMarshalerType.Underlying()
textMarshalerInterface := textMarshalerUnderlying.(*types.Interface)
+ ident, err := exportIdentifier(def.Identifier())
+ if err != nil {
+ return nil, err
+ }
+
method := &ast.FuncDecl{
- Name: ast.NewIdent(def.Identifier()),
+ Name: ast.NewIdent(ident),
Recv: &ast.FieldList{
List: []*ast.Field{
{Names: []*ast.Ident{ast.NewIdent(methodReceiverName)}, Type: ast.NewIdent(config.TemplateRoutePathsTypeName)},
@@ -231,3 +247,12 @@ func routePathFunc(file *File, config RoutesFileConfiguration, def *muxt.Definit
return method, nil
}
+
+func exportIdentifier(s string) (string, error) {
+ r, size := utf8.DecodeRuneInString(s)
+ exported := string(utf8.AppendRune(nil, unicode.ToUpper(r))) + s[size:]
+ if !token.IsExported(exported) {
+ return "", fmt.Errorf("cannot export identifier %q for TemplateRoutePaths method: first character %q has no uppercase form", s, r)
+ }
+ return exported, nil
+}