Views and assets
Pages are HTML files in app/resources/views/. Vite builds them, with Tailwind and your scripts, into public/build/, and the Rust server serves the result.
Views#
view("welcome") serves public/build/views/welcome.html. Router::view("/", "welcome") is the same thing as a route. When the front end is not built yet, the answer is a 503 page that tells you to run clamp dev.
Templates#
render passes data to a view, which reads it with a small part of Laravel's Blade. {{ }} escapes, so user text is safe by default; {!! !!} prints trusted HTML as-is:
render("post", &[("post", &post), ("comments", &comments)])
@extends('layouts.app')
@section('title', post.title)
@section('content')
<h1>{{ post.title }}</h1>
<div>{!! post.body !!}</div>
@if(comments)
<ol>
@foreach(comments as comment)
<li>{{ comment.name }}: {{ comment.body }}</li>
@endforeach
</ol>
@else
<p>No comments yet.</p>
@endif
@include('partials.notices')
@endsection
{{ post.title }} | Prints escaped. Dots read keys of a map. |
{!! post.body !!} | Prints as-is, for HTML you made yourself. |
@if(x) @if(!x) @else @endif | True for true, non-empty text and non-empty lists and maps. |
@foreach(posts as post) @endforeach | Repeats for each item of a list. |
@extends('layouts.app') | Renders the layout with this view's sections. |
@section('content') @endsection, @section('title', post.title) | Fills the layout's @yield('content'). |
@include('partials.nav') | Renders another view with the same data. |
@include('components.post-card', post: featured, compact: true) | Also passes named values, for that view only: a name from your data, 'text', true or false. Keep reusable pieces in components/ and layout fragments in partials/. |
{{-- note --}}, @@, @{{ | A comment, a literal @, a literal {{. |
There are no expressions: work out counts, dates and labels in the controller. A name the view uses but was not given is an error. So is an unclosed @if or a missing layout. The error names the view and line, goes to the log, and the visitor gets a 500. A missing key of a map is empty text. @ directly after a letter is left alone, so [email protected] and CSS @media pass through.
Data is anything that implements ToValue: text, numbers, bool, Vec, Option (None is false). Give a model the fields its views read:
impl ToValue for Post {
fn to_value(&self) -> Value {
Value::map(&[("slug", &self.slug), ("title", &self.title), ("body", &self.html())])
}
}
Safe output#
{{ }}escapes HTML, which is enough in text and quoted attributes. In anhreforsrcit does not stop ajavascript:link, so validate links people submit with theurlrule (httpandhttpsonly) before you store them.{!! !!}is for HTML you produced. Markdown from anyone else goes throughweb::markdown::to_html(themarkdownfeature), which shows raw HTML as text and points non-web links (javascript:,data:) to#.- The Content-Security-Policy from
security_headersis the safety net: even if markup slips through, inline script does not run.
Layouts and partials live in app/resources/views/ like any page, and Vite builds them too; the layout is where it injects your scripts and styles. They are only ever served rendered: /build/views/… is never sent raw.
Error pages#
error(status) looks for your own page first, then falls back to a built-in one:
errors/404.html: one exact status.errors/4xx.htmlorerrors/5xx.html: a whole class. The template ships both.- The built-in page, which follows the visitor's light or dark theme.
Error views get {{ status }} and {{ reason }}, so one 4xx page covers 404, 419, 429 and the rest. They can @extends your layout. If an error view itself fails to render, the built-in page answers.
Static files#
A GET that matches no route serves the file from public/: /robots.txt, images, and the Vite build under /build/. Paths that try to leave public/ are refused with 404. Recognised types: html, js, css, json, txt, svg, png, ico and woff2; anything else is sent as application/octet-stream.
The front end#
vite.config.ts builds every page in app/resources/views/ into public/build/, with Tailwind 4 from resources/css. clamp dev runs the watcher and the server together. Without Node.js, run npm run build once on a machine that has it and ship public/build/.