Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Link Field Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and let the resolver be your first test. There is no service provider, no config file, no publish step, and no migration — the package is a single Field subclass plus a compiled Vue component that Nova loads through its own asset mechanism. That means the entire integration surface is one line in composer.json and one line in fields().

composer require khalin/nova-link-field

Run it without --ignore-platform-reqs and without widening constraints. The last tagged release is 2020-05-17, which puts it in the Nova 2.x/3.x, Laravel 6/7, PHP 7.2–7.4 window. If the resolver refuses on your stack, that is the finding — don't force it (see Gotchas). If it resolves, you're on an older Nova and can proceed.

Where to look first, in this order:

  1. src/Link.php — the field class. Ground truth for the $component string, the constructor signature, and which fluent setters actually exist (label? text? target?). The README is short and may lag the code.
  2. resources/js/components/ — the Vue component. Two questions: does it render an editable input on forms, or is it display-only? And does it emit the label via v-text (safe) or v-html (an XSS escape hatch)?
  3. dist/js/field.js — what Nova actually loads at runtime. If dist/ is older than resources/js/, you are running whatever was compiled at that tag, not what you just read.

First use case: one resource, one column, three views. Take a resource with a full URL stored in a text column and swap the existing field:

use Khalin\NovaLinkField\Link;

public function fields(Request $request)
{
    return [
        ID::make()->sortable(),
        Text::make('Name'),
        Link::make('Website'),   // stored column rendered as a clickable anchor
    ];
}

Then check index, detail, and form individually. A field this small frequently nails the detail view and breaks the other two — if the form view shows a static anchor instead of an input, you've found the field's single most important limitation in about two minutes. That's the whole smoke test, and it's worth doing before you touch a real resource.


Implementation Patterns

Label independent of URL

The main value-add over Text::make()->displayUsing(...) is separating readable text from the stored URL:

// Same value serves as both URL and label
Link::make('Website'),

// Human label, machine URL — confirm the exact setter in src/Link.php
Link::make('Website')->label('Visit site'),   // or ->text(...) depending on version

If the package only exposes one value, keep the label in a second column and resolve it in the field rather than denormalizing the label into the URL column.

Deriving URLs that aren't stored as full URLs

The field renders whatever the column resolves to, so normalize in the field and leave the raw column clean for filters, exports, and API consumers:

Link::make('Domain')
    ->resolveUsing(fn ($value) => $value ? 'https://' . $value : null),

This is also where you strip trailing whitespace from copy-pasted values — a very common source of broken anchors in admin tools.

Non-HTTP schemes

Because it's just an anchor, mailto: and tel: work identically, which is genuinely useful for support resources where operators want one-click contact:

Link::make('Support Email')
    ->resolveUsing(fn ($value) => $value ? "mailto:{$value}" : null),

Treat this as a deliberate per-field opt-in. Blanket-prefixing a column that already contains arbitrary values is how you get mailto:https://....

The safe write pattern: Link on read, Text on write

If the component turns out to be display-only, do not expose it on forms. A display-only field in a form view can submit an empty or stale value and silently overwrite the column on save. The pattern that gives you the clickable UX with zero write-path risk:

Link::make('Website')
    ->onlyOnIndex()
    ->onlyOnDetail(),

Text::make('Website')
    ->onlyOnForms(),

You get clickable links in the two read views and a proven, boring input on the edit view. This should be your default until you have personally confirmed the component hydrates an input.

Pair with a copy affordance

Long URLs are painful to click-and-drag, and index columns truncate. On detail views, park a copyable plain text field next to the link:

Link::make('Website')->onlyOnDetail(),
Text::make('Website')->onlyOnDetail()->copyable(),

Costs nothing, covers operators who need the string rather than the navigation.

Standardize across resources

If you end up with more than two or three of these, wrap it so the target/rel policy lives in one file and Nova upgrades touch one place:

// app/Nova/Fields/SafeLink.php
class SafeLink extends \Khalin\NovaLinkField\Link
{
    public function __construct($name, $attribute = null, $resolveCallback = null)
    {
        parent::__construct($name, $attribute, $resolveCallback);
        $this->target('_blank');
        // rel handling goes here — or in the Vue component if the PHP side can't express it
    }
}

Fifteen scattered ->target('_blank') calls are fifteen places to audit the day you discover rel="noopener" isn't being emitted.

Day-to-day verification loop

Nova fields are hard to assert in PHPUnit because rendering happens in Vue, so the practical loop is manual and takes about 90 seconds:

  1. Log in as a user who can actually see the resource.
  2. Check the index row, the detail view, and the form view.
  3. Open the browser console — Vue mount failures surface here, not in laravel.log.
  4. Drop a javascript:alert(1) value into the column and confirm it does not execute on click.

Gotchas and Tips

Form-view data loss is the real risk

A display-only field on a form is worse than no field: Nova may render it non-editable while the form still submits a value for the attribute. If the component doesn't hydrate an input, you can blank the column on save. Verify on a throwaway record before shipping to a real resource. If you can't confirm it, use the onlyOnIndex/onlyOnDetail + Text::make()->onlyOnForms() split above and move on.

URL scheme handling is undocumented and unmaintained

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". Whether this package does either is not documented, and the maintainer has been inactive since May 2020. Assume you must verify, and expect to patch.

The attack surface is admin-only, but "only admins" is not a boundary you want to lean on in a support tool where non-engineers paste URLs from external tickets and emails.

The fix belongs in the Vue component, not the PHP class. Sanitization in resolveUsing is bypassable by anything that sets the attribute later (accessors, other fields, fillUsing), and the DOM is where the decision actually matters.

Constraint resolution failure on modern Nova

Nova 4 (2022) moved to Vue 3 + Inertia and reworked field asset registration; Nova 5 continues that. Expect composer.json to block install outright. If someone forces it with --ignore-platform-reqs, expect one of three outcomes:

  • field renders blank,
  • the Vue 2 component fails to mount,
  • a JS error that degrades the entire resource page, not just the field.

That third case is why you trial this on one low-traffic resource, never in a batch.

dist/ may not match resources/js/

Nova loads the compiled bundle. If you fork to patch sanitization, you must run the package's build and commit the new dist/js/field.js. Check whether the package ships prebuilt assets or expects a build step — if the latter, add it to CI. Editing the Vue source and forgetting to rebuild is the single most common "my fix didn't work" in Nova custom fields.

The blank-value case

An empty column typically produces <a href="">Label</a>, which on click reloads the current Nova page and reads as a broken app rather than empty data. Guard it:

Link::make('Website')
    ->resolveUsing(fn ($value) => $value ?: null)
    ->hideIf(fn ($resource) => empty($resource->website)),

A one-line v-if in the component is the cleaner fix if you're forking anyway.

Debugging a field that renders nothing

Work down this list in order:

  1. Browser console first — mount errors live here.
  2. Confirm the $component string in src/Link.php matches the component name registered in the JS.
  3. Confirm dist/js/field.js exists and is registered. Nova 4+ uses Nova::script(...) in a service provider — this package ships no service provider, which is itself the failure on Nova 4+.
  4. Dump the resolved value: temporarily ->displayUsing(fn ($v) => var_dump($v)) to confirm the column is populated and the resolver actually fires.
  5. Check the value isn't null — see the blank-value case above.

No truncation, no copy button, no icons, no sorting

This is an anchor and nothing more. Long URLs will break index column layout; CSS truncation on the container is on you. It almost certainly isn't meaningfully sortable(), and filtering/searching a URL column through a display field is unreliable — keep the raw column available for filters.

Dependency posture

Zero dependents, 31 stars, one release, one maintainer. There is no upstream to receive fixes from; anything you adopt becomes an internal artifact. If you proceed:

  • Pin the exact version in composer.json.
  • Name an owner in your dependency inventory and put it on a quarterly review.
  • Prefer a fork via internal VCS if you need the sanitization patch — you'll be maintaining it either way, so own it explicitly.
  • Recognize the build alternative is hours, not sprints. A Link field extending Laravel\Nova\Fields\Field with a displayUsing callback plus a ~20-line Vue component reproduces this behavior with zero version risk. Treat the package as a reference implementation for exactly that.

When to bail

If the install doesn't resolve cleanly on your Nova version, stop. Don't widen constraints, don't inline-alias, don't replace. The blast radius of a bad field is small — delete one line from fields() — but the maintenance tax is permanent, and the in-house replacement is genuinely a few hours of work.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment