7 Commits

Author SHA1 Message Date
Bartłomiej Janczak
c842096a22 style: fix qr-designer spacing when Tailwind helper classes are unbuilt 2026-06-01 12:35:49 +02:00
Bartłomiej Janczak
7035e5b84b Update README.md 2026-06-01 12:35:17 +02:00
Bartłomiej Janczak
3b97ad3d00 add LICENSE file 2026-06-01 12:17:49 +02:00
Bartłomiej Janczak
a5f5afa198 docs: improve README layout with centered 3D logo and Shields.io status badges 2026-06-01 12:08:14 +02:00
Bartłomiej Janczak
429896d8e3 docs: complete README documentation for all settings, builders, env parameters, and bot filtering 2026-06-01 12:06:04 +02:00
Bartłomiej Janczak
985aa43155 docs: update README with navigationLabel and navigationIcon, fix composer require username 2026-06-01 10:28:39 +02:00
Bartłomiej Janczak
cd9a4a1620 feat: add navigationLabel() and navigationIcon() to plugin API 2026-06-01 10:27:02 +02:00
5 changed files with 232 additions and 37 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Bartek Janczak
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

170
README.md
View File

@@ -1,4 +1,17 @@
# Filament Short URL
<p align="center">
<img src="https://cdn3d.iconscout.com/3d/premium/thumb/url-3d-icon-png-download-9292351.png" width="180" alt="Filament Short URL Logo">
</p>
<h1 align="center">Filament Short URL</h1>
<p align="center">
<a href="https://packagist.org/packages/janczakb/filament-short-url"><img src="https://img.shields.io/packagist/v/janczakb/filament-short-url.svg?style=flat-square" alt="Latest Version"></a>
<a href="https://packagist.org/packages/janczakb/filament-short-url"><img src="https://img.shields.io/packagist/l/janczakb/filament-short-url.svg?style=flat-square" alt="License"></a>
<a href="https://packagist.org/packages/janczakb/filament-short-url"><img src="https://img.shields.io/packagist/dt/janczakb/filament-short-url.svg?style=flat-square" alt="Total Downloads"></a>
<a href="https://github.com/janczakb/filament-short-url/stargazers"><img src="https://img.shields.io/github/stars/janczakb/filament-short-url.svg?style=flat-square" alt="GitHub Stars"></a>
<a href="https://github.com/janczakb/filament-short-url/issues"><img src="https://img.shields.io/github/issues/janczakb/filament-short-url.svg?style=flat-square" alt="GitHub Issues"></a>
<a href="https://github.com/janczakb/filament-short-url/actions"><img src="https://img.shields.io/badge/tests-passing-success.svg?style=flat-square" alt="Tests"></a>
</p>
A professional, high-performance **Short URL Manager** plugin for [Filament v5](https://filamentphp.com). Built from scratch with cutting-edge practices, proxy resistance, offline Geo-IP engines, and zero external shortening API dependencies.
@@ -6,17 +19,17 @@ A professional, high-performance **Short URL Manager** plugin for [Filament v5](
## Features
- 🔗 **Short URL Generation**custom or auto-generated collision-free base62 keys.
- 🌍 **Multiple Geo-IP Drivers** — offline detection using local MaxMind databases, edge-provided CDN headers (Cloudflare, CloudFront), or fallback API integration.
- 📈 **Real-Time Statistics Dashboard**sortable log of visits with cached aggregate performance metrics (browsers, devices, OS, referers, country maps, timeline charts).
- 🎨 **QR Code Designer**live design canvas (sizes, dot styles, gradient configurations, and background transparency triggers) with instant SVG download.
-**Ultra-Fast Redirects**redirects resolve in milliseconds; analytical tasks and GA4 payloads are processed asynchronously via Laravel Queue jobs.
- 🎯 **Google Analytics 4 Measurement Protocol**native server-side event tracking, bypassing browser-side AdBlockers completely.
- ⚙️ **Reactive UTM Campaign Builder**double-way synchronized UTM builder panel inside the Filament form, syncing seamlessly with destination URLs in real-time.
- 🔒 **Single-Use & Expirable Links** — automatically deactivates links after a single visit or at a specified date/time.
- ➡️ **Query Parameter Forwarding**dynamically appends incoming client query parameters (e.g. ad clicks, discount codes) to the final destination URL.
- 🛠️ **Dedicated Settings Interface** — full administrator control panel inside Filament to manage routing, Geo-IP, and GA4 credentials without editing `.env` files.
- 💻 **Fluent Developer Builder**static Model builder patterns and trace tagging support.
- 🔗 **Short URL Generation**Create custom links or let the system auto-generate collision-free Base62 keys.
- 🌍 **Multiple Geo-IP Drivers** High-speed offline detection using local MaxMind databases, edge-provided CDN headers (Cloudflare, CloudFront, generic), or fallback API integration.
- 📈 **Real-Time Statistics Dashboard**Track visits in real-time with cached aggregate metrics (countries, devices, browsers, operating systems, referrers, traffic charts, and maps).
- 🎨 **SVG QR Code Designer**Built-in interactive design canvas in Filament to customize dot styles, margins, gradient coloring, and background transparency with instant SVG download.
-**Ultra-Fast Redirects**Redirections resolve in milliseconds. Analytical tasks, event dispatching, and GA4 payloads are processed asynchronously via Laravel Queue jobs.
- 🎯 **Google Analytics 4 server-side tracking**Native integration with the GA4 Measurement Protocol to bypass client-side AdBlockers completely.
- ⚙️ **Dual-way UTM Campaign Builder**Built-in form builder synchronizes UTM parameters with your destination URLs in real-time (two-way binding).
- 🔒 **Link Rules & Expiry** — Disable links manually, set expiration dates, or activate single-use links that deactivate automatically after the first click.
- ➡️ **Query Parameter Forwarding**Dynamically forward client query parameters (e.g. ad tokens, discount codes) to the destination URL.
- 🛠️ **Dedicated Settings GUI** — Manage global configuration (routing, Geo-IP, GA4, cache) directly inside the Filament panel without modifying files or `.env` files.
- 💻 **Fluent Developer Builder**Native model query builder pattern and robust programmatic generation APIs.
---
@@ -33,7 +46,7 @@ A professional, high-performance **Short URL Manager** plugin for [Filament v5](
Install the package via Composer:
```bash
composer require bjanczak/filament-short-url
composer require janczakb/filament-short-url
```
Publish and run the database migrations:
@@ -47,27 +60,27 @@ php artisan migrate
## Publishing Package Assets
The package is built with standard publishing tags to let developers easily customize and override the default assets within the host application directory:
Customize and override views, translations, or configuration files by publishing the package assets:
### 1. Publish Config File
Copies the default config file to `config/filament-short-url.php` for code-level customization:
Copies the default config file to `config/filament-short-url.php`:
```bash
php artisan vendor:publish --tag=filament-short-url-config
```
### 2. Publish Translation Files
Copies localization files to `lang/vendor/filament-short-url/` so you can modify or add new languages (Polish and English included by default):
Copies localization files to `lang/vendor/filament-short-url/` (English and Polish included by default):
```bash
php artisan vendor:publish --tag=filament-short-url-translations
```
### 3. Publish Blade Views & Templates
Copies the dashboard components, charts, and QR designer templates to `resources/views/vendor/filament-short-url/` to completely override the styling:
Copies the dashboard components, charts, and QR designer templates to `resources/views/vendor/filament-short-url/`:
```bash
php artisan vendor:publish --tag=filament-short-url-views
```
### 4. Publish Everything at Once
### 4. Publish All Assets
```bash
php artisan vendor:publish --provider="Bjanczak\FilamentShortUrl\FilamentShortUrlServiceProvider"
```
@@ -86,28 +99,87 @@ public function panel(Panel $panel): Panel
return $panel
->plugins([
FilamentShortUrlPlugin::make()
->navigationGroup('Tools') // optional
->navigationSort(50), // optional
->navigationGroup('Marketing') // optional — sidebar group name
->navigationLabel('Short Links') // optional — override menu item name
->navigationIcon('heroicon-o-link') // optional — override menu icon
->navigationSort(50), // optional — sort order in sidebar
]);
}
```
### Navigation Configuration Options
All fluent methods on the plugin are optional. If not called, the plugin falls back to defaults or translation files:
| Method | Default | Description |
|--------|---------|-------------|
| `navigationGroup(string)` | `null` | Groups the resource menu item under a sidebar section. |
| `navigationLabel(string)` | `'Short URLs'` | Overrides the menu item display name. |
| `navigationIcon(string)` | `heroicon-o-link` | Overrides the Heroicon used in the sidebar. |
| `navigationSort(int)` | `50` | Controls the sort order within the navigation list. |
---
## Usage in Code
## Global Settings GUI
### 1. Programmatic Creation via Fluent Builder
You can easily create, configure, and append tracking tags to short URLs programmatically using the fluent builder:
The package comes with a built-in admin settings dashboard. You can access it by clicking the **Settings** action button on the top-right header of the Short URLs resource.
Settings are stored dynamically in `storage/app/filament-short-url-settings.json` and immediately override config defaults.
The settings panel allows you to configure:
### 1. General Routing & Queueing
* **Route Prefix**: The slug prepended to short URLs (e.g. `s` for `/s/{key}`).
* **Default Redirect Status**: Choose `302 (Found / Temporary)` or `301 (Moved Permanently)`.
* *Note: `302` is highly recommended for analytics accuracy because browsers cache `301` redirects, skipping subsequent logs.*
* **Key Length**: Default character count (base62) for auto-generated keys (default: `6`).
* **Queue Connection**: Define the Laravel queue connection (e.g. `redis`, `database`, `sync`) used for processing visit analytics asynchronously.
### 2. Geo-IP Country Detection
Toggle country tracking and select from three drivers:
* **Headers** (Edge Resolution): Automatically detects client country using standard edge headers (e.g. Cloudflare's `CF-IPCountry`, AWS CloudFront's `CloudFront-Viewer-Country`, or generic proxies).
* **MaxMind** (Offline Resolution): Reads from a local GeoIP2 database (such as the free GeoLite2-Country database).
* **IP-API** (Online Fallback): Makes an external API call to `ip-api.com` with configurable timeout.
### 3. Google Analytics 4 (GA4) Integration
Sends server-side `short_url_visit` hits using the **GA4 Measurement Protocol API**. This bypasses browser-side AdBlockers entirely.
* **GA4 API Secret**: Create this secret in Google Analytics under `Admin -> Data Streams -> Measurement Protocol API secrets`.
* **Firebase App ID / Measurement ID**: The target analytics stream identifier.
---
## Configuration Reference (.env)
You can also pre-configure all parameters via your `.env` file:
| Environment Variable | Config Path | Default | Description |
|---|---|---|---|
| `SHORT_URL_PREFIX` | `route_prefix` | `'s'` | URL prefix for short URL redirects. |
| `SHORT_URL_GEO_IP` | `geo_ip.enabled` | `true` | Globally enable/disable Geo-IP tracking. |
| `SHORT_URL_GEO_IP_DRIVER` | `geo_ip.driver` | `'headers'` | Geo-IP resolver driver (`headers`, `maxmind`, `ip-api`). |
| `SHORT_URL_MAXMIND_DB` | `geo_ip.maxmind.database_path` | `database_path('geoip/GeoLite2-Country.mmdb')` | Path to local MaxMind db. |
| `SHORT_URL_STATS_CACHE_TTL` | `geo_ip.stats_cache_ttl` | `300` | Caching TTL in seconds for dashboard charts. |
| `SHORT_URL_QUEUE` | `queue_connection` | `'sync'` | Queue connection for recording visits. |
| `SHORT_URL_CACHE_TTL` | `cache_ttl` | `3600` | Redirection model caching TTL (set to `0` to disable). |
| `GA4_API_SECRET` | `ga4.api_secret` | `null` | Google Analytics 4 Measurement Protocol API Secret. |
| `FIREBASE_APP_ID` | `ga4.firebase_app_id` | `null` | Google Analytics 4 Firebase App ID (or uses Measurement ID). |
---
## Programmatic Usage (Fluent Builder)
You can programmatically generate, customize, and trace short URLs anywhere in your code using the static `destination` builder on the `ShortUrl` model:
```php
use Bjanczak\FilamentShortUrl\Models\ShortUrl;
$shortUrl = ShortUrl::destination('https://example.com/very/long/url')
->urlKey('promo2026') // optional, auto-generated if empty
->notes('Spring campaign promo')
->singleUse() // deactivates after first visit
->forwardQueryParams() // forward incoming visitor query strings
->withTracing([ // dynamically filters and appends UTM parameters
$shortUrl = ShortUrl::destination('https://example.com/products/promo')
->urlKey('promo2026') // Optional, auto-generated base62 key if omitted
->notes('Spring social promo')
->singleUse() // Deactivates the link automatically after the first visit
->forwardQueryParams() // Appends incoming URL query strings to destination
->expiresAt(now()->addDays(7)) // Automatic link expiration
->trackVisits(true) // Enable or disable click logs
->withTracing([ // Dynamically filter and append UTM parameters
'utm_source' => 'linkedin',
'utm_medium' => 'social',
'utm_campaign' => 'spring_sale',
@@ -115,11 +187,36 @@ $shortUrl = ShortUrl::destination('https://example.com/very/long/url')
])
->create();
// Output the public short URL string
echo $shortUrl->getShortUrl(); // https://yourapp.com/s/promo2026
// Get the full URL string
echo $shortUrl->getShortUrl(); // https://yourdomain.com/s/promo2026
```
### 2. Standard Service Injection
### Fluent Builder API Reference
| Method | Argument Type | Description |
|--------|---------------|-------------|
| `urlKey()` | `string` | Sets a custom redirection key (slug). |
| `notes()` | `string` | Appends internal notes for administrators. |
| `singleUse()` | `bool` (default `true`) | Makes the short URL expire immediately after the first successful click. |
| `forwardQueryParams()` | `bool` (default `true`) | Forwards client-side incoming parameters (e.g. `?gclid=xxx`) to the final target. |
| `expiresAt()` | `DateTimeInterface\|Carbon\|null` | Automatically sets a timestamp after which the URL returns a `410 Gone` error. |
| `trackVisits()` | `bool` (default `true`) | Toggles statistical logging for this link. |
| `withTracing()` | `array` | Appends non-empty tracking parameters (like UTM codes) directly to the target URL. |
| `create()` | `void` | Persists the model to the database and returns the `ShortUrl` instance. |
---
## Model Helpers & Service
### ShortUrl Model Methods
* `isActive(): bool` — Checks if the short URL is enabled and within its activation/expiration timestamps.
* `isExpired(): bool` — Checks if the URL has passed its expiration date.
* `getShortUrl(): string` — Resolves the complete URL string.
* `incrementVisits(bool $isUnique = false): void` — Atomically increments visit counts.
* `getCachedStats(): array` — Returns cached key metrics for dashboard charts.
### Injecting the Service
For standard creations, you can inject `ShortUrlService`:
```php
use Bjanczak\FilamentShortUrl\Services\ShortUrlService;
@@ -135,7 +232,7 @@ $shortUrl = $service->create([
## Events
You can listen to short URL visits dynamically in your application (e.g. for real-time alerts or external webhooks):
The package fires the `ShortUrlVisited` event inside the queue job. You can listen to it to trigger custom logic, Slack notifications, or syncing external CRMs:
```php
use Bjanczak\FilamentShortUrl\Events\ShortUrlVisited;
@@ -143,12 +240,17 @@ use Illuminate\Support\Facades\Event;
Event::listen(ShortUrlVisited::class, function (ShortUrlVisited $event) {
$event->shortUrl; // The ShortUrl model instance
$event->visit; // The ShortUrlVisit model instance (contains resolved client IP, browser, OS, country, etc.)
$event->visit; // The ShortUrlVisit model (contains resolved IP, country, device, browser, OS, referrer)
});
```
---
## Visitor Filtering (Bot Detection)
Visits from scrapers, search bots, and web crawlers are automatically ignored to keep stats clean. The system matches user agents against a custom list (including Googlebot, Bingbot, Applebot, Facebook, Twitter, and generic curl/http request clients).
---
## License
MIT

View File

@@ -108,6 +108,15 @@
background-color:#f3f4f6;
}
@keyframes qr-spin { to { transform: rotate(360deg); } }
.qr-margin-top {
margin-top: 12px !important;
}
.qr-space-y > div {
margin-top: 12px !important;
}
.qr-space-y > div:first-child {
margin-top: 0 !important;
}
</style>
{{-- Preload QR library as soon as the tab renders --}}
@@ -297,7 +306,7 @@
</div>
{{-- Single color picker --}}
<div x-show="colorMode === 'solid'" x-transition style="display:block" class="mt-3">
<div x-show="colorMode === 'solid'" x-transition style="display:block" class="mt-3 qr-margin-top">
<span class="qr-label">{{ __('filament-short-url::default.qr_label_color') }}</span>
<div class="flex items-center gap-2">
<div class="qr-color-swatch qr-color-picker" :style="'background:'+fgColor">
@@ -311,7 +320,7 @@
</div>
{{-- Gradient pickers --}}
<div x-show="colorMode === 'gradient'" x-transition style="display:none" class="mt-3 space-y-3">
<div x-show="colorMode === 'gradient'" x-transition style="display:none" class="mt-3 space-y-3 qr-space-y qr-margin-top">
<div class="grid grid-cols-2 gap-2">
<div>
<span class="qr-label">{{ __('filament-short-url::default.qr_label_from') }}</span>
@@ -390,7 +399,7 @@
<span class="qr-toggle-thumb"></span>
</button>
</div>
<div x-show="eyeConfigEnabled" x-transition style="display:none" class="mt-3 space-y-3">
<div x-show="eyeConfigEnabled" x-transition style="display:none" class="mt-3 space-y-3 qr-space-y qr-margin-top">
<div>
<span class="qr-label">{{ __('filament-short-url::default.qr_label_eye_square_style') }}</span>
<select x-model="eyeSquareStyle" class="qr-select">

View File

@@ -26,6 +26,15 @@ class ShortUrlResource extends Resource
public static function getNavigationLabel(): string
{
try {
$label = FilamentShortUrlPlugin::get()->getNavigationLabel();
if ($label) {
return $label;
}
} catch (\Throwable) {
// Ignore
}
return __('filament-short-url::default.navigation_label');
}
@@ -41,6 +50,20 @@ class ShortUrlResource extends Resource
// ─── Plugin-aware navigation overrides ───────────────────────────────────
public static function getNavigationIcon(): string|\BackedEnum|null
{
try {
$icon = FilamentShortUrlPlugin::get()->getNavigationIcon();
if ($icon) {
return $icon;
}
} catch (\Throwable) {
// Ignore
}
return static::$navigationIcon;
}
public static function getNavigationGroup(): string|\UnitEnum|null
{
try {

View File

@@ -12,6 +12,10 @@ class FilamentShortUrlPlugin implements Plugin
protected ?int $navigationSort = null;
protected ?string $navigationLabel = null;
protected ?string $navigationIcon = null;
protected ?string $routePrefix = null;
// ─── Factory ─────────────────────────────────────────────────────────────
@@ -67,6 +71,8 @@ class FilamentShortUrlPlugin implements Plugin
/**
* Set the navigation sort order.
*
* @example FilamentShortUrlPlugin::make()->navigationSort(50)
*/
public function navigationSort(int $sort): static
{
@@ -79,4 +85,38 @@ class FilamentShortUrlPlugin implements Plugin
{
return $this->navigationSort;
}
/**
* Override the navigation label (menu item name).
*
* @example FilamentShortUrlPlugin::make()->navigationLabel('Short Links')
*/
public function navigationLabel(string $label): static
{
$this->navigationLabel = $label;
return $this;
}
public function getNavigationLabel(): ?string
{
return $this->navigationLabel;
}
/**
* Override the navigation icon.
*
* @example FilamentShortUrlPlugin::make()->navigationIcon('heroicon-o-link')
*/
public function navigationIcon(string $icon): static
{
$this->navigationIcon = $icon;
return $this;
}
public function getNavigationIcon(): ?string
{
return $this->navigationIcon;
}
}