๐ค Icons
rastrillo new writes an icon set into your app as an ordinary
app-owned package. Three flags choose what it writes, and all six set ร
delivery combinations scaffold, compile and pass
rastrillo generate --check.
rastrillo new --icons=lucide --icon-delivery=inline --ux=considered myapp
Take the defaults unless you have a reason not to. The rest of this page is the reasons.
The slugs are Rastrillo's, not a vendor's
Twelve slugs, and they mean the same thing in every app whatever set backs them:
alert-triangle check check-circle chevron-down help-circle
info kebab menu plus search x x-circle
rastrillo.IconSlugs() is the list.
This matters more than it sounds. Five of the twelve differ from
Lucide's canonical names โ kebab is Lucide's ellipsis-vertical, and
v1 renamed check-circle, alert-triangle, x-circle and
help-circle โ so even the Lucide set carries a translation table.
kebab and menu are the pair worth keeping straight: kebab is the
three dots that mean "more actions on this row", menu the three lines
that mean navigation. The shells use menu when they collapse.
The payoff is that {{icon "search"}} means the same thing everywhere
and the shipped ui/ partials never change when the set does.
internal/iconsets asserts that every scaffoldable set covers the whole
list, so an icon added to the framework that some set cannot answer
fails the build instead of vanishing the moment someone passes
--icons.
An unknown slug at run time renders nothing. A typo costs you a missing icon, not a crashed page mid-response.
--icons
lucide (default) or font-awesome.
--icons=font-awesome means Font Awesome Free. Pro is a paid product
Rastrillo cannot vendor or link on your behalf, so Pro-only icons will
not resolve. If you have a Pro licence, wire your own kit through the
same seam โ the icons package is app-owned source.
Choosing it also writes the CC BY 4.0 attribution the licence requires. That obligation is your app's, so it travels with your code.
--icon-delivery
inline (default), cdn, or js.
Inline is the recommendation: no build step, no second origin, works offline.
cdn and js are properly supported, not grudgingly tolerated. Each
prints its specific cost once at scaffold time, records it as a comment
in the generated package, and never mentions it again โ a supported
choice that nags on every build is not really supported.
The one cost worth repeating: with js, icons do not render at all
without JavaScript.
Both remote modes pin exact versions with real SRI hashes.
--ux
considered (default) or standard.
It seeds a UX convention profile into your AGENTS.md, which carries
your app's instructions and is the source of truth from then on.
CLAUDE.md is a one-line @AGENTS.md import, so the instructions reach
whatever agent someone uses.
The profile is a seed, not a live binding. The resolved list is written once, an explicit flag beats the profile's default so the file never lies about what your app does, and nothing re-reads the profile name afterwards. That is what makes editing a line as valid as picking a profile, and what stops a Rastrillo upgrade changing a shipped app's UX.
Conventions marked [x] are enforced by a vendored component; [ ]
ones an agent applies by hand. The gap between the two stays visible.
The conventions in considered are Rastrillo's own. For wider reading
on interface quality, impeccable.style, the
WAI-ARIA Authoring Practices and
Inclusive Components are all
worth your time โ offered as reading, not as anything this framework
implements.
Wiring
tmpl := template.Must(template.New("").
Funcs(ui.Funcs(ui.WithIcons(icons.Icon, icons.Assets))).
ParseFS(ui.Templates(), "*.html"))
The scaffold wires both seams into the generated render.go and puts
{{iconAssets}} in the layout's <head>. That renders empty for the
inline default, which is why switching delivery later needs no template
edit.
Checking your icons
rastrillo generate --check fails when a template names an icon nothing
answers. It catches both {{icon "x"}} and the commoner form where the
slug reaches a partial as data:
{{template "list-row-action" dict "ActionIcon" "plus"}}
A slug computed at run time cannot be checked, as with any static gate.
Checking the pins
go test -tags pins ./internal/iconsets/
Verifies that every pinned URL still hashes to the integrity value shipped beside it. A mismatch means the bytes changed under a version that is supposed to be immutable, which is serious. It separately reports whether a newer release exists, which is only informational.
Versions are pinned โ lucide@1.33.0, lucide-static@1.33.0,
@fortawesome/fontawesome-free@7.3.1 โ and nothing re-pins them
automatically. A version changed without its hash fails as an unstyled
page rather than an error, so check both together.
The test is build-tagged so the ordinary suite and CI never depend on jsdelivr or the npm registry being up. A check that fails when someone else's CDN has a bad afternoon teaches people to ignore it. Run it at release.
Read this page as markdown โ exact, unstyled, and cheap for an agent to fetch.