mriembau / laravel-urlable
A package to add friendly urls to any Eloquent model.
Requires
- php: ^8.2
- illuminate/database: ^12.0
- illuminate/events: ^12.0
- illuminate/http: ^12.0
- illuminate/queue: ^12.0
- illuminate/routing: ^12.0
- illuminate/support: ^12.0
Requires (Dev)
- laravel/pint: ^1.24
- orchestra/testbench: ^10.6
- spatie/laravel-translatable: ^6.11
This package is not auto-updated.
Last update: 2025-08-26 17:34:26 UTC
README
A Laravel package to add SEO-friendly, multilingual URLs to any Eloquent model.
✅ Features
- Automatic friendly slugs for any Eloquent model
- Multi-language support
- Optional integration with Spatie Laravel Translatable
- Automatic redirects for old slugs
- Event system for custom handling
- Middleware to handle redirects
- Full test coverage (PHPUnit)
📦 Installation
Install the package via Composer:
composer require mriembau/laravel-urlable
⚙️ Configuration
Publish the config and migration files:
php artisan vendor:publish --provider="Mriembau\Urlable\Providers\UrlableServiceProvider"
This will create:
- config/urlable.php
- A migration for the urls table
Run the migration:
php artisan migrate
config/urlable.php
return [ // List of supported locales for URL generation 'locales' => ['ca', 'en'], // Enable or disable the middleware that handles old slug redirects 'enable_middleware' => true, // If true, the package will automatically register a fallback route // to resolve URLs for your models 'resolve_urls' => true, // If true, the default listener will net be registering, and you will // be able to add your own listener 'custom_listener' => false ];
🚀 Usage
1. Add the HasUrl
trait to your model
namespace App\Models; use Illuminate\Database\Eloquent\Model; use Mriembau\Urlable\Traits\HasUrl; class Post extends Model { use HasUrl; protected $fillable = ['title']; // Optionally, customize URL generation fields protected array $urlFields = ['title']; // Optional: define supported locales for this model public array $urlLocales = ['en', 'ca']; }
When you create or update the model, URLs will be automatically generated for each locale.
2. Get the URL for a given locale
$post = Post::create(['title' => 'My first article']); // Get the URL object for the current locale $url = $post->url(); // Get full URL string (e.g., https://yourapp.com/en/my-first-article) echo $url->full_url;
3. Use with Spatie Translatable
If your model uses Spatie's HasTranslations, you can add HasUrlSpatie trait:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Spatie\Translatable\HasTranslations;
use Mriembau\Urlable\Traits\HasUrl;
use Mriembau\Urlable\Traits\HasUrlSpatie;
class Post extends Model
{
use HasTranslations, HasUrl, HasUrlSpatie;
protected $fillable = ['title'];
public array $translatable = ['title'];
}
This way, the package will automatically use the Spatie Translatable methods for each locale.
4. Automatic Redirects for Old Slugs
If a slug changes, the old one will be soft deleted and Laravel's RedirectIfOldSlug
middleware will handle redirects automatically.
By default, the middleware is registered and ready to use. You can apply it to your routes like this:
Route::middleware(['web', 'redirect'])->group(function () { // Your routes... });
This ensures that:
- If a user visits an old URL, they are redirected (301) to the current URL.
- If the URL is current, the request continues normally.
- If no URL is found, a 404 is returned.
🔍 Route Fallback for URLs
🔍 Route Fallback for URLs
The package automatically registers a fallback route to resolve URLs dynamically to the correct model.
How it works:
- When a request does not match any other route, it hits the fallback route.
- The
UrlController
:- Extracts the locale from the first segment (if it matches a configured locale).
- Extracts the remaining path as the slug.
- Looks up the
urls
table for a matching slug and locale.
- If the URL is found:
- If the slug is soft-deleted, it redirects to the current slug (301 redirect).
- Otherwise, it fires the
UrlResolved
event to handle the response.
- If no URL is found, it returns a 404 Not Found.
Note: No manual route registration is needed. The fallback route is only active if
'resolve_urls' => true
in the package config.
📂 Views
The package includes a default view for resolved URLs. You can publish it to customize:
php artisan vendor:publish --tag=urlable-views
After publishing, the view will be located at:
resources/views/vendor/urlable/default.blade.php
You can edit this view to display your model's content or design a custom layout.
Example default content:
{{-- resources/views/vendor/urlable/default.blade.php --}}
<h1>{{ $model->title ?? 'Untitled' }}</h1>
<p>{{ $model->content ?? '' }}</p>
🔔 Events
The package fires a UrlResolved
event whenever a URL is successfully resolved.
Default Listener
By default, the package includes a listener HandleUrlResolved
that will:
- Render the default view (
urlable.default
) if no custom handler is provided. - Call a model's
urlHandler
property if it exists.
This means out-of-the-box, you don't need to create any listener to get basic functionality.
🛠 Custom URL Handler by Model
By default, the package uses the HandleUrlResolved
listener, which renders a generic view.
You can override this behavior for every specific model by adding a $urlHandler property that points to a class with an __invoke method:
Example:
<?php namespace App\Handlers; use Mriembau\Urlable\Models\Url; class PostUrlHandler { public function __invoke($model) { // $model is the Eloquent model instance return view('posts.show', ['post' => $model]); } }
Then in your model:
use Mriembau\Urlable\Traits\HasUrl; class Post extends Model { use HasUrl; public string $urlHandler = \App\Handlers\PostUrlHandler::class; }
When a URL is resolved for this model, the package will automatically call your handler instead of the default view.
Custom Handling / Disabling the Default Listener
If you want full control:
- You can disable the default listener by setting
custom_listener
totrue
the config file. - Then, you can create your own listener for
UrlResolved
. - You need to register your listener in your
EventServiceProvider
:
protected $listen = [ \Mriembau\Urlable\Events\UrlResolved::class => [ // Remove the default listener // \Mriembau\Urlable\Listeners\HandleUrlResolved::class, \App\Listeners\MyCustomUrlResolvedListener::class, ], ];
Example Custom Listener
namespace App\Listeners; use Mriembau\Urlable\Events\UrlResolved; class MyCustomUrlResolvedListener { public function handle(UrlResolved $event) { $model = $event->model; // Custom logic per model return response()->view('my.custom.view', ['model' => $model], 200); } }
✅ Testing
This package uses PHPUnit for automated tests.
Run Tests
./vendor/bin/phpunit
📜 License
This package is open-source and licensed under the MIT License.
© Mriembau