| name | hlive-project |
| description | Structure a real multi-page HLive application in Go — the main.go entry point and net/http routing, a page/ package of page factories, reusable component functions, static asset serving, and build/run setup. Use when starting a new HLive project, adding a page or route, organizing components across files, or deciding where state and assets live (beyond the single-file _example/ demos). |
Structuring an HLive project
HLive's _example/ demos are single files to keep them readable. A real app
spreads across packages. This is the recommended layout (modeled on the
hlive_docs app), using hhtml for markup — see the hlive-hhtml and
hlive-core skills for the building blocks.
Layout
myapp/
├── main.go # entry point: routing + static assets
├── go.mod # module github.com/you/myapp
├── justfile # build/run recipes (optional)
├── page/ # one file per page; package "page"
│ ├── home.go # func Home() *l.Page
│ ├── about.go # func About() *l.Page
│ ├── components.go # shared unexported components: header(), footer()
│ └── demo/ # sub-package for a group of related routes
│ └── clicker.go # func Clickers() http.Handler
├── src/ # plain Go helper packages (no HLive), unit-testable
│ └── .../
└── assets/ # css/js/images served at /assets/
└── main.css
Keep it flat — most logic lives in page/. Add a sub-package under page/ only
to group a feature's routes.
Entry point — main.go
Use the standard library net/http; HLive's PageServer is just an
http.Handler. Register each page factory, then serve static files.
package main
import (
"log"
"net/http"
l "github.com/SamHennessy/hlive"
"github.com/you/myapp/page"
"github.com/you/myapp/page/demo"
)
func main() {
http.Handle("/", l.NewPageServer(page.Home))
http.Handle("/about", l.NewPageServer(page.About))
http.Handle("/demos/clickers", demo.Clickers())
fs := http.FileServer(http.Dir("./assets"))
http.Handle("/assets/", http.StripPrefix("/assets/", fs))
log.Println("listening on :3000")
if err := http.ListenAndServe(":3000", nil); err != nil {
log.Fatal(err)
}
}
l.NewPageServer takes a page factory func() *l.Page. For routes that need
extra wiring (their own handler, query params), expose a func() http.Handler
instead (as demo.Clickers() does).
Pages — the page package
One exported, PascalCase factory per page, each returning *l.Page. Build
the <head> (title + stylesheets) and <body> with hhtml, composing shared
components:
package page
import (
l "github.com/SamHennessy/hlive"
. "github.com/SamHennessy/hlive/hhtml"
)
func Home() *l.Page {
page := l.NewPage()
page.DOM().Title().Add("MyApp — Home")
page.DOM().Head().Add(Link(Rel("stylesheet"), Href("/assets/main.css")))
page.DOM().Body().Add(
Div(Class("container"),
header(),
Main(Class("main"),
H1("Welcome"),
P("Built with HLive."),
),
),
)
return page
}
There is no central layout wrapper; the per-page head/shell boilerplate is
repeated (or factored into a small helper you call from each page). One *l.Page
instance serves one connected user.
Reusable components
Shared UI is just unexported, camelCase functions returning l.Adder
(the interface hhtml builders return), kept in page/components.go:
func header() l.Adder {
return Header(Class("header"),
Div(Class("logo"), "MyApp"),
Nav(
Ul(Class("nav-links"),
Li(A(Href("/"), "Home")),
Li(A(Href("/about"), "About")),
),
),
)
}
Call them inside page factories: Body().Add(Div(header(), ...)).
Where state lives
- Page-local reactive values:
l.Box(...) / l.NewLockBox[T](...), captured in
the page factory's closure and updated from l.On handlers.
- Feature state that spans components or goroutines (live demos, real-time
feeds): a struct holding the boxes plus methods, often with
hlivekit.PubSub
for fan-out. See the hlivekit skill and hlive_docs/page/demo/clicker.go.
Keep non-UI logic (parsing, calculations, data access) in plain Go packages
under src/ so it's unit-testable without a browser.
Static assets
Put CSS/JS/images under assets/ and serve them as shown in main.go. Link
them in each page's <head> with Link(Rel("stylesheet"), Href("/assets/...")).
Build & run
A justfile keeps commands handy (or use go directly):
build:
go build -o ./deploy/server .
run:
go run .
go run . from the module root is enough for local development; the server
serves ./assets relative to its working directory.
Reference
hlive_docs/main.go, hlive_docs/page/home.go, hlive_docs/page/components.go,
hlive_docs/page/demo/clicker.go, hlive_docs/justfile. (hlive_docs is a
work-in-progress and is mid-migration to hhtml — follow the structure, and prefer
hhtml for new markup.) For testing pages from a page package, see the
hlive-testing skill.