The first path up the mountain · v1.0.0
Walk the path: build a component, page, and site.
Begin with visible HTML. Add typed data, nest small templates into a complete document, and render it from an ordinary net/http handler. At the summit, the browser receives a dynamic page and the production program depends only on Go and the small sando runtime.
You will make this small application:
hello-hime/
├── cmd/site/main.go
├── internal/views/
│ ├── badge.sando
│ ├── home.sando
│ ├── layout.sando
│ └── views.go
├── go.mod
└── go.sum
Live proof
Compiled before deployment. Rendered for this request.
This tutorial's .sando source was compiled into ordinary Go before the site was deployed. The running Go application has just called sando.Render with fresh typed data to build this response.
This panel is itself a typed .sando component nested into the tutorial with <?~ … ?>.
- Rendered at
- Cache policy
Cache-Control: no-store- Measured render
Server-Timing: sandoresponse header
Refresh this page and the render time changes. The timing header measures this response's buffered component render; it is evidence, not a universal benchmark.
Step 1
Prepare the v1.0.0 runtime and compiler
You need Go 1.25 or newer. On macOS, Linux, or another POSIX shell, create the application, add the small runtime first, and then install the development compiler:
mkdir hello-hime
cd hello-hime
go mod init example.com/hello-hime
go get gamertan.com/sandwich-hime/sando@v1.0.0
go install gamertan.com/sandwich-hime/cmd/himesan@v1.0.0
mkdir -p cmd/site internal/views
Make sure Go's install directory—normally $(go env GOPATH)/bin—is on your PATH. If you prefer not to edit PATH, replace each himesan command below with go run gamertan.com/sandwich-hime/cmd/himesan@v1.0.0.
No clone, sibling checkout, or go.work replacement is needed. Both engine modules declare zero third-party Go module requirements. The runtime tag and compiler tag are signed and were resolved through the public Go proxy and checksum database before this lesson was published.
If a compiler-first shared cache briefly says the parent module does not contain sando, run go mod download gamertan.com/sandwich-hime/sando@v1.0.0 and then repeat the exact go get above. Do not delete your global module cache.
Step 2
Define the data and the composition seam
Create internal/views/views.go. These ordinary Go types are the entire data contract between the handler and the templates:
package views
import "gamertan.com/sandwich-hime/sando"
type Link struct {
Label string
URL string
}
type HomeView struct {
Visitor string
Links []Link
}
type LayoutView struct {
Title string
Body sando.Component
}
HomeView carries ordinary page data. LayoutView.Body accepts any renderable component, so the outer document can wrap a home page today and another typed page tomorrow. There is no template-name registry or implicit request global.
Step 3
Build one small component
Create internal/views/badge.sando:
<?sando go
package views
func Badge(label string)
?>
<span class="badge"><?= label ?></span>
The header declares one typed template constructor. The body is direct HTML. <?= label ?> writes untrusted data using the escaping rule for HTML text. EOF closes the template.
Generation will create badge.sando.go beside it with an ordinary API shaped like:
func Badge(label string) sando.Component
Do not hand-edit the generated neighbor. Commit it after generation; Hime-san owns subsequent deterministic replacements of that marked file.
Step 4
Compose the component into a page
Create internal/views/home.sando. The Go islands are deliberately small: one nested component call and one loop.
<?sando go
package views
func Home(view HomeView)
?>
<main id="main">
<h1>Hello, <?= view.Visitor ?>.</h1>
<p><?~ Badge("rendered on request") ?></p>
<h2>Choose a trail</h2>
<ul>
<? for _, link := range view.Links { ?>
<li><a href="<?= link.URL ?>"><?= link.Label ?></a></li>
<? } ?>
</ul>
</main>
<?~ … ?> nests a component only at an HTML content boundary and propagates its render error. The loop is ordinary trusted Go source. Each label is escaped as text; each URL is checked and written using the recognized URL-attribute rules.
Now create internal/views/layout.sando for the complete document:
<?sando go
package views
func Layout(view LayoutView)
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= view.Title ?></title>
</head>
<body>
<a href="#main">Skip to content</a>
<?~ view.Body ?>
</body>
</html>
A page and a small partial use the same component contract. Nesting does not require an inheritance language: Layout(Home(...)) is typed Go composition.
Step 5
Render it from an ordinary Go server
Create cmd/site/main.go:
package main
import (
"bytes"
"log"
"net/http"
"strings"
"example.com/hello-hime/internal/views"
"gamertan.com/sandwich-hime/sando"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /{$}", home)
log.Println("listening on http://127.0.0.1:8080")
log.Fatal(http.ListenAndServe("127.0.0.1:8080", mux))
}
func home(w http.ResponseWriter, r *http.Request) {
visitor := strings.TrimSpace(r.URL.Query().Get("name"))
if visitor == "" {
visitor = "traveler"
}
body := views.Home(views.HomeView{
Visitor: visitor,
Links: []views.Link{
{Label: "Start", URL: "/"},
{Label: "Sandwich Hime", URL: "https://sandwichhime.com/"},
},
})
page := views.Layout(views.LayoutView{
Title: "Hello from the mountain",
Body: body,
})
var output bytes.Buffer
if err := sando.Render(r.Context(), &output, page); err != nil {
log.Printf("render home: %v", err)
http.Error(w, "could not render page", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(output.Bytes())
}
The router, status, headers, cache policy, logging, buffering, and listener remain application decisions. Sandwich Hime supplies components and rendering. Buffering means a render error can still become a clean 500 before response bytes are committed.
The ?name= query makes the response genuinely dynamic. It is intentionally treated as untrusted data and escaped by the generated component on every request.
himesan generate runs during development or CI when template source changes; it does not run on a request. The compiled application calls sando.Render on each request with that request's typed data. Nested <?~ … ?> composition has already become ordinary generated Go by then.This first path keeps the server deliberately small. The application integration tutorial is waiting when you want to understand why the exact-root route matters, where middleware fits, how a shared render helper protects status handling, and how a second page joins the application.
Step 6
Generate, check, test, and build
Run the compiler against the template directory, then use the normal Go toolchain:
himesan version
himesan generate internal/views
himesan check internal/views
GOWORK=off go mod tidy
GOWORK=off go test ./...
GOWORK=off go build ./cmd/site
GOWORK=off go run ./cmd/site
himesan version should begin with himesan v1.0.0.
Open http://127.0.0.1:8080/?name=Hime-san. Then try an adversarial value such as ?name=<script>alert(1)</script>: those characters appear as text, not executable markup.
generate writes the adjacent .sando.go files. check writes nothing and exits nonzero when a template is invalid or generated output is missing or stale. The later go test and go build steps type-check the Go names and expressions in the generated package.
Optional tooling
Let the same compiler help while you work
Hime-san includes an editor-neutral language server. The VS Code preview adds highlighting, snippets, live diagnostics, hover, symbols, component completion, and go-to-definition without generating on save or downloading a compiler.
If a compatible coding agent is helping with the project, install the portable 0BSD Agent Skill. It teaches the pinned runtime-first workflow, keeps generated files under Hime-san's ownership, and treats trusted values and handwritten components as explicit security capabilities.
Both tools are optional. The portable contract remains himesan generate, himesan check, and the ordinary Go toolchain.
Step 7
Turn the security contract into a test
Create internal/views/views_test.go:
package views
import (
"context"
"strings"
"testing"
"gamertan.com/sandwich-hime/sando"
)
func TestHomeEscapesUntrustedText(t *testing.T) {
var output strings.Builder
page := Home(HomeView{Visitor: `<script>alert("no")</script>`})
if err := sando.Render(context.Background(), &output, page); err != nil {
t.Fatal(err)
}
if strings.Contains(output.String(), "<script>") {
t.Fatalf("visitor became markup: %s", output.String())
}
}
func TestHomeRejectsDangerousURL(t *testing.T) {
var output strings.Builder
page := Home(HomeView{Links: []Link{
{Label: "unsafe", URL: "javascript:alert(1)"},
}})
if err := sando.Render(context.Background(), &output, page); err == nil {
t.Fatal("dangerous URL rendered without an error")
}
}
Run go test ./... again. Templates and embedded Go are trusted source code; values are untrusted. Sandwich Hime is a contextual compiler, not a sandbox. Unsupported or ambiguous contexts fail generation, dangerous ordinary URL schemes fail rendering, and explicit Trust* values are capabilities that deserve conspicuous review.
Continue with the full security model before accepting user-controlled content in a real application.
Step 8
Know what development uses—and what production ships
*.sando- Human-authored template source. Keep it.
*.sando.go- Deterministic, reviewable generated Go. Commit it.
himesan- The AGPL-3.0-only development compiler and optional local supervisor. It does not enter the production dependency graph.
sando- The small Apache-2.0 runtime used by generated components and the production application.
- Your application
- The router, middleware, HTTP policy, data access, tests, process, and deployment remain yours.
You can inspect the boundary directly:
go list -deps ./cmd/site | grep '^gamertan.com/sandwich-hime'
For this application, the result should contain gamertan.com/sandwich-hime/sando and not the compiler module root or cmd/himesan. The generated Go lets production use an ordinary go build without executing the generator or project templates.
The maintained targets are Linux/amd64 and Apple Silicon macOS/arm64. The RC.1 baseline was verified natively with pinned Go 1.26.7 and Go 1.27.0; the release evidence ledger records each exact release. Independent security audits, certification, and broader platform promises are not implied by v1.0.0.
You have walked the complete path: typed data → visible templates → nested components → generated Go → an ordinary dynamic server. From here, add another page constructor and route, extract another partial when repetition earns it, and keep the HTML in sight.
Want the finished trail beside the lesson? The 0BSD tutorial starter is a runnable companion with the same boundaries and a slightly richer dynamic trail model. Use it, change it, or don't—I'm just glad you're here.