GithubHelp home page GithubHelp logo

permalink's Introduction

Advanced Laravel Permalinks And SEO Management

Build Status

This package allows to create dynamic routes right from database, just like WordPress and other CMS do.

Work In Progress

This package is currently being developed and tested. Would love feedback ❤️.

Roadmap

Documentation

Installation

Install the package

composer require devio/permalink

Run the migrations

php artisan migrate

Getting started

This package handles dynamic routing directly from our database. It also supports slug inheritance so we can easily create routes like this jobs/frontend-web-developer.

Most of the solutions out there are totally bound to models with polymorphic relationships, however that's not flexible at all when dealing with routes without models. This package supports both, model bound routes and plain routes.

Basically, the package stores routes in a permalinks table which contains information about every route:

  • Slug
  • Parent slug
  • Model (if any)
  • Action
  • SEO

Example

Let's review a very basic example to understand how it works:

id slug parent_id parent_for permalinkable_type permalinkable_id action
1 users NULL App\User NULL NULL UserController@index
2 israel-ortuno 1 NULL App\User 1 NULL

It will run the following (this example tries to be as explicit as possible, internally it uses eager loading and some other performance optimizations):

$router->get('users', 'UserController@index');

$router->group(['prefix' => 'users'], function() {
  $user->get('israel-ortuno', User::find(1)->permalinkAction())
});

// Which will produce:
//    /users                UserController@index
//    /users/israelOrtuno   Whatever action configured into the permalinkAction method

Usage

Call the permalink route loader from the boot method from any of your application service providers: AppServiceProvider or RouteServiceProviderp are good examples:

class AppServiceProvider extends ServiceProvider {
    public function boot()
    {
        $this->app->make('permalink')->load();
    }
}

The configuration in the models is pretty simple, simply use the HasPermalinks trait and implement the Permalinkable interface in the models you want to provide the permalink functionality and implement the following methods:

class User extends Model implements \Devio\Permalink\Contracts\Permalinkable {
  use \Devio\Permalink\HasPermalinks;

  /**
   * Get the model action.
   *
   * @return string
   */
  public function permalinkAction()
  {
    return UserController::class . '@show';
  }

  /**
   * Get the options for the sluggable package.
   *
   * @return array
   */
  public function slugSource(): array
  {
    return ['source' => 'permalinkable.name'];
  }
}

This model is now fully ready to work with.

The package uses cviebrock/eloquent-sluggable for the automatic slug generation, so the slugSource method should return an array of options compatible with the eloquent-sluggable options. By just providing the source key should be enough for most cases, but in case you want to update other options, here you can do so. Basically we are pointing that the slug will be generated from the name field of the permalinkable relationship of the permalink model, which will be the current model.

The permalinkAction method should return the default action hanlder for this model, just like if we were setting a route here: Controller@action. You could even return a Closure.

NOTE: Be aware that Laravel cannot cache Closure based routes.

We are now ready to create a new User and the permalink will be automatically generated for us.

$user = User::create(['name' => 'Israel Ortuño']);

$route = $user->route; // 'http://localhost/israel-ortuno

// Permalink (
//  slug:               israel-ortuno
//  parent_id:          NULL
//  permalinkable_type: App\User
//  permalinkable_id:   1
//  action:             NULL
// )

Whenever we visit /israel-ortuno we will be executing UserController@show action. This action will receive the binded model as parameter:

<?php

namespace App\Http\Controllers;

use App\Product;
use Illuminate\Http\Request;

class UserController
{
    public function show(Request $request, User $user)
    {
        return $user;
    }
}

Route names

When routes are loaded, they will be named based on their related model or action.

Route names for permalinks with models

If the route is linked to a model, the route name will be generated by appending the permalink id to the string provided by the permalinkRouteName() method included in the HasPermalinks trait. By default this method returns permalink so all routes will be named as peramlink.{id}. Feel free to override this method and provide any other name you like.

Route names for permalinks without models (only action)

Static permalinks which are not linked to models will receive their names by extracting the controller name and the action from their fully qualified action names: UserController@index will be transformed into user.index.

However, if the permalink action is an "alias" (see Support for morphMap & actionMap), the action name itself will be used to name the route.

Getting the route for a resource

Routes can be resolved as any other Laravel route but just taking into account that they will cannot be resolved by route parameters, they have to be appended nevertheless.

route('permalink.1'); // Get the route of the permalink with id = 1
route('user.5'); // Get the route of the permalink with id = 1

NOTE: The id of the resource is appended to the route name route.{id} (note the .), be careful to NOT pass the key as parameter route('permalink', $id).

When we are manipulating our model, we do not really want to care about the key of its permalink. Despite we could resolve routes just like in the previous example, HasPermalinks trait includes a route accessor which will resolve the fully qualified route for the current entity permalink:

$route = $user->route;
// this is just an alias for:
route('permalink.' . $user->permalink->id);

Cool and neat!

Routes and route groups

If a route is not binded to a model and its action is also NULL, it will be threated as a route group but won't be registered:

id slug parent_id parent_for permalinkable_type permalinkable_id action
1 users NULL App\User NULL NULL NULL
2 israel-ortuno 1 NULL App\User 1 NULL

The example above will not generate a /users route, users will only act as parent of other routes but won't be registered.

Nesting Routes

At this point, you may be wondering why do we need a parent_for column if there's already a parent_id being used as foreign key for parent child nesting.

parent_for is used in order to automatically discover which route will be the parent of a new stored permalink. Using the table above this section, whenever we store a permalink for a App\User model, it will be automatically linked to the permalink 1.

The parent_for will be NULL in most cases.

Creating/updating permalinks

By default, this package comes with an observer class which is linked to the saved event of your model. Whenever a model is saved, this package will create/update accordingly.

NOTE: By default, slugs are only set when creating. They won't be modified when updating unless you explicitly configured the slugSource options to do so.

To disable the automatic permalink management you can simply set the value of the property managePermalinks of your model to false:

class User ... {
    public $managePermalinks = false;
}

This will disable any permalink creation/update and you will be then responisble of doing this manually. As the Permalink model is actually a polymorphic relationship, you can just create or update the permalink as any other relationship:

$user = User::create(['name' => 'Israel Ortuño']);

$user->permalink()->create(); // This is enough unless you want to manually specify an slug or other options

// or if the permalink exists...
$user->permalink->update([...]);

The values for the newly created or updated permalink will be extracted from:

  • A permalink key passed into the creation array.
  • A permalink key from the current request.

Overriding the default action

When a permalink is binded to a model, we will guess which action it points to the permalinkAction method defined in our Permalinkable model. However, we can override this action for a certain model by just specifiying a value into the action column of the permalink record:

id slug permalinkable_type permalinkable_id action
1 madrid App\City 1 App\Http\Controllers\CityController@show

You could update your model via code as any other normal relationship, for example:

$city = City::find(1);

$city->permalink->update(['action' => 'App\Http\Controllers\CityController@show']);

NOTE: The action namespace should always be a fully qualified name unless you are using the actionMap explained below.

Support for morphMap & actionMap

This package provides support for morphMap. As you may know, Laravel ships with a morphMap method to where you can define a relationship "morph map" to instruct Eloquent to use a custom name for each model instead of the class name.

In adition to the morphMap method, this package includes an actionMap static method under the Permalink model where you can also define a relationship "action map" just like the "morph map" but for the permalink actions:

use Illuminate\Database\Eloquent\Relations\Relation;

Relation::morphMap([
    'users' => 'App\User',
    'posts' => 'App\Post',
]);

use Devio\Permalink\Permalink;

Permalink::actionMap([
    'user.index'  => 'App\Http\Controllers\UserController@index',
    'user.show'   => 'App\Http\Controllers\UserController@show',
]);

You can register these maps in the boot method of your AppServiceProvider. The example above will make the permalinkable_type and action columns look much more readable than showing fully qualified class names.

id ... permalinkable_type ... action
1 user user.show

Automatic SEO generation

For SEO tags generation ARCANDEV/SEO-Helper is being used. This package offers a powerful set of tools to manage your SEO meta tags.

Permalink package provides content for ARCANDEV/SEO-Helper form a specific seo column in the permalinks table. This column is supposed to store all the SEO related data for a given permalink in a JSON format:

{
  "meta": {
    "title": "Specific title",                  // The <title>
    "description": "The meta description",      // The page meta description
    "robots": "noindex,nofollow"                // Robots control
  },
  "opengraph":{
    "title": "Specific OG title",               // The og:title tag
    "description": "The og description",        // The og:description tag
    "image": "path/to/og-image.jpg"             // The og:image tag
  },
  "twitter":{
    "title": "Specific Twitter title",          // The twitter:title tag
    "description": "The twitter description",   // The twitter:description tag
    "image": "path/to/og-image.jpg"             // The twitter:image tag
  }
}

In order to have all this content rendered in your HTML you should add the following you your <meta>:

<head>
    {!! seo_helper()->render() !!}
</head>
OR
<head>
    {{ seo_helper()->renderHtml() }}
</head>

Plase visit SEO-Helper – Laravel Usage to know more about what and how to render.

Under the hood, this JSON structure is calling to the different SEO helpers (meta, opengraph and twitter). Let's understand:

{ 
  "title": "Generic title",
  "image": "path/to/image.jpg",
  "description": "Generic description",
  
  "meta": {
    "title": "Default title",
  },
  "opengraph": {
    "image": "path/to/og-image.jpg"
  }
}

This structure will allow you to set a base value for the title in all the builders plus changing exclusively the title for the Meta builder. Same with the image, Twitter and OpenGraph will inherit the parent image but OpenGraph will replace its for the one on its builder.

This will call setTitle from the SeoMeta helper and setImage from the SeoOpenGraph helper. Same would happen with Twitter. Take some time to review these three contracts in order to know all the methods available:

In order to match any of the helper methods, every JSON option will be transformed to studly_case prefixed by set and add, so title will be converted to setTitle and google_analytics to setGoogleAnalytics. How cool is that?

All methods are called via call_user_func_array, so if an option contains an array, every key will be pased as parameter to the helper method. See setTitle or addWebmaster which allows multiple parameters.

Builders

To provide even more flexibility, the method calls are piped through 3 classes (one for each helper) called Builders. These builders are responsible for calling the right method from the ARCANDEV/SEO-Helper package.

If there is a method in this builders matching any of the JSON options, the package will execute that method instead of the default behaviour, which would be calling the method (if exists) from the SEO-Helper package.

Review the MetaBuilder as example. This builder contains a setCanonical method which is basically used as an alias for setUrl (just to be more explicit).

Extending Builders

In order to modify the behaviour of any of these builders, you can create your own Builder which should extend the Devio\Permalink\Contracts\SeoBuilder interface or inherit the Devio\Permalink\Builders\Builder class.

Once you have created your own Builder, just replace the default one in the Container. Add the following to the register method of any Service Provider in your application:

// Singleton or not, whatever you require
$this->app->singleton("permalink.meta", function ($app) use ($builder) { // meta, opengraph, twitter or base
  return new MyCustomBuilder;
  
  // Or if you are inheriting the default builder class
  
  return (new MyCustomBuilder($app->make(SeoHelper::class)));
});

Disabling SEO generation

If you wish to prevent the rendering of any of the three Builders (meta, OpenGraph or Twitter), just set its JSON option to false:

{
  "meta": { },
  "opengraph": false,
  "twitter": false
}

This will disable the execution of the OpenGraph and Twitter builders.

permalink's People

Contributors

crnkovic avatar israelortuno avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.