view

package
v0.16.0 Latest Latest
Warning

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

Go to latest
Published: Aug 9, 2026 License: MIT Imports: 16 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 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 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 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 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

	// 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
}

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) 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
}

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 (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 (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) 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.

Jump to

Keyboard shortcuts

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