Key part is this wrapper:

Path Model

In Doors, page routing starts from a struct.

That struct is your path model. Doors uses it to:

  • match incoming page URLs
  • decode path and query values
  • give your page a typed route value
  • build URLs again for links, redirects, and navigation

This is what makes routing in Doors feel like part of your app state instead of a separate string-based system.

UseModel

Register a page model with doors.UseModel:

type Path struct {
	Home bool `path:"/"`
	Post bool `path:"/posts/:ID"`
	ID   int
}

doors.UseModel(router, func(r doors.RequestModel, s doors.Source[Path]) doors.Response {
	return doors.ResponseComp(App{path: s})
})

When a request matches this model, Doors decodes the URL into Path and passes it to your handler as doors.Source[Path].

That source becomes the route state for the current page instance. If the user navigates within the same model type, the page can react to the updated model instead of reloading.

Inside the component body, subscribe to the source with doors.Sub(...) and render whichever view matches the current route:

type App struct {
	path doors.Source[Path]
}

elem (a App) Main() {
	<!doctype html>
	<html lang="en">
		<head>
			<meta charset="utf-8">
			<meta name="viewport" content="width=device-width, initial-scale=1">
			<title>Hello Doors!</title>
		</head>
		<body>
			~(doors.Sub(a.path, func(p Path) gox.Elem {
				if p.Post {
					return Post(p.ID)
				}
				return Home()
			}))
		</body>
	</html>
}

This keeps the page reactive: when the path model changes, the subscribed part of the page updates automatically.

Usually you do not use a Source directly for subscriptions. Instead, derive a smaller piece of state from it. For more advanced patterns, see State and Navigation.

Access the Path in the Handler

Inside the UseModel handler, read the current model with s.Get():

doors.UseModel(router, func(r doors.RequestModel, s doors.Source[Path]) doors.Response {
	path := s.Get()
	if path.Legacy {
		return doors.ResponseRedirect(Path{Home: true}, http.StatusMovedPermanently)
	}
	return doors.ResponseComp(Page(s))
})

Use Get() here because this handler does not run with a Doors render/runtime ctx. The Read(ctx)-style methods are for places that do have that runtime context.

Location

doors.Location is the special catch-all model.

If you register doors.Source[doors.Location], that handler matches every URL:

doors.UseModel(router, func(r doors.RequestModel, s doors.Source[doors.Location]) doors.Response {
	return doors.ResponseComp(Page(s))
})

This is useful when you want the raw path and query instead of a decoded struct model.

For example, a request like /any/deep/path?tag=hello&page=7 arrives as doors.Location with:

  • Path() as /any/deep/path
  • Query.Get("tag") as hello
  • Query.Get("page") as 7

Variants

A path model can contain multiple page variants. Each variant is an exported bool field tagged with path:"...".

type Path struct {
	Home  bool `path:"/"`
	Docs  bool `path:"/docs"`
	Guide bool `path:"/guide"`
}

When Doors decodes a URL, the matched variant field becomes true.

When Doors encodes a URL from a model value, it uses the variant whose marker field is true.

Leading and trailing slashes are normalized, so "/docs", "docs", and "/docs/" describe the same route pattern.

Params

Use :FieldName to capture a path segment into a struct field with the same name.

type Path struct {
	Post bool `path:"/posts/:ID"`
	ID   int
}

Supported single-segment field types are:

  • string
  • int, int64
  • uint, uint64
  • float64

Optional

Add ? to make the last captured segment optional.

Optional single-segment captures must use pointer fields:

type Path struct {
	Catalog bool `path:"/catalog/:ID?"`
	ID      *int
}

This matches both /catalog and /catalog/42.

Tail

Use + on the last parameter to capture the remaining path into []string.

type Path struct {
	Docs bool `path:"/docs/:Rest+"`
	Rest []string
}

This matches /docs/guide/setup and decodes Rest as []string{"guide", "setup"}.

Use * or +? to make that trailing capture optional:

type Path struct {
	Docs bool `path:"/docs/:Rest*"`
	Rest []string
}

This matches both /docs and /docs/guide/setup.

Rules to keep in mind:

  • optional captures must be the last segment
  • multi-segment captures must be the last segment
  • + and * require a []string field
  • required single-segment captures must use non-pointer fields

Query

Use query:"name" tags for query-string values.

type Path struct {
	Catalog bool     `path:"/catalog"`
	Color   []string `query:"color"`
	Page    *int     `query:"page"`
}

Examples:

  • /catalog?color=black&color=yellow
  • /catalog?page=2

Query fields do not decide which path variant matches. They are decoded after a path variant has matched.

Only fields tagged with query are encoded back into generated URLs.

For the exact query encoding and decoding rules, Doors uses go-playground/form v4 with the query tag in explicit mode.

Response

Your doors.UseModel handler returns a doors.Response, which lets you decide what should happen for the matched model.

doors.UseModel(router, func(r doors.RequestModel, s doors.Source[Path]) doors.Response {
	path := s.Get()

	if path.Legacy {
		return doors.ResponseRedirect(Path{Home: true}, http.StatusMovedPermanently)
	}

	if path.Dashboard && !authorized(r) {
		return doors.ResponseReroute(Path{Login: true})
	}

	return doors.ResponseComp(Page(s))
})

The common response helpers are:

  • doors.ResponseComp(comp) to render a page
  • doors.ResponseRedirect(model, status) to send an HTTP redirect
  • doors.ResponseReroute(model) to internally hand off to another registered model without an HTTP redirect

Request

doors.RequestModel gives you request/session access while deciding the response:

  • SessionStore()
  • RequestHeader()
  • ResponseHeader()
  • SetCookie(...)
  • GetCookie(...)

This is the right place for auth checks, redirects, response headers, and similar gatekeeping before the page is rendered.

It is also the usual place to initialize shared session state from cookies or headers. For that pattern, see Storage & Auth.

URLs

Usually you don't use direct URLs. Please refer to Navigation.

Use doors.NewLocation(ctx, model) when you need a URL from a registered model value.

loc, err := doors.NewLocation(ctx, Path{
	Catalog: true,
	Color:   []string{"black", "yellow"},
})
if err != nil {
	panic(err)
}

href := loc.String() // /catalog?color=black&color=yellow

doors.Location gives you:

  • Path() for the path only
  • String() for path plus query string

NewLocation works for model types that were registered with doors.UseModel.