pdewit/nova-external-url
Laravel Nova field for adding clickable external URL links to resources. Display a link or button in index and detail views, point it at any URL, and optionally open it in a new tab. Easy to configure in Nova resource classes for quick access to related external pages.
Check the Nova constraint before you type composer require. The last release is November 2018, which predates Nova 3 entirely. Open the package's composer.json and read the laravel/nova constraint against your app's installed Nova version. If it caps at ^1.0/^2.0 and you're on Nova 3+, stop here — the compiled Vue 2 assets will not register under Nova 4/5's Vite/Vue 3 build, and Nova ships Laravel\Nova\Fields\URL natively anyway.
If the constraint does match:
composer require pdewit/nova-external-url
php artisan vendor:publish --provider="Pdewit\NovaExternalUrl\FieldServiceProvider"
php artisan nova:publish
Confirm what you actually got. The description calls this "a field," but the package README has been read as describing sidebar/menu links. Two files settle it in under a minute:
src/*.php — does the class extend Laravel\Nova\Fields\Text (stored string, resolveUsing works), a bare Field (display-only), or is it a tool/menu provider (registered in NovaServiceProvider::tools() or menu(), not in a resource)?resources/js/field.js — what does it emit for a value? Does it set target="_blank" and rel="noopener noreferrer"? What does it render for null?First use case — a detail-page jump link. A Company resource with a stored website, or a record with a vendor ID you want to open in the vendor's own dashboard:
use Pdewit\NovaExternalUrl\ExternalUrl;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
public function fields(Request $request)
{
return [
ID::make()->sortable(),
Text::make('Name'),
ExternalUrl::make('Website')
->onlyOnDetail()
->rules('nullable', 'url', 'max:2048'),
];
}
If it turns out to be the menu variant, the same problem is solved by registering it in NovaServiceProvider::menu() with a label and URL, and you skip resource changes entirely.
Derive the URL; don't store it. The highest-value day-to-day pattern is storing an identifier and resolving the link at render time, so you generate a stable navigation target instead of duplicating URLs into your schema:
ExternalUrl::make('Open in Stripe')
->resolveUsing(fn ($value, $resource) => $resource->stripe_customer_id
? "https://dashboard.stripe.com/customers/{$resource->stripe_customer_id}"
: null)
->onlyOnDetail();
Keep the raw ID visible as a copyable Text field beside it — operators will need to paste it somewhere the link doesn't go.
Decide editability per resource, explicitly. For "follow only" links — the common case — chain ->readonly() or ->exceptOnForms() so the target is application-controlled rather than hand-typed into Nova. Make it a deliberate choice in every fields() method instead of inheriting the default.
Default to detail-only. A column of long URLs wrecks index layout, and Nova's row-click-through to the detail page fights with an inner anchor. Use ->onlyOnDetail() or ->hideFromIndex() as your default; surface the link on index only when operators genuinely click through from list views.
Handle the empty state deliberately. Combine ->nullable() with an explicit decision: hide the field, render an em-dash, or render nothing. A dangling <a href="#"> or a literal null on screen is the most common complaint with display-only link fields. If the package gives you no hook, compute null from resolveUsing or use ->hideFromDetail(fn () => blank($resource->website)).
Always pair with validation rules. The field renders whatever string it's given, so ->rules('nullable', 'url', 'max:2048') belongs at every call site. If you also render it through an HTML path, you own the escaping.
Menu variant: centralise and gate. If it's the sidebar integration, keep link definitions in a config file or one array in NovaServiceProvider rather than scattering them, and gate each entry by authorization — Nova's menu is not a permission boundary, but a finance dashboard link shouldn't sit in front of support agents.
A blank column is almost always assets, not code. The classic failure is a missing public/vendor symlink after a fresh composer install, or a stale config cache. Run php artisan nova:publish and php artisan optimize:clear before you start reading the Vue source.
It validates nothing — that's your job. There is no url rule baked in. Add rules at every call site, and never pass user-supplied text through an HTML-rendering path without sanitising it first.
target="_blank" needs rel="noopener noreferrer". A 2018-era component almost certainly omits it. Check resources/js/field.js; if it's missing, patch it locally — it's a one-line change with a real reverse-tabnabbing implication.
resolveUsing fields don't sort or filter correctly. Nova sorts and filters on the underlying column, not your computed value. If you need ordering on the displayed URL, sort the source column and accept it, or drop the field from index views where sorting matters.
Row-click conflicts on index. Nova makes the whole table row clickable through to the detail page. An inner <a> needs @click.stop to avoid navigating to the resource instead of the external site. If the component doesn't do this, treat it as a reason to keep the field detail-only.
Upgrading Nova is the real cost. Nova 4/5's native Laravel\Nova\Fields\URL renders as a link, and MenuSection/MenuItem covers the sidebar case. A Vue 2 component from 2018 will not run under Nova 4's Vite build without a rewrite. On 4+, use the native field and delete this dependency; on 2.x where it works, pin the commit SHA, name an owner, and budget the replacement into the next upgrade.
Prefer vendoring over depending. With one PHP class and one Vue component, copying it into nova-components/external-url/ costs an afternoon and removes the abandoned-dependency risk entirely — you get the rel fix, your own validation defaults, and no Composer constraint to fight next upgrade. Check the license before you copy.
How can I help you explore Laravel packages today?