jeffersongoncalves / laravel-short-url
A Laravel package for creating and redirecting short URLs, with caching and an extensible redirect pipeline.
Package info
github.com/jeffersongoncalves/laravel-short-url
pkg:composer/jeffersongoncalves/laravel-short-url
Fund package maintenance!
Requires
- php: ^8.3
- illuminate/contracts: ^12.0|^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- endroid/qr-code: ^6.0
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- setasign/fpdf: ^1.9
Suggests
- endroid/qr-code: Required to generate QR codes for short urls (SVG/PNG export).
- geoip2/geoip2: Required to resolve visitor geolocation with the MaxMind GeoIP driver.
- predis/predis: Required for Redis-buffered visit counters when the phpredis extension isn't available.
- setasign/fpdf: Required in addition to endroid/qr-code for PDF QR code export.
- stancl/tenancy: Required to resolve the current tenant automatically when short-url.tenancy.enabled is on.
This package is auto-updated.
Last update: 2026-08-18 20:31:29 UTC
README
Laravel Short URL
Headless URL-shortening engine for Laravel. Zero dependency on Filament — works standalone in any Laravel app via its Facade, REST API, or console commands.
Why this package
- High throughput. The redirect pipeline is a chain of independent, testable stages (
Illuminate\Pipeline), with the resolved link cached and analytics writes made asynchronous — no external integration failure (GeoIP, Safe Browsing, VPN detection, webhooks) can ever break a redirect. - Contract-driven. Every swappable piece — analytics driver, conversion API dispatcher, DNS verifier, QR code builder, Safe Browsing checker, VPN detector — is an interface under
src/Contracts/, with a default implementation and extensible registries (AnalyticsDriverRegistry,DeepLinkRegistry,PixelProviderRegistry,FilterTypeRegistry,ImporterDriverRegistry). - Minimal dependencies. Only
spatie/laravel-package-toolsandilluminate/contractsare required. GeoIP (MaxMind), QR codes (endroid/qr-code), multi-tenancy (stancl/tenancy) and Redis (predis/predis) are all optional — the package works perfectly without them, each integration guarded byclass_exists/a feature flag. - Multi-language. pt_BR, en and es ship out of the box — no hardcoded strings outside
resources/lang.
Requirements
- PHP 8.3+
- Laravel 12 or 13
Installation
composer require jeffersongoncalves/laravel-short-url
Publish config, migrations and translations:
php artisan vendor:publish --tag="short-url-config" php artisan vendor:publish --tag="short-url-migrations" php artisan vendor:publish --tag="short-url-translations" php artisan migrate
Quick usage
use JeffersonGoncalves\LaravelShortUrl\Facades\ShortUrl; // Create $link = ShortUrl::create(['destination_url' => 'https://example.com/product']); // Fluent $link = ShortUrl::destination('https://example.com/product') ->key('promo25') ->expiresAt(now()->addDays(30)) ->maxVisits(1000) ->password('secret') ->create(); // Resolve $link = ShortUrl::resolve('promo25'); // The ready-to-share URL — custom domain when set, otherwise the app's own host $link->fullUrl(); // https://short.test/promo25
Redirecting itself needs no extra code: any request to GET /{urlKey} already flows through the full pipeline.
Campaign tagging (UTM)
Every link can carry its own utm_source/utm_medium/utm_campaign/utm_term/utm_content — set directly, or from a reusable, tenant-scoped UtmTemplate ("campaign"):
use JeffersonGoncalves\LaravelShortUrl\Models\UtmTemplate; $campaign = UtmTemplate::create(['name' => 'Spring SMS', 'utm_medium' => 'sms', 'utm_campaign' => 'spring-sale']); $link = ShortUrl::destination('https://example.com/product') ->utmTemplate($campaign->id) // fills in unset utm_* fields from the template ->utm(['utm_source' => 'agent-42']) // explicit values always win over the template ->create();
These values are attached to the destination URL on redirect (see strip_utm_from_destination to drop the click's own incoming utm_* first) and become the default attribution recorded on the visit whenever the click itself carries no utm_* of its own — so a link generated specifically for SMS is still correctly attributed even if whoever forwards it doesn't append query params by hand.
Set short-url.utm.required (e.g. ['utm_medium']) to make ShortUrlManager reject creating — or updating — a link that doesn't declare those fields, directly or via a template. Enforced everywhere a link is created (facade, builder, REST API, CSV/Bitly import), not just at the API layer.
Destination types
destination_type is one of single, split, or rules:
// Weighted A/B rotation ShortUrl::create([ 'destination_url' => 'https://example.com/base', // fallback 'destination_type' => 'split', 'rotation_variants' => [ ['url' => 'https://a.test', 'weight' => 50, 'label' => 'A'], ['url' => 'https://b.test', 'weight' => 50, 'label' => 'B'], ], ]); // Conditional targeting, evaluated per request ShortUrl::create([ 'destination_url' => 'https://example.com/base', // used when no rule matches 'destination_type' => 'rules', 'targeting_rules' => [ [ 'conditions' => [ ['type' => 'country', 'value' => 'FR'], ['type' => 'device', 'value' => 'mobile'], ], 'destination' => 'https://example.com/france-mobile', ], ], ]);
A condition's type can be device, platform, browser, country, language, referer, utm_source/utm_medium/utm_campaign, a date/time window, visit_count, vpn, or bot. Conditions default to AND; wrap a group in ['or' => [...]] for OR logic. A rule's destination can itself be a nested split array to combine targeting with rotation, and rotation picks are evaluated with statistical-significance tracking (Z-test) so you can tell when a split has a real winner.
The redirect pipeline
ResolveHost → RateLimit → ResolveShortUrl(cache) → DetectBot → DetectVpnProxy
→ CheckAvailability → RequirePassword → ShowWarning → ResolveDestination
→ BuildFinalUrl → RenderInterstitial → Respond → DispatchTracking
Each stage can short-circuit by returning a Response directly (wrong password, destination warning, expired link, blocked VPN, plan limit). The resolved link is cached ({host}:{key}) and invalidated automatically on saved/deleted.
Feature overview
| Area | Description |
|---|---|
| Redirecting | Configurable Base62 keys, blacklist, uniqueness per domain, 301|302|307|308, single_use, max_visits, expiration with a fallback redirect. |
| Analytics | Asynchronous visit tracking (TrackShortUrlVisitJob), fast-path UA parsing, GeoIP (CDN headers / MaxMind / ip-api), bot detection, IP anonymization (IPv4 /24, IPv6 /48), daily aggregation with configurable retention. StatsAggregator breaks visits down by UTM source/medium/campaign, device, browser, OS, country, referer, and more. |
| Targeting | Nested and|or rules by device, platform, browser, country, language, referer, UTM, date/time window, visit count, VPN, bot. Weighted A/B rotation with statistical significance (Z-test). |
| Custom domains | DNS verification (TXT/CNAME/A), per-domain routing, wildcard support, root redirect. |
| Security | Bcrypt password protection, signed-token warning page, Google Safe Browsing (sync or async blocking), VPN/proxy detection (flag or 403 block), rate limiting, full audit trail (before/after). |
| Compliance | Configurable retention (package-wide or per tenant plan), per-subject data export/deletion (LGPD/GDPR), analytics-only mode (no PII stored). |
| REST API | /api/short-url/v1 (disabled by default), API-key auth with abilities, per-key rate limiting, link CRUD (accepts and returns custom_domain_id, utm_*, utm_template_id, and a ready-to-use short_url), bulk create (up to 500), per-link and global (GET /stats) breakdowns, visits, domains, webhooks, conversions. |
| Webhooks | HMAC-SHA256 + anti-replay timestamp, retries at 10s/60s/300s, manual replay, auto-disable after consecutive failures. |
| External analytics | GA4, Plausible, PostHog, Matomo, Umami, Mixpanel, and Segment built in; AnalyticsDriverRegistry::extend() to add any other provider. |
| Conversion tracking | Server-to-server forwarding to Meta CAPI, Google Enhanced Conversions, TikTok Events API, and LinkedIn CAPI on POST /conversions. |
| Alerts | Z-score anomaly detection against a 7-day baseline, notifications via mail, database, broadcast, Slack, Discord, Telegram, Teams. |
| QR codes | SVG/PNG/PDF/EPS export (via the optional endroid/qr-code), scan tracking (?source=qr). |
| Deep links & pixels | Mobile app opening via custom URL scheme, 10 pre-registered apps, optional AASA/assetlinks serving, retargeting pixels (Meta, Google Ads, TikTok, GA4) with an optional consent banner. |
| Organization | Hierarchical folders, tags, reusable UTM templates ("campaigns"), archiving. |
| Import/Export | Built-in CSV importer, Bitly API v4 as the reference per-provider importer, CSV export via the API. |
| ClickHouse | Alternative VisitRepository driver over ClickHouse's native HTTP interface — same contract, no client library dependency. |
| Multi-tenancy | Fully feature-flagged. Auto-scoped via stancl/tenancy when installed, or a manual config override. Configurable plan limits (links_per_month, domains, retention_days). |
| Link-in-bio | Public pages at /bio/{handle} with blocks (link, text, image, video) and per-block click tracking. |
Recording a conversion always persists it locally, then optionally forwards it server-to-server based on short-url.conversions.driver:
curl -X POST https://your-app.test/api/short-url/v1/conversions \ -H "Authorization: Bearer {api-key}" \ -d '{"url_key": "promo25", "event_name": "purchase", "value": 49.90, "currency": "USD"}'
Global stats — a breakdown across every link a caller can see, not just one — power a dashboard's overview without the consumer computing any aggregation itself:
# All links, optionally narrowed to ?folder_id= or ?tag_id=, and ?from=/?to= curl https://your-app.test/api/short-url/v1/stats -H "Authorization: Bearer {api-key}"
Same StatsAggregator internally — Contracts\StatsAggregator::forShortUrls(array $shortUrlIds) is the programmatic equivalent, for anywhere that isn't the REST API (a Filament dashboard, a scheduled report). It only does the aggregation math; which links belong in the set is always resolved by the caller through ShortUrl's own tenant-scoped query.
Configuration
Every option is documented inline in config/short-url.php. Main groups:
table_prefix, route, key, redirect, cache, tracking (includes clickhouse), domains, branding, security (password, warning, rate limit, VPN, safe browsing), compliance, audit, api, webhooks, analytics, conversions, alerts, notifications, qr, deep_links, pixels, importers, tenancy, bio.
Settings can also be read/written at runtime via Contracts\SettingsRepository, with a declarative schema (schema()) for building dynamic forms in the UI plugin.
Artisan commands
All self-register with the scheduler (packageBooted()), respecting their config toggles:
| Command | Frequency |
|---|---|
short-url:sync-counters |
every minute (when counter buffering is on) |
short-url:aggregate-and-prune |
daily at 02:00 |
short-url:verify-domains |
every 6h |
short-url:check-safe-browsing |
daily |
short-url:detect-anomalies |
hourly |
short-url:send-scheduled-reports |
daily |
short-url:prune-webhook-deliveries |
weekly |
short-url:import {driver} {source} |
manual |
aggregate-and-prune prunes each tenant's visit rows against its own plan retention_days when multi-tenancy is enabled, falling back to the package-wide short-url.tracking.retention_days otherwise.
Public surface (contract with the UI plugin)
ShortUrl::create(array $attributes): ShortUrlModel ShortUrl::destination(string $url): ShortUrlBuilder ShortUrl::resolve(string $key, ?string $host = null): ?ShortUrlModel // ShortUrlModel $shortUrl->fullUrl(): string // ready-to-share link (custom domain or app host) // ShortUrlBuilder, in addition to the setters shown above ->customDomain(?int $customDomainId) ->utmTemplate(int $utmTemplateId) ->utm(array $attributes) // utm_source, utm_medium, utm_campaign, utm_term, utm_content // src/Contracts/ VisitRepository, GeoIpDriver, VpnDetectionDriver, AnalyticsDriver, SafeBrowsingChecker, QrCodeBuilder, StatsAggregator, TargetingResolver, DnsVerifier, SettingsRepository, WebhookDispatcher, ImporterDriver, ConversionApiDispatcher // src/Registries/ FilterTypeRegistry, AnalyticsDriverRegistry, DeepLinkRegistry, PixelProviderRegistry, ImporterDriverRegistry
Testing
composer test # Pest composer analyse # PHPStan (Larastan) level 6 composer format # Pint
CI runs against PHP 8.4 / Laravel 13 on SQLite, MySQL, and PostgreSQL.
AI-assisted development
This package ships a Laravel Boost skill (resources/boost/skills/short-url-development/) and guideline (resources/boost/guidelines/core.blade.php) — if your project uses Boost, an AI assistant picks these up automatically and already knows the facade, contracts, destination types, campaign tagging, and conventions above.
Security
Found a security vulnerability? See SECURITY.md.
Credits
License
MIT. See LICENSE.md for more information.
