khalin/nova-link-field
A Laravel Nova field for adding clickable link inputs to your resources. khalin/nova-link-field makes it easy to display and manage URLs alongside model fields in Nova admin panels.
Architecture fit
khalin/nova-link-field is not an architectural component — it is a single Field subclass (src/Link.php) plus a compiled Vue component that Nova's own asset pipeline loads. There is no service provider, no config file, no migration, no publish step, and no facade. From a TPM's standpoint that is the whole architectural story: it is disposable glue code that occupies exactly one line in a fields() method and has zero coupling to application domain logic.
That shapes the evaluation. This is not a platform capability, not a differentiator, and not something to design around. It is a rendering convenience for admin-supplied URLs — supplier portals, campaign pages, documentation links, reference records — that converts a raw text column into a clickable anchor inside Nova's index and detail views. The correct mental model is "a snippet we didn't have to write," not "a dependency we are adopting."
Because the field renders whatever the column resolves to, the underlying data model is untouched. URLs stay in ordinary string columns, remain available to filters, exports, and API consumers, and any normalization (adding https://, wrapping in mailto:) can be pushed into resolveUsing so the raw column stays clean. That is architecturally healthy — it means adoption and removal are both one-line changes with no data migration either direction.
Integration feasibility
Feasibility is dominated by a single fact: the last tagged release is 2020-05-17, which places it in the Nova 2.x/3.x, Laravel 6/7, PHP 7.2–7.4 era. Nova 4 (2022) moved to Vue 3 + Inertia and reworked field asset registration; Nova 5 continues that lineage. A Vue 2 component from 2020 will not mount on a Vue 3 Nova, and the package ships no service provider to call Nova::script(...) — which is itself the registration failure on Nova 4+.
So the feasibility question is not "does it work" but "which of two worlds are we in":
composer.json to block install outright. Forcing it with --ignore-platform-reqs risks a blank field at best and a JS error that degrades the entire resource page at worst. That blast radius is the reason to trial on one low-traffic resource rather than batch-rolling across fifteen.The honest build-vs-buy comparison is stark. The in-house alternative — a Link field extending Laravel\Nova\Fields\Field with a displayUsing callback and a ~20-line Vue component — is hours, not sprints, and carries no version risk. Against that, the package offers a head start measured in a single afternoon. For a team already on a supported modern Nova, that trade is close to indefensible; the package's real value there is as a reference implementation for what the in-house field should look like.
Technical risk
Risk clusters into four buckets, roughly in order of severity:
resources/js/components/. The safe pattern regardless of verdict is to show Link on index and detail only, and keep an editable Text/URL field on forms with ->hideFromIndex()->hideFromDetail() — clickable UX without an unverified write path.rel policy (highest in a security review). A field whose entire job is rendering admin-supplied URLs must reject javascript: and data: schemes and must emit rel="noopener noreferrer" whenever target="_blank". Neither is documented. The maintainer has been inactive since 2020, so assume you verify and likely patch. The attack surface is admin-only — but "only admins" is a weak boundary in a support tool where non-engineers paste URLs copied from external sources. Any fix belongs in the Vue component, since sanitizing in resolveUsing is bypassable by anything that sets the attribute later.replace-ing to force install converts a clean "no" into a subtle, intermittent front-end failure. Recommend treating a resolver failure as the finding and stopping there.resources/js/ and dist/js/field.js. Nova loads the compiled bundle. If you fork to patch sanitization, you must run npm run prod and commit the rebuilt dist/js/field.js — and wire that build into CI, because "edited the Vue source, forgot to rebuild" is the most common false-negative in Nova custom fields.Secondary, but worth stating explicitly in scoping: no truncation, no copy button, no icons, no meaningful sortable(), and unreliable filtering/searching through a display field. Long URLs will break index column layout; CSS truncation on the container is your responsibility. If stakeholders expect anything richer than "a clickable anchor," scope has already outgrown the package.
Key questions
composer require khalin/nova-link-field resolve cleanly without --ignore-platform-reqs? The resolver message is the feasibility test.v-text (safe) or v-html (an XSS escape hatch)?<a href="">Label</a> that reloads the Nova page on click and reads as a broken app?dist/js/field.js byte-current with resources/js/, and does the package ship prebuilt assets or expect a build step?rel, target behavior) that the field must satisfy and that we would have to audit anyway?Stack fit
The field is stack-native in the sense that matters: it speaks Nova's Field API, so it slots into fields(Request $request) alongside ID, Text, and friends with no wiring. Use it by swapping an existing Text::make('Website') for Link::make('Website') — the stored column value renders as a clickable anchor.
The value-add over Text::make()->displayUsing(...) is separating clickable text from the stored URL, so a readable label can front a machine URL. Confirm the exact fluent setter name (->label(...) vs ->text(...)) in src/Link.php rather than trusting the short README — the code is ground truth for the $component string, constructor signature, and which setters genuinely exist. If only a single value is exposed, keep the label in a second column and resolve it.
Non-HTTP schemes work the same way because it is just an anchor — mailto: and tel: are useful for support resources where operators want one-click contact. Treat that as a deliberate per-field opt-in; blanket prefixes on a column containing arbitrary values is how you get mailto:https://....
Migration path
There is no migration in the data sense. Adoption is a one-line swap per resource; removal is deleting that line. That symmetry is the strongest argument for a bounded trial: the cost of being wrong is measured in minutes, provided you never let it near a write path you haven't verified.
Recommended path:
laravel.log. Then drop javascript:alert(1) into the column and confirm it does not execute on click.<a href="">. If it doesn't, guard with resolveUsing(fn ($value) => $value ?: null) plus a hideIf, or fix it in the component if you're forking.->onlyOnIndex()->onlyOnDetail() (or ->exceptOnForms() depending on Nova version) and keep the editable field on forms. Verify on a throwaway record before it touches a real one.dist/ committed and the build in CI.Compatibility
Compatibility is the gating constraint and should be assessed per application, not per organization. Treat Nova major version as the primary filter: 2.x/3.x is a candidate; 4/5 is a rebuild. Pin the exact version in composer.json regardless of outcome. Do not widen constraints, do not inline-alias, do not replace — the blast radius of a bad field is small, but the maintenance tax is permanent.
Where compatibility is borderline, the in-house field is not a fallback so much as the primary option with the package as its spec. A Field subclass with displayUsing and a ~20-line Vue component reproduces the behavior with no version exposure, and gives you the sanitization and rel handling by construction rather than by audit.
Sequencing
Sequence to fail cheap and fail early:
src/Link.php, resources/js/components/, dist/js/field.js. Three files, under an hour, and they determine whether the README is trustworthy and whether the form view is safe.javascript: verification loop.app/Nova/Fields/SafeLink.php) so target/rel policy lives in one file. Fifteen sprinkled ->target('_blank') calls are fifteen places to audit the day you discover rel="noopener" isn't being emitted.Text field so operators can still copy long URLs that CSS truncates. Costs nothing, covers the common complaint.Steps 1–3 should complete inside a day. If they don't, that itself is the signal to stop.
Maintenance
Zero dependents, 31 stars, one release, one maintainer, inactive since May 2020. There is no upstream to receive fixes from. The practical consequence: anything you adopt becomes an internal artifact on day one, whether or not you formally fork it. The only real choice is whether you own it explicitly or implicitly.
If adopted, the minimum governance is: pin the exact version, name an owner in the dependency inventory, and put it on the quarterly review with a standing question — "has our Nova version moved past this?" Given the size of the artifact, the honest framing for the roadmap is that the maintenance line item is not the package; it's the fork you will inevitably make to add rel="noopener noreferrer" and scheme validation. Budget the fork, not the install.
Support
This is low-support code by score and by nature — opportunity: 34.63 and a 0.435 composite read as "no ecosystem, no momentum." No maintainer response should be assumed for any defect, including security defects. Internal support burden is real but bounded: the field's failure modes are visible and localized, and the remediation (delete one line, or swap in the in-house field) is trivial.
The support case to plan for is the post-upgrade break. Nova upgrades are the known trigger; when they happen, this field is a candidate for silent front-end failure with no upstream fix coming. That belongs in the Nova upgrade runbook as a named checkpoint, not discovered during the upgrade window.
Scaling
Scaling is not about load. A rendered anchor costs nothing; there is no runtime service, no query, no cache, no external call. The scaling dimension that matters is breadth of adoption — and breadth is where the risk concentrates.
One resource is a cheap experiment. Fifteen resources is fifteen target/rel policy sites to audit, fifteen places a form-view data-loss bug could silently blank a column, and fifteen breakages on the next Nova upgrade. If the decision is to go broad, the standardization wrapper (SafeLink extending the package's Link) is mandatory rather than nice-to-have, because it collapses the audit surface to one file. Even then, recognize that the wrapper is the thing you're maintaining — the package underneath is just a starting point you'll likely replace.
Failure modes
Ranked by what actually hurts:
javascript: / data: scheme execution. Admin-only surface, but admin-only is not a boundary you want to lean on when non-engineers paste URLs from external sources. Mitigation: patch the Vue component; PHP-side sanitization is bypassable.rel="noopener noreferrer" on target="_blank". Low drama, real exposure, trivially fixable at the component level, and exactly the kind of thing you only notice when someone asks.<a href="">. Clicking reloads the current Nova page and reads as a broken app rather than empty data. One-line v-if fix if you're forking; otherwise guard in resolveUsing + hideIf.dist/js/field.js. Your fix isn't in the bundle Nova loads. Add the build to CI the moment you fork.Ramp-up
Ramp-up for a competent Laravel/Nova developer is measured in an afternoon: read three files, swap one line, check three views, run the javascript: test. There is no framework to learn, no configuration surface, no service provider to reason about. The only non-obvious knowledge is Nova-field-specific folklore — that dist/ is what actually loads, that mount errors appear in the console rather than the log, and that display-only fields are a write-path hazard.
The organizational ramp-up is the part to plan. Stakeholders should be told plainly that this is a tactical UI enhancement, not a platform capability: clickable links in the back office, for internal operators, replacing raw text. If anyone starts describing it as a "link management feature," scope has drifted past what a 2020 single-maintainer field can carry — and that is the moment to redirect to the in-house build, which was always the cheaper path for anything beyond the first resource.
How can I help you explore Laravel packages today?