Nitro

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:

app/Nitro/Pages/Posts/Show.php Browser + server
<?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:

resources/views/nitro/pages/posts/show.blade.php
<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:

PHP
#[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.