mriembau/laravel-urlable

A package to add friendly urls to any Eloquent model.

1.0.0 2025-08-25 18:45 UTC

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:

  1. When a request does not match any other route, it hits the fallback route.
  2. 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.
  3. 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.
  4. 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:

  1. You can disable the default listener by setting custom_listener to true the config file.
  2. Then, you can create your own listener for UrlResolved.
  3. 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