think.studio/nova-html-field
A Laravel Nova field for rendering custom HTML content in your admin panel. Use it to display formatted text, links, or embedded markup in forms and detail views, with simple configuration for Nova resources.
This is a Nova custom field, so the only real prerequisite is a working Laravel app with Nova installed. Because the package has zero stars and no dependents, do a two-minute audit before you build on top of it.
1. Check compatibility first. Nova fields are tightly coupled to the Nova major (Vue 2 on Nova 3, Vue 3 on Nova 4/5). Open the package's composer.json and read the require block:
composer require think.studio/nova-html-field
cat vendor/think.studio/nova-html-field/composer.json # look for laravel/nova constraint
ls vendor/think.studio/nova-html-field/src # find the Field class name
ls vendor/think.studio/nova-html-field/resources/js # confirm compiled assets exist
If the nova constraint pins an old major, or the repo has no compiled dist/ assets, stop and treat this as a fork-and-own component rather than a drop-in dependency.
2. Register the field. Nova's package auto-discovery should register the service provider. If the field class isn't resolvable, add the provider listed under extra.laravel.providers to config/app.php and run php artisan config:clear. Confirm the resolved field class name from src/ — the description only calls it "Nova HTML Field", and the actual import (use ThinkStudio\NovaHtmlField\...) matters.
3. First use case: an informational block with no database column. The highest-value, lowest-risk usage is not persisting HTML at all — it's rendering a read-only notice into a resource form or detail screen. This is where you get value in five minutes, with no migration, no column, and no XSS surface:
use ThinkStudio\NovaHtmlField\Html; // verify exact namespace in src/
public function fields(Request $request)
{
return [
ID::make()->sortable(),
Html::make('Billing notice')
->html('<p class="text-sm">Invoices are generated on the 1st. Contact finance@… for disputes.</p>')
->onlyOnDetail(),
Text::make('Name'),
// …
];
}
The field supports three content sources — a raw string, a Blade view, or a closure — so the same construct covers static copy, a partial you want to reuse across resources, and dynamic text (e.g. a computed "last synced" note). Start with the static-string variant; it's the one you can reason about without reading the source.
Pattern: read-only guidance injected into forms. The most common day-to-day use is dropping contextual help between two real fields — a banner above a Key::make() file upload, a warning under a Boolean::make() toggle. Because the field takes no column, you don't touch the model, the migration, or the resource's fields() persistence; it renders purely on the UI side. Use ->onlyOnForms() or ->onlyOnDetail() so the same resource doesn't render the block everywhere.
Pattern: Blade view as the single source of truth. Instead of inlining HTML strings across resources, pass a view name so the markup lives in resources/views/nova/... where it can be reviewed, styled with your admin CSS, and versioned alongside the rest of your templates:
Html::make('Onboarding')
->view('nova.notices.onboarding', ['user' => $request->user()])
->onlyOnDetail();
This is also the seam you want for swapping the package out later — your markup isn't trapped in a ->html() string inside a resource class.
Pattern: closure for dynamic status. Closures let you render state that depends on the authenticated user or a model at render time. This is where the field earns its keep — a cheap "dashboard card" inside a detail view showing counts, sync status, or links, without building a full Nova card or metric:
Html::make('Account status')
->closure(fn () => view('nova.notices.status', [
'count' => $this->unresolvedTickets(),
]))
->onlyOnDetail();
Keep closures cheap — see the gotchas on index-context queries.
Pattern: wrap writes in sanitization if the HTML is ever user-editable. The field itself is a renderer, not a sanitizer. If any of the content comes from a Textarea/Trix field or an ops-team editor, sanitize at write time with an allowlisted library (e.g. mews/purifier) and store clean HTML — do not rely on the field to strip <script> or onerror=. The field's own escaping/sanitization toggles (whatever they're named in this package) are a second line of defense for trusted HTML, not a substitute for a policy.
Pattern: reuse across resources. Because the field is just a Nova field, you can factor the shared blocks into a small helper or a static method on a Notices class and spread the field definition across resources. That keeps the coupling to this package in one file — which is exactly what you want for a dependency you may have to replace.
Pattern: index-context usage is rare but supported. The field advertises index-context rendering, so you can surface a tiny HTML snippet in a resource table. In practice, prefer ->hideFromIndex() unless you really want markup in a table cell — the value is almost always low and the payload cost is real.
NOASSERTION. This is the single biggest caveat. You cannot confidently fork, patch, or redistribute the code inside a commercial product until it's clarified. If the field matters, ask the maintainer in writing, or treat any usage as internal-only and non-redistributed.composer require does not prove the Vue component compiles or renders.->onlyOnForms(), ->onlyOnDetail(), ->hideFromIndex(), or ->hideFromDetail(), you'll see the block where you didn't expect it — often on the index view where it bloats every row.->escape(...)? ->asHtml()?), view vs closure vs string, and the class namespace need to be confirmed by reading src/ and resources/js/. Don't trust guesses in this assessment.longText. That way, replacing this field with a first-party one or a maintained WYSIWYG later is a code change, not a migration.Html field in-house is a one-to-three-day job — worth pricing against the cost of owning an unmaintained component through the next Nova upgrade.How can I help you explore Laravel packages today?