Documentation
¶
Overview ¶
Package templates is a gallery of starter kits you can deploy as they come.
The Hanzo starter-kit gallery at /v1/templates, in TWO layers that never mix:
- the PUBLIC catalog: deployable app/site scaffolds (source of truth: hanzoai/gallery), vendored so the unified `cloud` binary ships it with no external dependency. Reference content — embedded, immutable, and with NO write route, so nothing a customer does can add to it.
- a customer's OWN templates: rows in {DataDir}/templates.db keyed by the gateway-minted org (principal.Org — never a request field), PRIVATE to that org. Only that org lists, reads, edits, deletes, or forks them.
Two layers rather than one visibility flag is the whole safety argument: a private template cannot surface in the public hanzo.app catalog by CONSTRUCTION — it lives in a different container, reached only by a query that binds org — not by a filter every future reader has to remember. An anonymous GET never touches the store at all.
A slug is single-valued across both layers: publishing over a public slug is 409, so a slug still names exactly one template and no org can shadow the gallery.
ONE template is ONE entry. The shapes it ships in — format, page, theme — are Variants inside that entry, chosen at fork time.
Surface — every route is a TYPED op (zip.Get[In, Out] and friends), so each is ONE registry entry the document, the MCP tool, the CLI command and the generated SDK method are all projected from:
GET /v1/templates public catalog + (validated caller) that org's own -> {data:[StarterKit]}
GET /v1/templates/:slug one kit: the caller org's own, else public -> StarterKit
POST /v1/templates publish a kit PRIVATE to the caller's org -> 201 StarterKit
PUT /v1/templates/:slug replace the caller org's own kit -> StarterKit
DELETE /v1/templates/:slug delete the caller org's own kit -> 204
The value is StarterKit, not Template: the OpenAPI schema namespace is FLAT across the whole fleet and apps/guide already publishes a `Template` (a Guide playbook prompt/snippet, {id,title,body,enabled}). openapi.Weave refuses one name with two shapes — every generated SDK would bind whichever it read last — so the name that was not yet published is the one that yields, qualified by the value's own vocabulary ("Template is one starter kit", below) rather than by a place.
Index ¶
- func Mount(app cloud.Router, deps cloud.Deps) error
- func Shutdown(_ context.Context) error
- type StarterKit
- type Store
- func (s *Store) Close() error
- func (s *Store) Delete(ctx context.Context, org, slug string) (bool, error)
- func (s *Store) Get(ctx context.Context, org, slug string) (StarterKit, bool, error)
- func (s *Store) List(ctx context.Context, org string) ([]StarterKit, error)
- func (s *Store) Put(ctx context.Context, t StarterKit, create bool, now int64) error
- type Variant
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type StarterKit ¶ added in v1.801.350
type StarterKit struct {
Slug string `json:"slug"` // the kit's identity — lowercase alphanumeric with dashes, max 40
Title string `json:"title"` // display name
Category string `json:"category"` // groups the kit in the gallery browser ("Portfolio", "SaaS")
Description string `json:"description"` // the browse-card blurb
Framework string `json:"framework"` // the stack the kit is built on ("Next.js 14.2 + TS")
Features []string `json:"features"` // the highlights the card lists, at most 32
UseCase string `json:"useCase"` // what the kit is for, in a phrase
// Tier is public-gallery curation, carried verbatim from the embedded catalog.
// No request can set it — neither write body has the field and neither builds a
// kit carrying one — so it is absent on every customer-published kit.
Tier *int `json:"tier,omitempty"`
// Rating is public-gallery curation, on the same terms as Tier: catalog-only,
// never accepted from a request, absent on a customer's own kit.
Rating *float64 `json:"rating,omitempty"`
Source string `json:"source"` // the repository the kit is forked from
Preview string `json:"preview"` // the still image the browse card renders
Demo string `json:"demo,omitempty"` // live demo (<slug>.hanzo.app), when deployed
Variants []Variant `json:"variants,omitempty"` // the shapes this template ships in
Org string `json:"org,omitempty"` // owner of a PRIVATE template; empty in the public catalog
}
StarterKit is one starter kit as the console gallery browser consumes it. The `Source`/`Preview` URLs point at the live gallery (gallery.hanzo.ai) for a public entry and at whatever the customer supplies for one of their own; `Demo` is the deployed site itself.
Org is the OWNER of a private template, stamped by the SERVER from the row's key. It is empty on every public catalog entry — that emptiness is what the console badges "yours" on, and it is never read from a request body.
func List ¶
func List() ([]StarterKit, error)
List returns the validated PUBLIC starter-kit catalog (the SAME slice the anonymous HTTP GET serves). Read-only reference content; callers must not mutate the returned slice. Private org templates are deliberately NOT here — they are reachable only through Lookup, which takes the org it isolates on.
func Lookup ¶
func Lookup(ctx context.Context, org, slug string) (StarterKit, bool)
Lookup resolves ONE template for a caller org: that org's OWN private template first, then the public catalog. It is the single door other subsystems (the projects fork flow) read templates through, so "which templates may this org use" is answered in exactly one place. org "" (anonymous/unvalidated) resolves against the public catalog only.
func (StarterKit) Variant ¶ added in v1.801.350
func (t StarterKit) Variant(id string) (Variant, bool)
Variant resolves a variant id against the template and is the ONE place the resolution rule lives. The empty id means "no preference" and yields the template's first (default) shape; a template that ships in a single shape answers with itself, so callers never branch on len(Variants).
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store holds every org's PRIVATE templates — ONE SQLite file ({DataDir}/templates.db) whose isolation key is the org column, the same discipline clients/projects and clients/marketplace keep.
The PUBLIC catalog is deliberately NOT in this table: it stays the embedded catalog.json, which has no write route at all. So a private row has no path into the public gallery by CONSTRUCTION, not by a filter someone has to remember to write — the two live in different containers, and every read of this one binds org.
The whole StarterKit is one JSON `doc`: it is already the exact shape the API serves, so a row can never drift from it as fields are added, and (org, slug) — the only two things ever queried on — stay real columns.
func (*Store) Get ¶
Get returns org's own template at slug. found=false (nil error) when absent — including when the row belongs to ANOTHER org, because the WHERE binds org.
func (*Store) List ¶
List returns org's own templates (tenant-scoped read; there is no all-orgs counterpart, because nothing in this subsystem legitimately wants one).
type Variant ¶
type Variant struct {
ID string `json:"id"` // selector, unique within the template ("react", "grid-3-fluid")
Label string `json:"label"` // human label for the picker
Kind string `json:"kind"` // the axis it varies: format | page | theme
Framework string `json:"framework,omitempty"` // only when it differs from the template's
Source string `json:"source"` // the repository this shape is forked from; the synthesized default shape carries the template's own
}
Variant is one SHAPE of a template: the same design in another format (html/react/bootstrap), on another page (folio's about/contact/grid-3), or in another theme. A variant is an option resolved at fork time from what the user asks for — never a catalog row of its own, which is what made one portfolio template read as 26 templates and one dashboard as 2.