treestoneit/html
TreestoneIT HTML gives Laravel and PHP developers a fluent, chainable API for generating HTML elements, attributes, and nested markup in code, keeping views clean and output consistent. It integrates smoothly with Blade and existing templates.
Use it for one thing: dropping a read-only block of HTML into a Nova resource — a link, a badge, a help banner, an embed. It is a display-only field, not a storage field, and it is not a general-purpose card or layout tool. On Nova 3+ you can usually replace it with Text::make('...')->asHtml(), so treat this package as a fit for older Nova installs or as a stopgap.
Install. The package is archived and effectively unmaintained since 2018-08, so Packagist resolution may fail. If composer require treestoneit/html errors out, pull it from VCS and pin a commit:
"repositories": [
{ "type": "vcs", "url": "https://github.com/treeStoneIT/nova-html" }
],
"require": {
"treestoneit/html": "dev-master#<commit-sha>"
}
Then confirm what you actually got:
composer show treestoneit/html
ls vendor/treestoneit/html
Look here first, in this order:
src/ — the field class and service provider. Note the exact namespace (TreeStoneIT\Html\...-style, casing matters on case-sensitive filesystems) and whether the provider registers assets via Nova::script()/Nova::style() or $this->publishes().resources/js/field.js — the Vue component. Its props array tells you the fluent setter name. If the prop is html, the setter is the html() macro; if it's content, it's content().dist/js/field.js — the compiled bundle. Search it for Nova.booting vs app.component. That single grep tells you whether it was written for Nova 1/2 (Vue 2) or is remotely compatible with Nova 4/5.First use case — a link on the detail page:
use TreeStoneIT\Html\Html; // confirm exact namespace from src/
public function fields(Request $request)
{
return [
Html::make('Portal')
->html('<a href="https://portal.example.com" target="_blank" rel="noopener">Open portal</a>')
->onlyOnDetail(),
];
}
If ->html() isn't the method, the fallback always works because Nova fields serialize their meta as props:
Html::make('Portal')
->withMeta(['html' => '<a href="...">Open portal</a>']);
Check $field->meta() in tinker to see exactly what prop name the component expects.
Static notice or banner on detail pages. Keep markup out of the resource class by rendering a Blade partial:
Html::make('Billing notice')
->html(view('nova.partials.billing-notice', ['user' => $this->resource])->render())
->onlyOnDetail()
->canSee(fn ($request) => $request->user()->can('billing.manage'));
canSee() is the cleanest way to make the block conditional — the HTML is never sent to the browser when it returns false.
Index-only action column. The field is ideal for a per-row link when you only need one or two static anchors:
Html::make('Stripe')
->html('<a href="https://dashboard.stripe.com" target="_blank" rel="noopener">Dashboard</a>')
->onlyOnIndex(),
For per-row dynamic URLs, don't fight the field — the HTML string lives in field meta, which is resolved once per field definition. Either:
Text::make('Stripe')->asHtml()->displayUsing(fn () => "<a href=\"{$this->resource->stripe_url}\">Dashboard</a>") on Nova 3+, orresolveUsing()-aware setter.Group and order it like any other field. It composes with Panel, sortable-free fields() ordering, fullWidth(), and help(). Put a long notice in its own Panel so it doesn't sit awkwardly next to input fields.
Keep it out of forms. Because it renders in create/update views by default, always pair it with ->exceptOnForms() or ->onlyOnDetail()/->onlyOnIndex(). A non-editable HTML blob in a form is confusing and does nothing on submit.
Multiple blocks, one resource. Names must be unique; use short internal identifiers (Html::make('docs-link')) so you can reuse the label as a heading inside your own markup.
Embeds and iframes. <script> tags injected via v-html will not execute in Vue. Use an <iframe> (YouTube, Loom, a status page) and set width/height inline, since Nova's utility classes differ by version.
Replacement path. If you outgrow it, generate a local field with php artisan nova:field acme/html — you get the same Vue component shape with your own prop names, plus support for Nova 4/5's asset registration. Copy the ~40 lines you need from vendor/treestoneit/html/src and drop the dependency.
Nova.booting and Vue 2 will fail to register in Nova 4+ (Vue 3), producing a blank cell and an Unknown custom element warning in the browser console. Test on your exact Nova version before committing.$field->meta() — wrong prop name; (3) the dist/ bundle exists and matches resources/js/. Nova packages that never call Nova::script() need a manual php artisan vendor:publish or a nova-components-style registration.v-html. Never interpolate unescaped user input — a value stored by a low-privilege user becomes stored XSS with admin session privileges. Wrap dynamic pieces in e() (htmlspecialchars) before concatenating.$fillable, don't attach ->rules(), don't expect sortable() or filterable() to do anything. It won't appear in search, filters, or exports.hideLabel() in the Nova 1.x era; pass a short, low-noise name and put the human-readable heading inside your own markup (e.g. <h3 class="...">Billing</h3>).view('nova.partials.x')->render() also gives you translations and @can checks for free.style="..." for anything that must survive an upgrade, and always add rel="noopener" alongside target="_blank".dev-master against an archived repo is stable in practice, but a fork or VCS typo will bite silently. A commit SHA in composer.json makes builds reproducible.$this->assertInstanceOf(Html::class, collect((new UserResource($user))->fields(request()))->first()), and assert the meta string contains your expected anchor. Nova's browser-level rendering isn't worth asserting through this package.How can I help you explore Laravel packages today?