carlson/nova-link-field
A Laravel Nova field that renders a clickable link directly in your resource index and detail views. Pair a display label with a target URL, optionally open in a new tab, and skip raw HTML or custom components for simple link display.
Architecture fit. carlson/nova-link-field is a presentational Nova field and nothing more: it takes an existing string column and renders it as an <a href> in index and detail views. There is no migration, no storage layer, no model concern, no service contract. Architecturally that is the cheapest possible footprint — one attribute in, one anchor out — which means the decision to adopt it is not an architecture decision at all. It is a UI polish decision scoped to the admin panel, and it should be funded and tracked as internal tooling (DevEx/maintenance), not as a product capability. Roadmap weight is effectively zero: no customer-facing surface, no revenue impact, no differentiation.
Integration feasibility. This is conditional and the condition is binary. The last release was 2020-02-26, placing it in the Nova 2/3, Vue 2, Laravel Mix era. On Nova 3 or earlier it is plausibly a drop-in. On Nova 4/5 the frontend is Vue 3 and a 2020-era compiled field bundle will not mount — the dangerous variant is a clean Composer install followed by a silently blank column, not a hard failure. Feasibility therefore reduces to one question that must be answered from the lockfile, not from the README: which Nova major are we on? If it is 4+, the package is a specification, not a dependency — roughly 60–120 lines to reimplement first-party.
Technical risk.
| Risk | Severity | Note |
|---|---|---|
| Unmaintained (5-year gap, 22 stars, 0 dependents) | High | Nobody has validated this against a real Nova upgrade; we would be first |
| Silent failure mode (blank column) | Medium | Component-name mismatch or missing dist/js/field.js looks identical to "no data" |
Stored XSS via href |
High | A link field is the textbook admin-panel XSS surface; 2020-era components routinely omit rel="noopener noreferrer" |
| Auto-discovery assumptions | Low | Packages of this vintage predate universal Nova asset discovery |
| Setter API drift | Low | text() / target() / openInNewTab() spellings vary; grep src/Link.php before writing twenty fields |
Key questions. (1) What Nova version is actually pinned in composer.lock? (2) Can the value ever be written by a non-superadmin, an import, or an upstream API — i.e., is href hostile input in our context? (3) Do we need validation, link-health checks, redirect resolution, or multiple links per record? If yes to any, this is the wrong tool. (4) Who owns the forked code in our repo, and is that person named before we start?
Stack fit. Clean and narrow: PHP Field subclass plus a small Vue component. The idiomatic usage is a display/editor pair on the same attribute — Text::make('…')->onlyOnForms()->rules('nullable','url') for editing, Link::make('…')->text('Open asset')->target('_blank')->exceptOnForms() for display. That split sidesteps the question of whether the field even supports editing and keeps a plain input box for operators pasting new URLs.
Recommended path: fork-and-vendor, not composer require. With zero dependents and no release in five years, the honest model is copy, not depend. Two routes, both under a day: clone the source into app/Nova/Fields/Link, or scaffold with php artisan nova:field acme/link and port the behaviour. Either way the field lands under our CI, our code review, and our composer audit, and it never appears in the manifest as an unmaintained transitive dependency that surfaces during a dependency audit at the worst possible moment.
Compatibility gate before any work. Run composer why-not carlson/nova-link-field to see what in the lockfile blocks it. Then read three files in order — src/Link.php (the entire API surface), resources/js/components/IndexField.vue and DetailField.vue (component name and href binding), and dist/js/field.js (is the compiled asset actually committed, or are we rebuilding JS from a 2020 Node toolchain?). Under 30 minutes, and it is the cheapest insurance in the engagement.
Sequencing.
http, https, mailto) enforced on save, rel="noopener noreferrer" on any target="_blank", :href attribute binding rather than v-html or string concatenation, plus a feature test asserting escaping of javascript: and quote-breaking payloads.Where to put logic. Keep the field dumb. If a URL is computed (storefront, DAM, ticketing system), build it in a model accessor where it is testable in PHP, and leave Link::make('Column') as the whole field definition. This also documents the integration seam: the Nova resource mirrors an object owned elsewhere, and the link makes that ownership visible without building a sync.
Maintenance. The real cost is ownership transfer, and it is small but permanent: a forked Nova field is code we carry across Nova upgrades. A first-party field of 60–120 lines is a one-place fix at upgrade time; a vendored 2020 bundle is the same fix plus the archaeology. If we take the dependency route instead, we inherit an unmaintained package with no upstream and no CVE feed — acceptable only inside an internal admin panel, and only with the scheme allowlist and rel hardening applied.
Support. There is no vendor to escalate to. Support means whoever forked it. That is a named-owner requirement, not a hope. Set expectations accordingly: bug reports route to the platform team, not GitHub.
Scaling. Effectively free. It is a presentational field over a string column — no queries, no background work, no storage growth, no per-record runtime cost. The only scaling surface is the number of resources carrying it, which is a review-discipline question, not a capacity one.
Failure modes and detection. The dominant risk is silent breakage. A blank index column almost always means a component-name mismatch or that dist/js/field.js never loaded — check the component name, then DevTools Network on a Nova index page, then the console for an "unknown custom element" warning. Works locally but blank in staging points at asset path, Nova::script(), or the package discovery cache (bootstrap/cache/packages.php, then package:discover). Console errors immediately after a Nova upgrade are expected — field bundles are the most common Nova upgrade breakage. The second failure mode is security: a javascript: or data: value saved into the column and rendered unescaped. Both are cheap to test for and expensive to discover in production.
Ramp-up. Under a day end to end for a mid-level Laravel engineer: the source is a few dozen lines of PHP and two small Vue components. The knowledge is not portable or scarce, which is itself a reason not to over-invest in the dependency.
Recommendation. Timebox the compatibility spike. On Nova 3 or earlier with a genuinely internal blast radius, vendor the field, harden it, ship it to one resource, and widen slowly. On Nova 4/5, skip the package entirely and build the field first-party from the source as a spec — same behaviour, no dead dependency, and Nova upgrades stay a one-place fix. Do not let it drift into composer.json as a casual or transitive dependency. Both endings are fine; the ambiguous middle is not.
How can I help you explore Laravel packages today?