Skip to content
Trilha
Chapters

Reference

File conventions

Complete table of what each file and folder name in app/ means.

Files#

FileExported functionSignatureScope
page.goPagefunc(c *trilha.Ctx) (h.Node, error)the folder's GET route
page.goPOST, PUT, PATCH, DELETE (optional)func(c *trilha.Ctx) errorforms; CSRF required
route.goGET, POST, PUT, PATCH, DELETE, OPTIONS (at least one)func(c *trilha.Ctx) errorthe folder's JSON API
kind.go, or any file (optional)Kindvar Kind = trilha.KindPage or KindAPIsubtree: how errors are rendered and whether CSRF applies (see Errors)
route.go (optional)CORSvar CORS = trilha.CORS{...}cross-origin policy of this route alone, preflight included
layout.goLayoutfunc(c *trilha.Ctx, children h.Node) (h.Node, error)subtree
middleware.goMiddlewarefunc(c *trilha.Ctx, next trilha.Next) errorsubtree
middleware.go (optional)MiddlewareGET, MiddlewarePOST, MiddlewarePUT, MiddlewarePATCH, MiddlewareDELETE, MiddlewareOPTIONSfunc(c *trilha.Ctx, next trilha.Next) errorsubtree, that method only
not_found.go (root only)NotFoundfunc(c *trilha.Ctx) (h.Node, error)the app's 404
error.go (root only)Errorfunc(c *trilha.Ctx, err error) (h.Node, error)every error status but 404
setup.go (root only)Setupfunc(a *trilha.App) errorbefore serving
setup.go (optional)Configfunc(cfg *trilha.Config) or func(cfg *trilha.Config) errorbefore trilha.New; an error stops the boot
setup.go (optional)Shutdownfunc(a *trilha.App) errorafter the app stops accepting requests (close pool, queue, flush logs)

page.go and route.go in the same folder is an error. The function may live in any file of the package; the file name is what binds the convention.

Kind follows the subtree#

Kind is a variable, not a function, and it is inherited like Layout and Middleware: declared in the package of a folder, it decides that folder and everything below it, and the deepest declaration wins. kind.go is the file name for a folder that has no route.go of its own — a subtree root has to be able to speak without owning a route:

// app/painel/kind.go — this branch is browser pages, so its writes enforce CSRF
package painel

var Kind = trilha.KindPage

This matters more than error rendering: Kind is what turns CSRF on. A route.go is an API by default, and an API does not check the token, so the same form action moved from a page.go into a route.go starts accepting a POST from another site — silently. One line at the root of the branch covers every leaf, including the leaf someone adds next month. trilha audit reports a write route that no Kind reaches in an app that also serves pages.

A page.go route is a page whatever the branch above it says: an inherited KindAPI never turns a page into JSON.

Folders#

NameBecomesExample
eventsliteral segment/events
slug_parameter {slug}/events/{slug}c.Param("slug")
path__catch-all {path...}; must be a leaf/docs/{path...}
organizer-route group; not part of the URLlayout/middleware for the subtree
app.css, robots.txtfixed path with an extension (dot in the middle of the name)/app.css, /manifest.webmanifest, /sw.js
.well-knownthe one dot folder that is a route/.well-known/security.txt
_x, .x, testdataignored

A folder with a dot in its name serves a fixed path with an extension. Since app.css is not a Go identifier, declare another package name in the file (package appcss); the generator imports everything with an alias, so the package name does not matter.

Folders that start with a dot stay ignored, with a single exception: .well-known, where RFC 8414, RFC 9728, RFC 8555, RFC 9116 and OpenID Discovery publish their documents. Inside it the conventions are the usual ones — app/.well-known/security.txt/route.go answers /.well-known/security.txt. A page.go or route.go inside any other dot folder is now an E_HIDDEN_ROUTE error instead of a 404 nobody can explain; to park a folder out of the routing on purpose, start its name with _.

The Go tool does not match a path with a dot in ./..., so go vet ./... and go test ./... skip that package as a target. It still compiles: trilha_gen.go imports it by its explicit path.

Cross-origin on one route#

Config.CORS is the policy of the whole app. When only a few paths are public — the discovery documents under /.well-known/, fetched from another origin by a client that has no session yet — the route carries its own:

package oauthresource

// Only this route. The other routes stay same-origin.
var CORS = trilha.CORS{Origins: []string{"*"}, Methods: []string{"GET"}}

func GET(c *trilha.Ctx) error { ... }

The framework answers the preflight from the policy (204 with Access-Control-Allow-*, or 403 for an origin or method that is not on the list) and adds the headers to every response of that route. A route that declares its own policy decides alone: the app-wide list neither widens nor narrows it. Writing func OPTIONS in the same file takes the preflight back — the common case is declarative, the odd one is still yours.

HEAD is not a handler name: since Go 1.22 the router answers HEAD with the GET handler.

Precedence: literal beats parameter, which beats catch-all. Two sibling dynamic folders are an error. Two folders producing the same URL (through groups) are an error.

Other project folders#

FolderRole
public/static files served at the root; embedded in the binary in production
trilha_gen.gogenerated; committed; never edited by hand; carries the package the folder declares (see CLI)
.trilha/temporary binaries of dev and export; ignored by git

Execution order for GET /a/b#

middleware(app) → middleware(app/a) → middleware(app/a/b)
  → middlewareGET(app) → middlewareGET(app/a) → middlewareGET(app/a/b)
  → Page (or method)
  → layout(app/a/b) → layout(app/a) → layout(app)

The per-method chain runs inside the route-wide one: a rule for a single method refines what the route already decided. For POST it is MiddlewarePOST, and so on; a method with no chain of its own just runs the route's.

Generation errors#

CodeCause
E_PAGE_AND_ROUTEpage.go and route.go in the same folder
E_NO_PAGE_FUNCpage.go without Page
E_NO_METHODroute.go without an exported method
E_NO_LAYOUT_FUNC, E_NO_MIDDLEWARE_FUNC, E_NO_NOT_FOUND_FUNC, E_NO_ERROR_FUNC, E_NO_SETUP_FUNCfile without the expected function
E_AMBIGUOUS_SEGMENTtwo dynamic folders at the same level
E_CATCHALL_NOT_LEAFroutes below an x__ folder
E_BAD_SEGMENTinvalid parameter name or dynamic group (x_-)
E_DUPLICATE_ROUTEtwo folders producing the same URL
E_UNUSED_METHOD_MIDDLEWAREMiddlewareX that reaches no route serving X
E_PARSEGo file that does not compile
E_NO_APPthere is no app/ folder
E_HIDDEN_ROUTEpage.go or route.go inside a folder whose name starts with a dot
E_UNROUTABLE_METHODfunc HEAD, TRACE or CONNECT: the router does not take those from a file
E_CORS_ON_PAGEvar CORS in a page.go