Essentials
Server-made HTML
Some HTML is best made on the server: Markdown rendered, code highlighted, a diff, an SVG chart. Mark the method #[Html], and Nitro makes it there and hands it to the view, without making the component's state any heavier.
On this page
A component's properties are its state. They go to the browser, signed, and come back with every
#[Server] call, so the server can trust them. That suits a title or a quantity. It doesn't suit
a rendered article or a 5,000-line highlighted file: as a property, that HTML would travel to the server
and back with every click on the page.
An #[Html] method runs on the server only. Its HTML goes to the browser beside the state, not in
it. It is never signed, and never sent back.
Writing one
Mark a public method that takes nothing and returns a string:
<?php
namespace App\Nitro\Pages\Posts;
use App\Models\Post;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
use Nitro\Attributes\Html;
use Nitro\Attributes\Locked;
use Nitro\Attributes\Server;
use Nitro\Component;
class Show extends Component
{
#[Locked]
public int $postId;
public string $title = '';
public string $markdown = '';
public function mount(Post $post): void
{
$this->postId = $post->id;
$this->title = $post->title;
$this->markdown = $post->body;
}
/** The post's body as HTML: made on the server, placed by the view. */
#[Html]
public function body(): string
{
return Str::markdown($this->markdown, ['html_input' => 'escape']);
}
#[Server]
public function save(): void
{
Gate::authorize('update', $post = Post::findOrFail($this->postId));
$post->update(['body' => $this->markdown]);
}
}
The view reads it like a property, and places it unescaped, since it is HTML already:
<article>
<h1>{{ $title }}</h1>
<div class="prose">{!! $this->body !!}</div>
</article>
A JavaScript view reads it by the method's name, as each framework places HTML:
| View | Placing body |
|---|---|
| Vue |
<div class="prose" v-html="body" />
|
| React |
<div className="prose" dangerouslySetInnerHTML={{ __html: body }} />
|
| Solid |
<div class="prose" innerHTML={body} />
|
| Svelte |
<div class="prose">{@html body}</div>
|
When it runs
Whenever the server sends the component's state to the browser: on the first page, on a
visit, and after each #[Server] call. The view
always shows the latest HTML the server made.
Changed in the browser, made again by the server
The browser can't run an #[Html] method. When a browser method changes what the HTML is made
from, such as $markdown while someone types, the HTML shows the old text until the next
#[Server] call. A preview as you type belongs in the browser: keep #[Html] for the
HTML the server must make.
What travels
The browser keeps the HTML apart from the state, with a short fingerprint of it. A call sends back the fingerprint alone, and the server leaves out of its answer any HTML whose fingerprint still matches. So a click on a page showing a long file sends a few bytes, and gets the file again only if it changed.
| A long file | To the browser | Back to the server | Again after a call |
|---|---|---|---|
| As a property | With the page, inside the signed state | All of it, with every call | With every call |
As
#[Html]
|
With the page, beside the state | Its fingerprint (a few bytes) | Only when it changed |
Keeping it fast
The method runs each time the server answers. A slow one (highlighting, rendering a large document) should keep its result, keyed by what it is made from:
#[Html]
public function code(): string
{
/** One file's highlighting, made once: its hash is its content, so the key never goes stale. */
return Cache::rememberForever("highlighted:{$this->blobSha}", fn () => $this->highlighter->html($this->blobSha));
}
Safe HTML
The view places the HTML as it is, so make it safe where you make it: escape what you put in it, and render
Markdown with raw HTML escaped ('html_input' => 'escape'), as above. The HTML a browser could
send back is never read: the server makes it again each time.
Rules
The build stops, naming the file and line, when an #[Html] method:
- doesn't declare
: string, or takes parameters; - isn't public;
- is also
#[Server]or#[Computed](it runs on the server already, and is read as a property); - is on a store, which has no view: put it on the component that shows it.