janczakb/filament-short-url
A Filament plugin for creating and managing short URLs from your admin panel. Generate compact links, organize them in a dedicated resource, and simplify sharing long URLs with an integrated workflow.
Prereqs (verify before you install). This is a Filament panel plugin, not a standalone service. You need an existing Laravel 10/11 + Filament v3 + Livewire app on PHP 8.1+, a relational database, and — realistically — a queue worker and a cache store. If you are not already a Filament shop, stop: the cost of adoption is the cost of adopting Filament, not the cost of this package.
Step 0 — read the LICENSE file first. The repo reports NOASSERTION, meaning GitHub could not map the license to a known SPDX identifier. That is a five-minute legal read that gates everything else. Do not spend engineering time before it is cleared.
Install sequence.
composer require janczakb/filament-short-url on a throwaway branch.php artisan vendor:publish and inspect the available tags (config, migrations, assets). Publish the config and migrations; publish assets only if the QR/UI needs them.PanelProvider:
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugins([
\Janczakb\FilamentShortUrl\ShortUrlPlugin::make(),
]);
}
(Namespace and plugin class name are illustrative — confirm against vendor/janczakb/ after install.)php artisan migrate against a scratch database first, not your shared dev DB.php artisan route:list | grep -i short.Where to look first. Because this is a 9-star, zero-dependent package, your first hour should be source reading, not docs reading. Open, in order:
config/filament-short-url.php — the redirect route prefix, path/domain config, GA4 settings, any cache TTL.routes/ — the public redirect route definition. This is the production hot path; understand exactly what it matches.database/migrations/ — the links table and the analytics table. Note indexes on the short-code column and the shape of the analytics rows (IP, UA, referrer).src/ — the Filament Resource (the CRUD UI), the redirect controller/middleware, and wherever GA4 is called.First use case. Create one link in the resource UI, hit the short URL in an incognito tab, and confirm a 302 with the right Location. Then check whether an analytics row landed and whether a GA4 call fired. If any of those three steps requires you to queue, cache, or patch something, you have found the real scope of the integration on day one.
Keep the redirect route decoupled from the panel provider. The admin CRUD UI and the public redirect are two separate concerns bundled in one artifact. Register the redirect route from your app's own service provider (or a dedicated routes file) rather than implicitly via the panel, so an admin panel outage or a Filament upgrade failure does not take down every published short link. This is the single highest-leverage structural decision you will make.
Cache the code→destination lookup. A short-link redirect is read-heavy and latency-critical. Do not accept a DB query per click. Wrap lookups:
$destination = Cache::remember(
"short-url:{$code}",
now()->addMinutes(10),
fn () => ShortLink::where('slug', $code)->value('destination')
);
Invalidate the key in the resource's afterSave/model observer when a destination is edited. Measure p99 yourself — the package publishes no benchmark, and the "high-performance" claim is unverified.
Queue the GA4 calls (and fail open). Server-side GA4 tracking implies outbound HTTP to Google. If it is synchronous, your redirect latency becomes a function of Google's availability. Move it to a job, give it a short timeout, and swallow failures — a dropped analytics event is cheap, a dropped redirect is not:
dispatch(new SendGa4Event($link, $request->userAgent()))->onQueue('analytics');
Gate access with Filament authorization, not the plugin. The resource appears in the panel; control who sees it with a policy or canViewAny() on the resource class, consistent with how you gate your other resources. Do not assume the plugin ships a permission model.
Import existing links via a model/artisan command, not the UI. If you are migrating off Bitly/Shlink or a hand-rolled redirect table, write a one-off php artisan short-urls:import command that inserts directly into the package's model. Editing hundreds of links through the Filament form is a waste of an afternoon.
Extend the resource rather than fork it (initially). Add columns, filters, and custom table actions by subclassing the package's Resource class or using Filament's table()/form() extension points in your own panel. Fork only when you have hit something you cannot override — and when you do, pin the fork and log the divergence.
Test the redirect path as a feature, not the UI. The UI is Filament's problem. Yours is:
public function test_short_code_redirects(): void
{
$link = ShortLink::factory()->create(['slug' => 'promo', 'destination' => 'https://example.com']);
$this->get('/s/promo')->assertRedirect('https://example.com');
}
Also add a test that a missing code returns 404 (not 500), and one that a disabled/expired link behaves as you intend.
Staging vs production analytics. Point GA4 at a test property in staging, or disable it entirely outside production. Otherwise your staging click-throughs pollute the campaign numbers the marketing team is reporting on.
License ambiguity is the first real blocker. NOASSERTION is not necessarily restrictive, but it is not auto-clearable. If legal comes back with terms that do not grant redistribution or patent rights, anything you build on top is a rewrite. Resolve this before writing integration code.
Filament's next major will probably break you. A single-maintainer plugin with 9 stars and 0 dependents will likely lag Filament v3 → v4. Assume fork-and-own: vendor the package, name an internal owner, and keep the redirect route independent of the panel so an admin-side break is degraded operations, not an outage.
Route collisions. The redirect route is typically a catch-all under a prefix like /s/{code}. Check it does not shadow an existing route, and confirm the prefix is configurable in the published config. Move it if it collides — do not rename your app's routes around a plugin.
301 vs 302 matters more than you think. If the package (or your config) issues a 301, browsers cache the redirect indefinitely and an edited destination silently does not take effect for anyone who already clicked. Prefer 302 for editable links, and be explicit about it in the code review.
Open-redirect surface. A short-link manager is, by construction, a redirector. Validate destination schemes (reject javascript:, data:, and ideally anything but http/https), and consider an allowlist of destination domains if your team is small and the link set is controlled. If the resource lets users type arbitrary targets, that is a phishing vector against your own brand domain.
MySQL index length on utf8mb4. The short-code column needs to be small enough to index cleanly on older MySQL. If migrations fail with an index-length error, reduce the column length or set an index prefix — this is a classic, unglamorous failure when you point a published migration at a real database.
Analytics grows unbounded. Every click is a row. Add a retention job (e.g. prune rows older than 90 days) on day one, not when the table has 40M rows. Also decide now how you store IP and user agent: hash or truncate the IP at write time. Stored IPs + UAs + referrers are personal data under GDPR, and the retention/DSAR obligation is yours, not the maintainer's.
Cache invalidation is the bug you will actually ship. If you cache the lookup, editing a destination in the admin must bust the key. Wire it into the model's saved/deleted events and write a test for it — this is the kind of defect that surfaces as "the link still goes to the old page" three days later.
Never migrate:fresh in a shared environment. Upstream updates can drift the schema. Review published migrations as diffs before applying, and treat the package's migrations as part of your schema you now own.
Debugging checklist when a redirect misbehaves: php artisan route:list | grep short, confirm the published config is actually the one loaded (php artisan config:show), clear the route and config caches, check the cache store for a stale key, and tail the log for the redirect controller (many packages log misses or exceptions quietly). If a link 404s, check soft-delete/status scoping before assuming a routing problem.
Set expectations with stakeholders. This is not a drop-in dependency — it is a small subsystem you will operate. The Filament-native admin UX is the genuine differentiator; if your team does not live in Filament daily, a dedicated shortener (e.g. Shlink) wins on operational isolation and you should say so early.
How can I help you explore Laravel packages today?