Doors Starter
May 23, 2026 · View on GitHub
A starter project for Doors — a reactive Go web framework built on GoX.
Project Structure
.
├── main.go # Entry point: creates the Doors app, attaches middleware, starts HTTP server
├── app.gox # Root component: HTML shell with head elements, nav, router, and footer
├── go.mod / go.sum # Go module definition and dependency checksums
│
├── path/ # URL path models — Go structs describing URL shapes
│ └── root.go # Root path model: "/" and "/about" variants
│
├── components/ # Reusable UI components shared across pages
│ ├── nav.gox # Top navigation bar with doors.ALink navigation links
│ ├── footer.gox # Page footer
│ └── not_found.gox # 404 fallback page with reactive path display
│
├── segments/ # Self-contained page groups (one per path model)
│ └── root/ # Segment for the "/" section
│ ├── main.gox # Routes the Root path model to Landing or About
│ ├── landing.gox # Landing page ("/") — includes the counter demo
│ ├── about.gox # About page ("/about")
│ └── counter.gox # Interactive counter (Source + Bind demo)
│
└── assets/ # Embedded static files
├── embed.go # Go embed directives exposing Style bytes and Static FS
├── style.css # Application stylesheet
└── static/ # Raw static files served via UseFS middleware
└── ico.png # Favicon
Generated
.x.gofiles are auto-managed by GoX and omitted from this tree.
The
segments/layout above is a suggested convention, not a strict rule. For larger sections you may want to split further — e.g. separate Go packages per page, or sharedcomponents/anddrivers/packages within a segment. Keep the codebase systematically organized in whatever way fits your project.
How to Extend
Add a New Page to an Existing Path Model
If you want a new page under the existing Root path model (e.g. /contact):
-
Add the variant to
path/root.go:const ( RootLanding RootPage = iota RootAbout RootContact // new )Update the struct tag:
`path:"/ | /about | /contact"` -
Create a new page component in
segments/root/:elem Contact(p doors.Source[path.Root]) { <title>Contact</title> <h1>Contact</h1> } -
Add a new route match in
segments/root/main.gox:doors.RouteMatch(func(p path.Root) bool { return p.Page == path.RootContact }).Source(Contact), -
Link to it in
components/nav.gox:<li>~topNavLink{model: path.Root{Page: path.RootContact}, text: "Contact"}</li>
Add a New Path Model (New Section)
To add an entirely new section like /blog, /blog/:id:
-
Create
path/blog.go:package path type Blog struct { Section BlogSection `path:"/blog | /blog/:ID"` ID string } type BlogSection int const ( BlogIndex BlogSection = iota BlogPost ) -
Create
segments/blog/main.gox:package blog import ( "github.com/doors-dev/doors" "github.com/doors-dev/doors-starter/path" ) elem Main(p doors.Source[path.Blog]) { ~(p.Route( doors.RouteMatch(func(p path.Blog) bool { return p.Section == path.BlogIndex }).Source(Index), doors.RouteDerive(func(p path.Blog) (string, bool) { return p.ID, p.Section == path.BlogPost }).Beam(Post), )) }RouteDerivematches whenSection == BlogPostand derives the ID into aBeam[string]. ThePostcomponent receives just the ID instead of the full Blog source, keeping updates narrower. -
Create the page components:
// segments/blog/index.gox elem Index(p doors.Source[path.Blog]) { <h1>Blog Index</h1> } // segments/blog/post.gox — receives Beam[string] (the ID) elem Post(id doors.Beam[string]) { <h1>Post: ~(id.Bind(elem(v string) { ~(v) }))</h1> } -
Add the model to the router in
app.gox:<> ~(doors.Route( doors.RouteModel(root.Main), doors.RouteModel(blog.Main), // new — tried in order doors.RouteLocationDefault(components.NotFound), )) </>
Add a New Static Asset
- For a cacheable file (favicon, images): place it in
assets/static/— it's served viaUseFSunder/static/. - For a managed resource (stylesheet, JS): embed it as
[]byteinassets/embed.goand use it inline:<script src=(assets.MyScript)></script>
Use Tailwind CSS
Create a style.css at the project root:
@import "tailwindcss";
Build it to assets/style.css (which is embedded by assets/embed.go):
npx @tailwindcss/cli -i ./style.css -o ./assets/style.css
See Tailwind CLI installation for setup instructions. If using npm, add node_modules/ to .gitignore. A standalone binary is also available (slower but no npm needed).
Inline Scripts and Styles
It is perfectly fine to use inline <script> and <style> tags directly. Doors converts them into loadable, cacheable resources at render time. Inline scripts are wrapped in an anonymous async function with access to $data, $hook, $fetch, $on, and $sys. See the JavaScript docs for details.
GoX Workflow
- Write GoX source in
.goxfiles — this is where templates and components live. - Use
.gofiles for plain Go logic (path models, embed directives, helpers). .x.gofiles are auto-generated — the GoX language server keeps them up to date while you edit.goxfiles. Do not edit them manually.- Run
gox fmtthengox gen(fmt first, gen second) to format and regenerate from the command line.
Running the Project
go run .
Open http://localhost:8080.
Safari on localhost: Doors uses a
Securesession cookie by default, which Safari rejects on plain HTTP. Usedoors.WithConf(doors.Conf{ServerSessionCookieNoSecure: true})or set up local HTTPS.
Local HTTPS: Use mkcert for trusted self-signed certs —
mkcert -install && mkcert localhost 127.0.0.1 ::1.
File Watching
Doors reloads the page automatically when it detects the server has restarted. For file watching during development, install wgo and run:
wgo -file=.go -file=.css -file=.js -file=.ts go run .
This restarts the server on file changes and the browser reloads automatically.
Less useful in agentic development — the agent typically restarts the server explicitly after changes.