The First Page
Let’s build a dynamic, reactive dashboard app — written entirely in Go. Along the way, you’ll see how each core piece of the framework fits together: from live HTML updates and event handling to state management, concurrency control, and navigation.

For tutorial purposes, I deliberately omitted error handling to reduce LOC; that’s not how it should be done!
1. SSL Certs (optional, but recommended)
Framework is optimized for HTTP/2/3. Without SSL, the browser limits the number of simultaneous requests to 6, which can cause issues in some rare, highly interactive and heavy-sync scenarios.
6 requests are not enough?
Each event goes via an individual HTTP request (it has benefits, e.g., native form data support). With some long-running processing and no concurrency control enabled, it’s easy to hit the limit.
What about overhead?
The HTTP/2/3 multiplexing and header compression keep the cost of additional requests low; we are cool.
Cook self-signed SSL certs:
# install package
$ go install filippo.io/mkcert@latest
# makes generated certs trustable by the system (removes browser warning for you), optional
$ mkcert -install
# create certs in the current folder
$ mkcert localhost 127.0.0.1 ::1
...
The certificate is at "./localhost+2.pem" and the key at "./localhost+2-key.pem"
In a production environment behind a reverse proxy, there is no need for SSL on a Go app itself.
2. General Page Template
./page_template.templ
This app has one page with multiple path variants, so a separate template isn’t needed.
Still, it’s nice to have concerns separated.
Page Interface
The page must provide head and body content to the template:
// all our pages must be of this shape
type Page interface {
// method returns component for head insertion
Head() templ.Component
// method returns component for body insertion
Body() templ.Component
}
Template
// template that takes `Page` as arg
templ Template(p Page) {
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="color-scheme" content="light dark"/>
// IMPORTANT: include frameworks' assets (~10KB)
@doors.Include()
// Generates <link rel="stylesheet" ... > while collecting info for CSP
@doors.ImportStyleExternal{
Href: "https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css",
}
// page title, meta-data, etc
@p.Head()
</head>
<body>
<main class="container">
// page content
@p.Body()
</main>
</body>
</html>
}
Two notes:
We include the framework’s assets; that’s crucial.
Instead of just
<link rel="stylesheet" href="...">, we useddoors.ImportExternalStyle, which also collects information for. CSP header generation. CSP is disabled by default, but this prepares us for it.
doors.Import...handles local, embedded, and external CSS and JS assets. For JavaScript/TypeScript modules, it eenables build/bundle steps and generates an import map
3. App and Path
./app.templ
App Path
In doors, the URI is decoded into a Path Model. It supports path variants, parameters, and query values.
Our path will have two variants:
/location selector/:Iddashboard for selected location
One parameter:
- Id of the city
And two query values:
forecast days
units (metric/imperial)
We’ll omit query values for now and add them later.
Our path model:
type Path struct {
Selector bool `path:"/"` // the first variant
Dashboard bool `path:"/:Id"` // the second variant
Id int // path parameter with City Id
}
The framework uses
pathtags to match the request path against the provided pattern.The matched variant’s field is set to true.
App Component
The path structure is wrapped in the state primitive (Beam) and passed to the app render function:
type app struct{}
// app render function, follows doors.App interface
func (a *app) Render(path doors.SourceBeam[Path]) templ.Component {
return Template(a)
}
// head component for our template
templ (a *app) Head() {
<title>Dashboard App</title>
}
// body component for our template
templ (a *app) Body() {
<h1>Hello <i>doors</i>!</h1>
}
To be compatible with the framework, the app type must implement Render(), return a component, and accept a Beam with the path model.
Model Handler
A function that runs when the path matches. It reads request data (doors.RModel) and chooses the response (doors.ModelRouter).
In our case, it’s straightforward:
func Handler(m doors.doors.ModelRouter[Path], r doors.RModel[Path]) doors.ModelRoute {
// just serve the app instance, no checks
return m.App(&app{})
}
doors.AppRouteralso supports soft (internal) and hard (HTTP) redirects and serving static pages.
4. Router
./main.go
Create a doors router, provide the app handler, and launch the server.
package main
import (
"github.com/doors-dev/doors"
"net/http"
)
func main() {
// create doors router
dr := doors.NewRouter()
dr.Use(
// attach app handler function
doors.UseModel(Handler),
)
// start standard Go server with our self signed cert
// and router
err := http.ListenAndServeTLS(":8443", "localhost+2.pem", "localhost+2-key.pem", dr)
if err != nil {
panic(err)
}
}
Notice how
doors.Routerjust plugs into the Go standard server! Go is awesome.
5. Launch
Build & launch
templ generate && go run .

Next: Live Reloading
Code
./page_template.templ
package main
import "github.com/doors-dev/doors"
// all our pages must be of this shape
type Page interface {
// method returns component for head insertion
Head() templ.Component
// method returns component for body insertion
Body() templ.Component
}
// template that takes `Page` as arg
templ Template(p Page) {
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="color-scheme" content="light dark"/>
// IMPORTANT: include frameworks' assets (~10KB)
@doors.Include()
// Generates <link rel="stylesheet" ... > while collecting info for CSP
@doors.ImportStyleExternal{
Href: "https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css",
}
// page title, meta-data, etc
@p.Head()
</head>
<body>
<main class="container">
// page content
@p.Body()
</main>
</body>
</html>
}
app.templ
package main
import "github.com/doors-dev/doors"
type Path struct {
Selector bool `path:"/"` // the first variant
Dashboard bool `path:"/:Id"` // the second variant
Id int // path parameter with City Id
}
func Handler(m doors.ModelRouter[Path], r doors.RModel[Path]) doors.ModelRoute {
// just serve the app instance, no checks
return m.App(&app{})
}
type app struct{}
// app render function, follows doors.App interface
func (a *app) Render(path doors.SourceBeam[Path]) templ.Component {
return Template(a)
}
// head component for our template
templ (a *app) Head() {
<title>Dashboard App</title>
}
// body component for our template
templ (a *app) Body() {
<h1>Hello <i>doors</i>!</h1>
}
./main.go
package main
import (
"github.com/doors-dev/doors"
"net/http"
)
func main() {
// create doors router
dr := doors.NewRouter()
dr.Use(
// attach app handler function
doors.UseModel(Handler),
)
// start standard Go server with our self signed cert
// and router
err := http.ListenAndServeTLS(":8443", "localhost+2.pem", "localhost+2-key.pem", dr)
if err != nil {
panic(err)
}
}