view

package
v0.25.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 10, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package view is the view layer: kyse for markup, HTMX for interaction, Alpine for ephemeral client state, Tailwind for style. It is a binary and it is never Node.

A project that uses it still runs with `git clone && aru dev`: no node_modules, no package.json, no lockfile of JavaScript, nothing installed beyond Go and the standalone binaries the CLI fetches. Having a build step is allowed; being Node is not (RULE 13).

It lives in the framework and the views do not, and that split is deliberate: resources/views/ belongs to the project, because it is edited; the rendering machinery belongs here, because it is not. It used to be a repository of its own, and dissolving it is ADR 0021.

The error page deliberately does not use this package. It has to render when the rest is broken, including when the view build failed, so it stays as html/template inline in observability/errorpage.

Index

Constants

View Source
const AssetPath = "/_arandu/assets/"

AssetPath is where assets are served from. The hash is in the path, so the response can be cached forever and a new build simply has a new URL.

View Source
const Stylesheet = "app.css"

Stylesheet is the name of the one stylesheet, and there is only one.

The framework embeds a default under this name and RegisterStylesheet replaces it. Not a second file, not a second URL, not a cascade order: one name, one URL, one set of bytes (RULE 9).

Variables

This section is empty.

Functions

func AssetHash added in v0.20.0

func AssetHash(body []byte) string

AssetHash is the path segment an asset is served under: the first twelve hex characters of the SHA-256 of its bytes.

It is exported because one thing outside this repository has to produce the same string. `aru font:add` writes an absolute src: into the stylesheet it generates, and it has to name the font's own hash -- a URL carrying any other hash is served without caching, by design, so a relative url() inheriting the STYLESHEET's hash means the font is re-downloaded on every page view. Eighteen kilobytes, every request, silently.

The CLI is a separate module and cannot import this one, so it computes the same three lines. That is a contract across a repository boundary, which is why this function exists rather than the expression being inlined above: it has a name, it has a test, and both sides point at it.

func CSRF

func CSRF(w io.Writer, data any) error

CSRF writes the hidden input a form needs.

The token comes from the data, through an interface the page data satisfies. It is not read from a global: a template that reaches for request state outside the data it was given is how a form ends up with another session's token under load.

func Handler

func Handler(w http.ResponseWriter, r *http.Request)

Handler serves the embedded assets.

Anything whose path carries the right hash is immutable and cached for a year; a wrong hash is served without caching, so a stale reference degrades into a slow page rather than a broken one.

func Include

func Include(w io.Writer, name string, data any) error

Include renders a partial with the same data as the page.

A partial shares the page's data. That data is one typed struct, so the partial receives exactly it -- and a partial that wants something else is a partial that takes different data, which is what a component is for.

func Register

func Register(name string, f Func)

Register records a compiled view under the name a controller renders it by.

view.Register("invoices/index", renderInvoicesIndex)

Generated code calls it from init(), so importing the views package is what makes them reachable -- the same shape as a database/sql driver.

Registering twice panics rather than replacing. Two views for one name is a build artifact that outlived its source, and finding out at boot beats finding out from a page that renders the wrong thing.

func RegisterAsset added in v0.21.0

func RegisterAsset(name, contentType string, body []byte)

RegisterAsset adds one file to the served assets.

It is the transport primitive and it knows nothing about what it carries: a name, a content type and the bytes. What kinds of file an application ships is the view stack's question, not this package's -- kyse/fonts vendors faces through here, and anything else that needs a content-addressed URL can too.

It used to be RegisterFont, with a table of font extensions and a preload helper beside it. That put a typographic decision in the layer that serves bytes: the framework had opinions about woff2 against ttf, and about what a <link rel=preload> should say. Both belong to the stack that draws the page.

Unlike RegisterStylesheet this ADDS rather than replaces -- there is one stylesheet and there may be many of these. Registering one name twice panics, because two sets of bytes under one URL is a generated file that outlived its source, and every browser that cached the first holds it forever: the URL is served immutable.

The name is what a generated stylesheet references. It is referenced by the ABSOLUTE path AssetPath+AssetHash(body)+"/"+name, and not relatively: Handler compares the hash in the path against the hash of the bytes and serves a mismatch uncached, so a relative url() inheriting the stylesheet's hash is a file re-downloaded on every page view.

func RegisterLayout

func RegisterLayout(name string, f LayoutFunc)

RegisterLayout records a compiled layout. Generated code calls it from init() when the view contains a @yield.

func RegisterStylesheet added in v0.12.0

func RegisterStylesheet(css []byte)

RegisterStylesheet replaces the embedded stylesheet with the application's.

`aru view:build` compiles resources/css/app.css into assets/app.css, and the skeleton hands those bytes over from init(), the same shape as Register:

//go:embed assets/app.css
var appCSS []byte

func init() { view.RegisterStylesheet(appCSS) }

It replaces rather than adds. The framework's copy is a default so that a project renders before its first view:build, not a base layer to cascade on top of -- two stylesheets would mean two URLs, an order that matters, and a specificity fight nobody can win from the application side.

Without it the browser received the framework's stylesheet, md5 identical, and every class written in a project's own views did nothing. Nothing failed: the page was served, with 200, unstyled.

Registering twice panics rather than replacing, for the same reason Register does: two stylesheets for one name is a build artifact that outlived its source, and finding out at boot beats finding out from a page that renders with somebody else's design.

func Registered

func Registered() []string

Registered returns the known view names, sorted. `aru doctor` reads it to check that every ctx.View("x") has a view named x.

func ReloadTag added in v0.24.0

func ReloadTag(stream string) string

ReloadTag registers the reload script and returns the tag that runs it.

stream is where the script listens, and it comes from the caller because the route belongs to the kernel: two constants for one address is how a client and a server come to disagree about it.

Registering happens once. A second call returns the same tag rather than panicking on a duplicate asset, so a test that boots two kernels is not a crash.

func RenderInto

func RenderInto(w io.Writer, layout string, data any, sections map[string]func(io.Writer) error) error

RenderInto renders a layout, handing it the sections of the child view.

This is what `@extends` compiles to: the child does not write markup, it renders the layout and passes what goes in the holes: the child is evaluated to fill the sections before the layout runs.

func Text

func Text(v any) string

Text renders a value as a string, for interpolation.

It handles the types a view actually interpolates, and formats anything else with %v. It is not reflection over a struct: the field access already happened in generated Go, and this only turns the result into characters.

func URL

func URL(name string) string

URL returns the versioned path of an asset: /_arandu/assets/<hash>/htmx.min.js

The hash comes from the content, so upgrading HTMX changes the URL and no browser serves a stale script -- without anyone remembering to bump a version.

func Version

func Version() string

Version reports the served version of each asset, for `aru doctor` and for the debug page.

It reports what is served rather than what is embedded, so a stylesheet that never reached the browser shows up here as the framework's hash next to a project that thought it had built its own.

func WrongData

func WrongData(view, want string, got any) error

WrongData is what a generated view returns when the data is not the struct it declared.

Generated code calls it, so the message is the same everywhere:

d, ok := data.(HomeData)
if !ok { return view.WrongData("home", "HomeData", data) }

The alternative -- rendering the zero value -- is a blank page with a 200, which is the failure this framework exists to make impossible.

func Yield

func Yield(w io.Writer, sections map[string]func(io.Writer) error, name string) error

Yield renders the section a child view declared, or nothing.

A layout yields sections that a given child may not have, and the answer is the empty string. A missing section is a page without a sidebar, not an error.

Types

type Asset added in v0.21.0

type Asset struct {
	Name        string
	ContentType string
	URL         string
}

Asset is one served file, as Registered reports it.

func Assets added in v0.21.0

func Assets() []Asset

Assets reports the name, content type and URL of everything served.

It exists so a caller can build markup about the assets it registered -- the <link rel=preload> for a font, for instance -- without this package having to know what a font is. Sorted by name, so the answer does not change between two runs of a binary serving the same bytes: a map has no order, and markup that differs by line order is markup no diff and no cache can compare.

type Func

type Func func(w io.Writer, data any) error

Func is what a compiled view is: a function that writes HTML.

`aru view:build` emits one per `.kyse.go` and registers it by name. The data arrives as `any` and the generated function asserts it back to the struct the view declared -- which is why a wrong type is an error naming both sides rather than a blank page.

type Layout added in v0.14.0

type Layout interface {
	// PageTitle is the document title, and what HTMX swaps on navigation.
	PageTitle() string
	// PageDescription is the meta description. Empty writes no tag, because a
	// missing description outranks an empty one.
	PageDescription() string
	// CanonicalURL is the absolute address of this page, or empty for none.
	CanonicalURL() string
	// IsCurrent says whether a navigation target is this page, so the header
	// can stop linking to where you already are.
	IsCurrent(href string) bool

	// BrandName is the application name in the navigation bar.
	BrandName() string
	// CSRFToken is what @csrf reads, and what <body> carries into every HTMX
	// request.
	CSRFToken() string

	// SignedIn decides which half of the navigation is drawn, and SignedInName
	// is who it greets.
	SignedIn() bool
	SignedInName() string

	// The navigation targets. An empty one draws no link, which is how a route
	// the application never registered stays out of the markup instead of
	// becoming a 404 the layout put there.
	HomeLink() string
	LoginLink() string
	LogoutLink() string
	RegisterLink() string

	// PanelLink is the signed-in person's own area, and AdminLink is the one
	// only some of them may open. Both follow the same rule as the four above:
	// empty draws nothing.
	//
	// AdminLink is empty for anybody who would be refused, which is what keeps
	// the header from ever offering an address that answers 403. The decision is
	// the controller's -- it holds the subject and the policy -- and the markup
	// only ever sees a string. A layout that asked about roles would be a second
	// place where authorization is decided, and the second place is the one that
	// gets it wrong.
	PanelLink() string
	AdminLink() string
}

Layout is what a layout asks of the data every screen hands it.

It lives here rather than in the application because every Arandu application declared the same ninety lines of it, and ninety lines nobody wrote are ninety lines nobody reads. A project that needs different chrome declares its own interface in the layout's @go block; this is the one the delivered layout uses.

It is an interface rather than a struct so that pages with unrelated data share one frame: the layout asks for behaviour, and Page below is the implementation a page embeds to get it.

type LayoutFunc

type LayoutFunc func(w io.Writer, data any, sections map[string]func(io.Writer) error) error

LayoutFunc is a view that receives sections, which is what a layout is.

type Module

type Module struct{}

Module serves the embedded assets and wires the renderer.

It is a kernel.Module for one reason: it was not, and every application shipped a page that asked for its own stylesheet and got 404.

The route used to be exported as Mount, a plain function, with a comment arguing that "a module that exists only to serve two files is a module people have to remember to register". That reasoning was backwards, and the code proved it: Mount had zero call sites across every repository -- the kernel did not call it, the generated main.go did not call it, and neither did the sign-in screen that `aru make:auth` writes. That screen emits three tags, and against a real server all three answered 404: no stylesheet, no HTMX, no Alpine. Found by audit, reproduced end to end.

A function has to be remembered. A module appears in the Register call next to events, jobs and the scheduler, which is where somebody reading main.go already looks to learn what an application is made of.

func NewModule

func NewModule() *Module

NewModule returns the module.

k.Register(view.NewModule(), auth.New(...), events.NewModule())

func (*Module) Name

func (*Module) Name() string

Name is the module identifier.

func (*Module) ReloadTag added in v0.24.0

func (*Module) ReloadTag(stream string) string

ReloadTag supplies the development live-reload tag to the kernel.

kernel.ReloadTagger, an optional interface, asked for only in development. The kernel gives the address of the stream it serves; this package owns the script and the asset it is served as.

func (*Module) Renderer

func (*Module) Renderer() httpx.Renderer

Renderer supplies the view renderer to the kernel.

It is kernel.RendererProvider, an optional interface: the kernel asks every registered module whether it brings one, before any route is registered. That is what makes ctx.View work without the application calling a wiring function that somebody eventually forgets.

func (*Module) Routes

func (*Module) Routes(r *httpx.Router)

Routes registers the content-addressed asset route.

One route, one handler. The hash in the path is what makes the response cacheable forever, and what makes a deploy invalidate it without anybody clearing anything.

type Page added in v0.14.0

type Page struct {
	// Title is the document title.
	Title string
	// Description is the meta description, and the og:description. Leave it
	// empty on a page that has nothing specific to say.
	Description string
	// Canonical is the absolute URL of this page. It is what stops the same post
	// counting twice when it answers on more than one address.
	Canonical string

	// AppName is the brand in the navigation bar.
	AppName string

	// Token is the CSRF token issued for this session. It reaches the markup
	// twice: as the hidden field @csrf writes, and as the hx-headers attribute
	// on <body> that makes every HTMX request carry it.
	Token string

	// Authenticated decides which half of the navigation bar is drawn, and
	// UserName is the signed-in person's display name.
	Authenticated bool
	UserName      string

	// Where the navigation points. They come from the router, through the
	// controller. RegisterURL is empty when registration is not open.
	HomeURL     string
	LoginURL    string
	LogoutURL   string
	RegisterURL string

	// PanelURL and AdminURL are drawn only when signed in, and AdminURL only
	// for somebody the policy would let in. Filled by the controller, from the
	// subject it already holds -- see the Layout interface.
	PanelURL string
	AdminURL string

	// Path is the address this page was served at, so the navigation can say
	// where you are.
	//
	// A header that offers "Sign in" on the sign-in page is a header with a link
	// to the page you are reading -- and the one control that would help, the
	// way out to registering, is the one it does not show. The layout reads this
	// through IsCurrent; nothing else needs it.
	Path string
}

Page is the chrome every screen hands the layout, embedded rather than repeated:

type PostsIndexData struct {
	view.Page
	Posts []PostRow
}

A page declares a struct of its own -- which is what turns a typo in a field name into a compile error -- and takes the frame from here.

Nothing on it is a helper a view reaches for by itself. There is no config(), no route() and no auth(): the controller fills these in, so a name that drifts is a compile error rather than a blank link, and a form can never end up carrying another session's token under load.

func (p Page) AdminLink() string

AdminLink is the administration area, or empty for anybody who would be refused it.

func (Page) BrandName added in v0.14.0

func (p Page) BrandName() string

BrandName is the application name, shown in the navigation bar.

func (Page) CSRFToken added in v0.14.0

func (p Page) CSRFToken() string

CSRFToken is what @csrf reads to write the hidden field.

It is a method rather than the field itself because the field is also interpolated into hx-headers, and one name cannot be both.

func (Page) CanonicalURL added in v0.14.0

func (p Page) CanonicalURL() string

CanonicalURL is the address search engines should treat as this page's own.

func (p Page) HomeLink() string

HomeLink is where the brand points.

func (Page) IsCurrent added in v0.23.0

func (p Page) IsCurrent(href string) bool

IsCurrent reports whether a navigation target is the page being read.

It compares paths and ignores the query, because "?resent=1" is still the same page -- a navigation that changes when a flash message is added is one nobody can predict.

An empty href is never current: an empty URL draws no control, and a control that is not drawn cannot be the one you are on.

func (p Page) LoginLink() string

LoginLink is the sign-in screen.

func (p Page) LogoutLink() string

LogoutLink is what the sign-out form posts to.

func (Page) PageDescription added in v0.14.0

func (p Page) PageDescription() string

PageDescription is the meta description of this page.

func (Page) PageTitle added in v0.14.0

func (p Page) PageTitle() string

PageTitle is what the browser tab shows.

func (p Page) PanelLink() string

PanelLink is the signed-in person's own area, or empty.

func (p Page) RegisterLink() string

RegisterLink is the sign-up screen, or empty when registration is closed.

func (Page) SignedIn added in v0.14.0

func (p Page) SignedIn() bool

SignedIn reports whether there is a session behind this render.

func (Page) SignedInName added in v0.14.0

func (p Page) SignedInName() string

SignedInName is who the navigation bar greets.

type Renderer

type Renderer struct{}

Renderer draws a view. It is the concrete side of httpx.Renderer.

func NewRenderer

func NewRenderer() *Renderer

NewRenderer returns the renderer. The kernel hands it to the router at boot.

func (*Renderer) Render

func (*Renderer) Render(ctx context.Context, w http.ResponseWriter, status int, name string, data any) error

Render writes a named view to the response.

The render is recorded on the Collector, so a slow page shows on the console whether the time went to the database or to the markup -- which is the difference between two very different afternoons.

func (*Renderer) RenderToString added in v0.17.0

func (*Renderer) RenderToString(name string, data any) (string, error)

RenderToString draws a view and returns it, instead of writing it to a response.

It exists for mail, and for anything else that produces HTML nobody is waiting on a socket for. The Renderer above cannot serve that: it takes an http.ResponseWriter and sets a status, and a message being composed in a job has neither.

It is the same registry and the same compiled function, which is the point: an e-mail is drawn by the view layer a page is drawn by, so a field that does not exist is a compile error there too, and interpolation is escaped by construction rather than by whoever wrote the template.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL