porang

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 13 Imported by: 0

README

porang

The Arandu view layer: templ for markup, HTMX for interaction, Tailwind for style. It is a binary, and it is never Node.

A project using it still runs with git clone && aru dev. No node_modules, no package.json, no JavaScript lockfile, nothing installed beyond Go. Having a build step is allowed; being Node is not.

What is in the binary

HTMX, Alpine and the compiled stylesheet are embedded with go:embed and served from /_arandu/assets/<hash>/<name>.

That is not preference. SecurityHeaders sets script-src 'self', so loading HTMX from a CDN would mean loosening the CSP — paying in security for convenience. It also keeps the deploy at one binary: no asset publishing step, no CDN to invalidate, no storage:link.

The hash comes from the content, so upgrading HTMX changes the URL and nobody serves a stale script. A request carrying an old hash still gets the file, with no-cache instead of immutable: a stale reference degrades into a slow page, never a broken one.

The line that matters most

@layout.Base(layout.Props{Title: "Sign in", CSRFToken: token})

Base puts hx-headers on <body>, carrying the CSRF token into every HTMX request made from anywhere on the page. Without it, every hx-post and hx-delete fails the CSRF check — and the failure looks like a session problem, so people spend an afternoon on it.

It lives here, in the framework's own layout, so nobody has to remember. aru doctor checks for it too.

Components are functions

@components.Button(components.ButtonProps{
    Label:   "Delete",
    Variant: components.Danger,
    HX:      components.HX{Delete: "/invoices/1", Confirm: "Delete this invoice?"},
})

There is no <nx-modal /> and no web component registered at runtime. A custom tag would be a second component system beside templ, resolved at runtime, with errors that only appear on screen. templ already gives composition, types and a compile error.

The HX struct exists for the same reason: hx-swep instead of hx-swap is a typo that silently does nothing, and a review misses it.

Where Alpine is allowed

Only when the state is all three at once: purely client-side, ephemeral (dies on reload with nothing lost), and invisible to the server.

Allowed: a dropdown being open, the selected tab, focus, an input mask, a confirmation before submitting.

Not allowed: anything that reaches the server, anything persisted, anything derived from data the server already has. If you are writing a business rule in x-data, the component should have been an HTMX fragment.

Building the stylesheet

tailwindcss -i assets/app.src.css -o assets/app.css --minify

The standalone binary, a single file, managed by aru and pinned by version — the same way Go handles its own toolchain. Never npx, never a package from npm.

What this package does not touch

The error page. It has to render when everything else is broken, including when the asset build failed, so it stays as inline html/template in observability/errorpage.

License

MIT, the same license Laravel uses. See LICENSE.md.

Documentation

Overview

Package porang is the view layer: templ for markup, HTMX for interaction, 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. Having a build step is allowed; being Node is not (RULE 13).

The error page deliberately does not use this package. It has to render when the rest is broken, including when the asset 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.

Variables

This section is empty.

Functions

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 Render added in v0.3.0

func Render(ctx context.Context, w io.Writer, name string, c templ.Component) error

Render writes a component and records how long it took.

Use it instead of calling Component.Render directly. That is the whole reason it exists: rendering through the component's own method leaves "render" at zero on the request timeline, and a timeline with an empty column is worse than no timeline -- it says the view is free when nobody measured it.

return porang.Render(r.Context(), w, "customer/list", page)

The name is what shows on the timeline, so it should say which template, not which function.

func RenderFragment added in v0.3.0

func RenderFragment(ctx context.Context, w http.ResponseWriter, status int, name string, c templ.Component) error

RenderFragment writes an HTMX fragment with a status.

The status is explicit because a rejected form is 422 and a refused action is 403, and HTMX swaps the body of both -- answering 200 for a rejection means the browser, the logs and every metric agree that it worked.

func RenderPage added in v0.3.0

func RenderPage(ctx context.Context, w http.ResponseWriter, name string, c templ.Component) error

RenderPage writes a full HTML page: the content type, then the component.

The header is set before rendering rather than after, because a component that fails halfway has already written bytes, and a response with markup and no content type is one browsers guess at.

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 embedded version of each asset, for `aru doctor` and for the debug page.

Types

type Module added in v0.5.0

type Module struct{}

Module serves the embedded assets.

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 added in v0.5.0

func NewModule() *Module

NewModule returns the module.

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

func (*Module) Name added in v0.5.0

func (*Module) Name() string

Name is the module identifier.

func (*Module) Routes added in v0.5.0

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.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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