From 927be4ea777636fe72a9bfd28cb52fb1061eca76 Mon Sep 17 00:00:00 2001 From: ludo Date: Mon, 19 Feb 2024 23:51:32 +0100 Subject: [PATCH] add seo --- app/Console/Commands/FixSlug.php | 25 +++ .../Controllers/Shop/ArticleController.php | 6 + .../Admin/Shop/StoreVariationPost.php | 1 - app/Models/Shop/Article.php | 31 ++++ app/Repositories/Shop/Articles.php | 8 +- build/css/site.css | 1 + composer.json | 4 +- config/health.php | 121 ++++++++++++++ config/sluggable.php | 158 ++++++++++++++++++ ...1_22_224325_create_shop_articles_table.php | 7 +- ...22_224325_create_shop_categories_table.php | 4 +- ...2_224325_create_shop_price_lists_table.php | 2 +- .../views/Admin/Shop/Articles/form.blade.php | 7 +- .../Admin/Shop/Variations/form.blade.php | 8 - .../Shop/Articles/partials/article.blade.php | 74 ++++---- .../partials/sliderByShelve.blade.php | 59 ++++--- .../Shop/layout/partials/megamenu.blade.php | 6 +- routes/Shop/Articles.php | 3 +- 18 files changed, 440 insertions(+), 85 deletions(-) create mode 100644 app/Console/Commands/FixSlug.php create mode 100644 config/health.php create mode 100644 config/sluggable.php diff --git a/app/Console/Commands/FixSlug.php b/app/Console/Commands/FixSlug.php new file mode 100644 index 00000000..bc27b87d --- /dev/null +++ b/app/Console/Commands/FixSlug.php @@ -0,0 +1,25 @@ +slug = null; + $article->update(['name' => $article->name]); + } + + return 0; + } +} diff --git a/app/Http/Controllers/Shop/ArticleController.php b/app/Http/Controllers/Shop/ArticleController.php index 78c7ea11..07ac0665 100644 --- a/app/Http/Controllers/Shop/ArticleController.php +++ b/app/Http/Controllers/Shop/ArticleController.php @@ -16,6 +16,12 @@ class ArticleController extends Controller return response()->json(Articles::autocomplete($str)); } + public function showBySlug($slug) + { + $id = Articles::getIDBySlug($slug); + return $id ? $this->show($id) : view('errors.404'); + } + public function show($id) { $data = [ diff --git a/app/Http/Requests/Admin/Shop/StoreVariationPost.php b/app/Http/Requests/Admin/Shop/StoreVariationPost.php index 23074c87..7c2a950c 100644 --- a/app/Http/Requests/Admin/Shop/StoreVariationPost.php +++ b/app/Http/Requests/Admin/Shop/StoreVariationPost.php @@ -17,7 +17,6 @@ class StoreVariationPost extends FormRequest 'package_id' => 'required', 'quantity' => 'required', 'unity_id' => 'required', - 'weight' => 'required', ]; } } diff --git a/app/Models/Shop/Article.php b/app/Models/Shop/Article.php index 71f2590d..5cc0acb6 100644 --- a/app/Models/Shop/Article.php +++ b/app/Models/Shop/Article.php @@ -6,6 +6,7 @@ use App\Models\Botanic\Specie; use App\Models\Botanic\Variety; use App\Traits\Model\HasComments; use App\Traits\Model\Imageable; +use Cviebrock\EloquentSluggable\Sluggable; use Fico7489\Laravel\EloquentJoin\Traits\EloquentJoin; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -16,6 +17,8 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Database\Eloquent\SoftDeletes; use Kirschbaum\PowerJoins\PowerJoins; use Laravel\Scout\Searchable; +use RalphJSmit\Laravel\SEO\Support\HasSEO; +use RalphJSmit\Laravel\SEO\Support\SEOData; use Rinvex\Categories\Traits\Categorizable; use Rinvex\Tags\Traits\Taggable; use Spatie\MediaLibrary\HasMedia; @@ -29,10 +32,12 @@ class Article extends Model implements HasMedia use EloquentJoin; use HasComments; use HasRelationships; + use HasSEO; use Imageable; use Powerjoins; use RevisionableTrait; use Searchable; + use Sluggable; use SoftDeletes; use Taggable; use UserStamps; @@ -177,6 +182,11 @@ class Article extends Model implements HasMedia return $productId ? $query->where($this->table.'.product_id', $productId) : $query; } + public function scopeBySlug($query, $slug) + { + return $slug ? $query->where($this->table.'.slug', $slug) : $query; + } + public function scopeByTag($query, $tagId) { return $tagId ? $query->whereHas('tags', function ($query) use ($tagId) { @@ -227,4 +237,25 @@ class Article extends Model implements HasMedia 'description' => html_entity_decode(strip_tags($description)), ]; } + + public function getDynamicSEOData(): SEOData + { + // $pathToFeaturedImageRelativeToPublicPath = // ..; + + // Override only the properties you want: + return new SEOData( + title: $this->name, + description: $this->description, + // image: $pathToFeaturedImageRelativeToPublicPath, + ); + } + + public function sluggable(): array + { + return [ + 'slug' => [ + 'source' => 'name' + ] + ]; + } } diff --git a/app/Repositories/Shop/Articles.php b/app/Repositories/Shop/Articles.php index 66ca6259..6c7dee60 100644 --- a/app/Repositories/Shop/Articles.php +++ b/app/Repositories/Shop/Articles.php @@ -3,11 +3,9 @@ namespace App\Repositories\Shop; use App\Models\Shop\Article; -use App\Models\Shop\Merchandise; use App\Repositories\Botanic\Species; use App\Repositories\Botanic\Varieties; use App\Repositories\Core\Comments; -use App\Repositories\Core\Tag; use App\Traits\Model\Basic; use App\Traits\Repository\Imageable; use Illuminate\Support\Str; @@ -27,6 +25,11 @@ class Articles return $export; } + public static function getIDBySlug($slug) + { + return Article::bySlug($slug)->first()->id; + } + public static function getOffersGroupedByNature($id, $saleChannelId = false) { $articleIds = ArticleSiblings::getSiblingsIds($id); @@ -178,6 +181,7 @@ class Articles 'product_name' => $article->product->name, 'parent_name' => trim(str_replace($article->product->name, '', $article->name)), 'offers' => $article->offers->toArray(), + 'slug' => $article->slug, ]; } diff --git a/build/css/site.css b/build/css/site.css index 86060568..d1e5f9f5 100644 --- a/build/css/site.css +++ b/build/css/site.css @@ -204,6 +204,7 @@ div.megamenu ul.megamenu li.megamenu.level1 #navbarContent > ul > li:hover, #navbarContent > ul > li.show, #navbarContent > ul > li.active { border-bottom: 3px solid #F2B90F!important; + margin-bottom: -3px; } #navbarContent > ul > li > a diff --git a/composer.json b/composer.json index 97c3ddb8..9d520d7b 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ ], "license": "proprietary", "require": { - "php": "^7.4|^8.0", + "php": "^8.1", "akaunting/laravel-apexcharts": "^3.0", "alexisgeneau/mailvalidate": "dev-master", "arrilot/laravel-widgets": "^3.14", @@ -22,6 +22,7 @@ "coduo/php-humanizer": "^4.0", "composer/composer": "^2.6", "cornford/googlmapper": "^3.4", + "cviebrock/eloquent-sluggable": "^9.0", "darryldecode/cart": "^4.2", "datatables/datatables": "^1.10", "ddzobov/laravel-pivot-softdeletes": "^2.1", @@ -117,6 +118,7 @@ "fossbarrow/laravel-phpcs": "dev-main", "kevincobain2000/laravel-erd": "^1.6", "kitloong/laravel-migrations-generator": "^6.11", + "laracraft-tech/laravel-schema-rules": "^1.3", "laravel/pint": "^1.13", "mockery/mockery": "^1.6", "nunomaduro/collision": "^7.10", diff --git a/config/health.php b/config/health.php new file mode 100644 index 00000000..dedf638a --- /dev/null +++ b/config/health.php @@ -0,0 +1,121 @@ + [ + Spatie\Health\ResultStores\EloquentHealthResultStore::class => [ + 'connection' => env('HEALTH_DB_CONNECTION', env('DB_CONNECTION')), + 'model' => Spatie\Health\Models\HealthCheckResultHistoryItem::class, + 'keep_history_for_days' => 5, + ], + + /* + Spatie\Health\ResultStores\CacheHealthResultStore::class => [ + 'store' => 'file', + ], + + Spatie\Health\ResultStores\JsonFileHealthResultStore::class => [ + 'disk' => 's3', + 'path' => 'health.json', + ], + + Spatie\Health\ResultStores\InMemoryHealthResultStore::class, + */ + ], + + /* + * You can get notified when specific events occur. Out of the box you can use 'mail' and 'slack'. + * For Slack you need to install laravel/slack-notification-channel. + */ + 'notifications' => [ + /* + * Notifications will only get sent if this option is set to `true`. + */ + 'enabled' => true, + + 'notifications' => [ + Spatie\Health\Notifications\CheckFailedNotification::class => ['mail'], + ], + + /* + * Here you can specify the notifiable to which the notifications should be sent. The default + * notifiable will use the variables specified in this config file. + */ + 'notifiable' => Spatie\Health\Notifications\Notifiable::class, + + /* + * When checks start failing, you could potentially end up getting + * a notification every minute. + * + * With this setting, notifications are throttled. By default, you'll + * only get one notification per hour. + */ + 'throttle_notifications_for_minutes' => 60, + 'throttle_notifications_key' => 'health:latestNotificationSentAt:', + + 'mail' => [ + 'to' => 'your@example.com', + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + ], + + 'slack' => [ + 'webhook_url' => env('HEALTH_SLACK_WEBHOOK_URL', ''), + + /* + * If this is set to null the default channel of the webhook will be used. + */ + 'channel' => null, + + 'username' => null, + + 'icon' => null, + ], + ], + + /* + * You can let Oh Dear monitor the results of all health checks. This way, you'll + * get notified of any problems even if your application goes totally down. Via + * Oh Dear, you can also have access to more advanced notification options. + */ + 'oh_dear_endpoint' => [ + 'enabled' => false, + + /* + * When this option is enabled, the checks will run before sending a response. + * Otherwise, we'll send the results from the last time the checks have run. + */ + 'always_send_fresh_results' => true, + + /* + * The secret that is displayed at the Application Health settings at Oh Dear. + */ + 'secret' => env('OH_DEAR_HEALTH_CHECK_SECRET'), + + /* + * The URL that should be configured in the Application health settings at Oh Dear. + */ + 'url' => '/oh-dear-health-check-results', + ], + + /* + * You can set a theme for the local results page + * + * - light: light mode + * - dark: dark mode + */ + 'theme' => 'light', + + /* + * When enabled, completed `HealthQueueJob`s will be displayed + * in Horizon's silenced jobs screen. + */ + 'silence_health_queue_job' => true, +]; diff --git a/config/sluggable.php b/config/sluggable.php new file mode 100644 index 00000000..2b1495b7 --- /dev/null +++ b/config/sluggable.php @@ -0,0 +1,158 @@ +name; + * + * Or it can be an array of fields, like ["name", "company"], which builds a slug from: + * + * $model->name . ' ' . $model->company; + * + * If you've defined custom getters in your model, you can use those too, + * since Eloquent will call them when you request a custom attribute. + * + * Defaults to null, which uses the toString() method on your model. + */ + + 'source' => null, + + /** + * The maximum length of a generated slug. Defaults to "null", which means + * no length restrictions are enforced. Set it to a positive integer if you + * want to make sure your slugs aren't too long. + */ + + 'maxLength' => null, + + /** + * If you are setting a maximum length on your slugs, you may not want the + * truncated string to split a word in half. The default setting of "true" + * will ensure this, e.g. with a maxLength of 12: + * + * "my source string" -> "my-source" + * + * Setting it to "false" will simply truncate the generated slug at the + * desired length, e.g.: + * + * "my source string" -> "my-source-st" + */ + + 'maxLengthKeepWords' => true, + + /** + * If left to "null", then use the cocur/slugify package to generate the slug + * (with the separator defined below). + * + * Set this to a closure that accepts two parameters (string and separator) + * to define a custom slugger. e.g.: + * + * 'method' => function( $string, $sep ) { + * return preg_replace('/[^a-z]+/i', $sep, $string); + * }, + * + * Otherwise, this will be treated as a callable to be used. e.g.: + * + * 'method' => array('Str','slug'), + */ + + 'method' => null, + + /** + * Separator to use when generating slugs. Defaults to a hyphen. + */ + + 'separator' => '-', + + /** + * Enforce uniqueness of slugs? Defaults to true. + * If a generated slug already exists, an incremental numeric + * value will be appended to the end until a unique slug is found. e.g.: + * + * my-slug + * my-slug-1 + * my-slug-2 + */ + + 'unique' => true, + + /** + * If you are enforcing unique slugs, the default is to add an + * incremental value to the end of the base slug. Alternatively, you + * can change this value to a closure that accepts three parameters: + * the base slug, the separator, and a Collection of the other + * "similar" slugs. The closure should return the new unique + * suffix to append to the slug. + */ + + 'uniqueSuffix' => null, + + /** + * What is the first suffix to add to a slug to make it unique? + * For the default method of adding incremental integers, we start + * counting at 2, so the list of slugs would be, e.g.: + * + * - my-post + * - my-post-2 + * - my-post-3 + */ + 'firstUniqueSuffix' => 2, + + /** + * Should we include the trashed items when generating a unique slug? + * This only applies if the softDelete property is set for the Eloquent model. + * If set to "false", then a new slug could duplicate one that exists on a trashed model. + * If set to "true", then uniqueness is enforced across trashed and existing models. + */ + + 'includeTrashed' => false, + + /** + * An array of slug names that can never be used for this model, + * e.g. to prevent collisions with existing routes or controller methods, etc.. + * Defaults to null (i.e. no reserved names). + * Can be a static array, e.g.: + * + * 'reserved' => array('add', 'delete'), + * + * or a closure that returns an array of reserved names. + * If using a closure, it will accept one parameter: the model itself, and should + * return an array of reserved names, or null. e.g. + * + * 'reserved' => function( Model $model) { + * return $model->some_method_that_returns_an_array(); + * } + * + * In the case of a slug that gets generated with one of these reserved names, + * we will do: + * + * $slug .= $separator + "1" + * + * and continue from there. + */ + + 'reserved' => null, + + /** + * Whether to update the slug value when a model is being + * re-saved (i.e. already exists). Defaults to false, which + * means slugs are not updated. + * + * Be careful! If you are using slugs to generate URLs, then + * updating your slug automatically might change your URLs which + * is probably not a good idea from an SEO point of view. + * Only set this to true if you understand the possible consequences. + */ + + 'onUpdate' => false, + + /** + * If the default slug engine of cocur/slugify is used, this array of + * configuration options will be used when instantiating the engine. + */ + 'slugEngineOptions' => [], + +]; diff --git a/database/migrations/shop/2024_01_22_224325_create_shop_articles_table.php b/database/migrations/shop/2024_01_22_224325_create_shop_articles_table.php index afb58b53..ca929e06 100644 --- a/database/migrations/shop/2024_01_22_224325_create_shop_articles_table.php +++ b/database/migrations/shop/2024_01_22_224325_create_shop_articles_table.php @@ -15,15 +15,16 @@ return new class extends Migration { Schema::create('shop_articles', function (Blueprint $table) { $table->increments('id'); - $table->unsignedInteger('article_nature_id')->nullable()->index('family_id'); + $table->unsignedInteger('article_nature_id')->nullable()->index('article_nature_id'); $table->string('product_type', 100)->nullable(); $table->unsignedInteger('product_id')->nullable(); $table->string('ref', 50)->nullable()->unique('ref'); $table->string('name')->nullable(); + $table->string('slug')->nullable()->unique('slug'); $table->unsignedInteger('hash')->nullable(); $table->text('description')->nullable(); - $table->tinyInteger('visible')->nullable(); - $table->unsignedTinyInteger('homepage')->nullable(); + $table->tinyInteger('visible')->nullable()->index('visible'); + $table->unsignedTinyInteger('homepage')->nullable()->index('homepage'); $table->unsignedSmallInteger('created_by')->nullable(); $table->unsignedSmallInteger('updated_by')->nullable(); $table->unsignedSmallInteger('deleted_by')->nullable(); diff --git a/database/migrations/shop/2024_01_22_224325_create_shop_categories_table.php b/database/migrations/shop/2024_01_22_224325_create_shop_categories_table.php index 12bc4f1a..b6975970 100644 --- a/database/migrations/shop/2024_01_22_224325_create_shop_categories_table.php +++ b/database/migrations/shop/2024_01_22_224325_create_shop_categories_table.php @@ -18,8 +18,8 @@ return new class extends Migration $table->unsignedInteger('category_id')->nullable()->index('category_id'); $table->string('name', 50)->nullable(); $table->text('description')->nullable(); - $table->tinyInteger('visible')->nullable(); - $table->unsignedTinyInteger('homepage')->nullable(); + $table->tinyInteger('visible')->nullable()->index('visible'); + $table->unsignedTinyInteger('homepage')->nullable()->index('homepage'); $table->unsignedSmallInteger('created_by')->nullable(); $table->unsignedSmallInteger('updated_by')->nullable(); $table->unsignedSmallInteger('deleted_by')->nullable(); diff --git a/database/migrations/shop/2024_01_22_224325_create_shop_price_lists_table.php b/database/migrations/shop/2024_01_22_224325_create_shop_price_lists_table.php index fb3904d8..8d8e52f7 100644 --- a/database/migrations/shop/2024_01_22_224325_create_shop_price_lists_table.php +++ b/database/migrations/shop/2024_01_22_224325_create_shop_price_lists_table.php @@ -17,7 +17,7 @@ return new class extends Migration $table->increments('id'); $table->unsignedInteger('tariff_id')->nullable()->index('tariff_id'); $table->unsignedInteger('sale_channel_id')->nullable()->index('sale_channel_id'); - $table->unsignedTinyInteger('status_id')->nullable(); + $table->unsignedTinyInteger('status_id')->nullable()->index('status_id'); $table->string('name', 100)->nullable(); $table->unsignedSmallInteger('created_by')->nullable(); $table->unsignedSmallInteger('updated_by')->nullable(); diff --git a/resources/views/Admin/Shop/Articles/form.blade.php b/resources/views/Admin/Shop/Articles/form.blade.php index 195fee3d..daa9087b 100644 --- a/resources/views/Admin/Shop/Articles/form.blade.php +++ b/resources/views/Admin/Shop/Articles/form.blade.php @@ -1,4 +1,9 @@ -{{ Form::open(['route' => 'Admin.Shop.Articles.store', 'id' => 'article-form', 'autocomplete' => 'off', 'files' => true]) }} +{{ Form::open([ + 'route' => 'Admin.Shop.Articles.store', + 'id' => 'article-form', + 'autocomplete' => 'off', + 'files' => true, +]) }} @include('Admin.Shop.Articles.partials.characteristics') {{ Form::close() }} diff --git a/resources/views/Admin/Shop/Variations/form.blade.php b/resources/views/Admin/Shop/Variations/form.blade.php index 757a07d9..0abd51fb 100644 --- a/resources/views/Admin/Shop/Variations/form.blade.php +++ b/resources/views/Admin/Shop/Variations/form.blade.php @@ -34,14 +34,6 @@ -
- @include('components.form.input', [ - 'name' => 'weight', - 'value' => $variation['weight'] ?? false, - 'required' => true, - 'label' => 'Poids', - ]) -
diff --git a/resources/views/Shop/Articles/partials/article.blade.php b/resources/views/Shop/Articles/partials/article.blade.php index 235be644..ce3e8b72 100644 --- a/resources/views/Shop/Articles/partials/article.blade.php +++ b/resources/views/Shop/Articles/partials/article.blade.php @@ -1,38 +1,40 @@ diff --git a/resources/views/Shop/Homepage/partials/sliderByShelve.blade.php b/resources/views/Shop/Homepage/partials/sliderByShelve.blade.php index 656c17a2..1d0e8cf4 100644 --- a/resources/views/Shop/Homepage/partials/sliderByShelve.blade.php +++ b/resources/views/Shop/Homepage/partials/sliderByShelve.blade.php @@ -1,27 +1,34 @@ @if ($shelve['articles']) -
-
-
-

{{ $shelve['name'] }}

-
- -
-
-
- @foreach ($shelve['articles'] as $name => $article) - - @endforeach -
-
-
-@endif \ No newline at end of file +
+
+
+

{{ $shelve['name'] }}

+
+ +
+
+
+ @foreach ($shelve['articles'] as $name => $article) + + @endforeach +
+
+
+@endif diff --git a/resources/views/Shop/layout/partials/megamenu.blade.php b/resources/views/Shop/layout/partials/megamenu.blade.php index 930f9678..e71b9080 100644 --- a/resources/views/Shop/layout/partials/megamenu.blade.php +++ b/resources/views/Shop/layout/partials/megamenu.blade.php @@ -1,9 +1,9 @@ @php $submenu = count($menu['children'] ?? []) ? App\Repositories\Core\Arrays::slotify($menu['children'], 6) : []; -@endphp +@endphp diff --git a/routes/Shop/Articles.php b/routes/Shop/Articles.php index 1cb5e8bc..c535541f 100644 --- a/routes/Shop/Articles.php +++ b/routes/Shop/Articles.php @@ -2,5 +2,6 @@ Route::prefix('Articles')->name('Articles.')->group(function () { Route::any('autocomplete/{q?}', 'ArticleController@autocomplete')->name('autocomplete'); - Route::get('show/{id}', 'ArticleController@show')->name('show'); + Route::get('show/{id?}', 'ArticleController@show')->name('show'); + Route::get('voir/{slug?}', 'ArticleController@showBySlug')->name('slug'); });