introduced url structure for categories and products (SEO improvements)

This commit is contained in:
Mona Hartdegen 2019-11-21 17:14:53 +01:00
parent e3d9377dc4
commit 25c2a15b65
18 changed files with 437 additions and 49 deletions

View File

@ -151,6 +151,24 @@ class CategoryRepository extends Repository
);
}
/**
* @param string $urlPath
*
* @return mixed
*/
public function findByPathOrFail(string $urlPath)
{
$category = $this->model->whereTranslation('url_path', $urlPath)->first();
if ($category) {
return $category;
}
throw (new ModelNotFoundException)->setModel(
get_class($this->model), $urlPath
);
}
/**
* @param array $data
* @param $id

View File

@ -166,14 +166,14 @@ class WishlistController extends Controller
} else {
session()->flash('info', trans('shop::app.wishlist.option-missing'));
return redirect()->route('shop.products.index', $wishlistItem->product->url_key);
return redirect()->route('shop.productOrCategory.index', $wishlistItem->product->url_key);
}
return redirect()->back();
} catch (\Exception $e) {
session()->flash('warning', $e->getMessage());
return redirect()->route('shop.products.index', ['slug' => $wishlistItem->product->url_key]);
return redirect()->route('shop.productOrCategory.index', ['slug' => $wishlistItem->product->url_key]);
}
}

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddColumnUrlPathToCategoryTranslations extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('category_translations', function (Blueprint $table) {
$table->string('url_path');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('category_translations', function (Blueprint $table) {
$table->dropColumn('url_path');
});
}
}

View File

@ -0,0 +1,65 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration;
class AddStoredFunctionToGetUrlPathOfCategory extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$functionSQL = <<< SQL
DROP FUNCTION IF EXISTS `get_url_path_of_category`;
CREATE FUNCTION get_url_path_of_category(
categoryId INT
)
RETURNS VARCHAR(255)
DETERMINISTIC
BEGIN
DECLARE urlPath VARCHAR(255);
IF categoryId != 1
THEN
SELECT
GROUP_CONCAT(parent_translations.slug SEPARATOR '/') INTO urlPath
FROM
categories AS node,
categories AS parent
JOIN category_translations AS parent_translations ON parent.id = parent_translations.category_id
WHERE
node._lft >= parent._lft
AND node._rgt <= parent._rgt
AND node.id = categoryId
AND parent.id <> 1
GROUP BY
node.id;
IF urlPath IS NULL
THEN
SET urlPath = (SELECT slug FROM category_translations WHERE category_translations.category_id = categoryId);
END IF;
ELSE
SET urlPath = '';
END IF;
RETURN urlPath;
END;
SQL;
DB::unprepared($functionSQL);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::unprepared('DROP FUNCTION IF EXISTS `get_url_path_of_category`;');
}
}

View File

@ -0,0 +1,99 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration;
class AddTriggerToCategoryTranslations extends Migration
{
private const TRIGGER_NAME_INSERT = 'trig_category_translations_insert';
private const TRIGGER_NAME_UPDATE = 'trig_category_translations_update';
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$insertTriggerBody = $this->getTriggerBody('insert');
$insertTrigger = <<< SQL
CREATE TRIGGER %s
BEFORE INSERT ON category_translations
FOR EACH ROW
BEGIN
$insertTriggerBody
END;
SQL;
$updateTriggerBody = $this->getTriggerBody();
$updateTrigger = <<< SQL
CREATE TRIGGER %s
BEFORE UPDATE ON category_translations
FOR EACH ROW
BEGIN
$updateTriggerBody
END;
SQL;
DB::unprepared(sprintf('DROP TRIGGER IF EXISTS %s;', self::TRIGGER_NAME_INSERT));
DB::unprepared(sprintf($insertTrigger, self::TRIGGER_NAME_INSERT));
DB::unprepared(sprintf('DROP TRIGGER IF EXISTS %s;', self::TRIGGER_NAME_UPDATE));
DB::unprepared(sprintf($updateTrigger, self::TRIGGER_NAME_UPDATE));
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::unprepared(sprintf('DROP TRIGGER IF EXISTS %s;', self::TRIGGER_NAME_INSERT));
DB::unprepared(sprintf('DROP TRIGGER IF EXISTS %s;', self::TRIGGER_NAME_UPDATE));
}
/**
* Returns trigger body as string based on type ('update' or 'insert').
*
* @param string $type
*
* @return string
*/
private function getTriggerBody(string $type = 'update'): string
{
$addOnInsert = ($type === 'update') ? '' : <<< SQL
ELSE
SELECT CONCAT(urlPath, '/', NEW.slug) INTO urlPath;
SQL;
return <<< SQL
DECLARE urlPath varchar(255);
IF NEW.category_id != 1
THEN
SELECT
GROUP_CONCAT(parent_translations.slug SEPARATOR '/') INTO urlPath
FROM
categories AS node,
categories AS parent
JOIN category_translations AS parent_translations ON parent.id = parent_translations.category_id
WHERE
node._lft >= parent._lft
AND node._rgt <= parent._rgt
AND node.id = NEW.category_id
AND parent.id <> 1
GROUP BY
node.id;
IF urlPath IS NULL
THEN
SET urlPath = NEW.slug;
$addOnInsert
END IF;
SET NEW.url_path = urlPath;
END IF;
SQL;
}
}

View File

@ -0,0 +1,44 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration;
use Webkul\Category\Models\CategoryTranslation;
class AddUrlPathToExistingCategoryTranslations extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$sql = <<< SQL
SELECT
GROUP_CONCAT(parent_translations.slug SEPARATOR '/') AS url_path
FROM
categories AS node,
categories AS parent
JOIN category_translations AS parent_translations ON parent.id = parent_translations.category_id
WHERE
node._lft >= parent._lft
AND node._rgt <= parent._rgt
AND node.id = :category_id
AND node.id <> 1
AND parent.id <> 1
GROUP BY
node.id
SQL;
$categoryTranslationsTableName = app(CategoryTranslation::class)->getTable();
foreach (DB::table($categoryTranslationsTableName)->get() as $categoryTranslation) {
$urlPathQueryResult = DB::selectOne($sql, ['category_id' => $categoryTranslation->category_id]);
$url_path = $urlPathQueryResult ? $urlPathQueryResult->url_path : '';
DB::table($categoryTranslationsTableName)
->where('id', $categoryTranslation->id)
->update(['url_path' => $url_path]);
}
}
}

View File

@ -0,0 +1,66 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration;
class AddTriggerToCategories extends Migration
{
private const TRIGGER_NAME_INSERT = 'trig_categories_insert';
private const TRIGGER_NAME_UPDATE = 'trig_categories_update';
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$triggerBody = <<< SQL
DECLARE urlPath VARCHAR(255);
SELECT get_url_path_of_category(NEW.id) INTO urlPath;
IF EXISTS (SELECT * FROM category_translations WHERE category_id = NEW.id)
THEN
UPDATE category_translations
SET url_path = urlPath
WHERE category_translations.category_id = NEW.id;
END IF;
SQL;
$insertTrigger = <<< SQL
CREATE TRIGGER %s
AFTER INSERT ON categories
FOR EACH ROW
BEGIN
$triggerBody
END;
SQL;
$updateTrigger = <<< SQL
CREATE TRIGGER %s
AFTER UPDATE ON categories
FOR EACH ROW
BEGIN
$triggerBody
END;
SQL;
DB::unprepared(sprintf('DROP TRIGGER IF EXISTS %s;', self::TRIGGER_NAME_INSERT));
DB::unprepared(sprintf($insertTrigger, self::TRIGGER_NAME_INSERT));
DB::unprepared(sprintf('DROP TRIGGER IF EXISTS %s;', self::TRIGGER_NAME_UPDATE));
DB::unprepared(sprintf($updateTrigger, self::TRIGGER_NAME_UPDATE));
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::unprepared(sprintf('DROP TRIGGER IF EXISTS %s;', self::TRIGGER_NAME_INSERT));
DB::unprepared(sprintf('DROP TRIGGER IF EXISTS %s;', self::TRIGGER_NAME_UPDATE));
}
}

View File

@ -96,7 +96,7 @@ class CartController extends Controller
$product = $this->productRepository->find($id);
return redirect()->route('shop.products.index', ['slug' => $product->url_key]);
return redirect()->route('shop.productOrCategory.index', ['slug' => $product->url_key]);
}
return redirect()->back();

View File

@ -38,17 +38,4 @@ class CategoryController extends Controller
$this->_config = request('_config');
}
/**
* Display a listing of the resource.
*
* @param string $slug
* @return \Illuminate\View\View
*/
public function index($slug)
{
$category = $this->categoryRepository->findBySlugOrFail($slug);
return view($this->_config['view'], compact('category'));
}
}

View File

@ -79,21 +79,6 @@ class ProductController extends Controller
$this->_config = request('_config');
}
/**
* Display a listing of the resource.
*
* @param string $slug
* @return \Illuminate\View\View
*/
public function index($slug)
{
$product = $this->productRepository->findBySlugOrFail($slug);
$customer = auth()->guard('customer')->user();
return view($this->_config['view'], compact('product', 'customer'));
}
/**
* Download image or file
*

View File

@ -0,0 +1,92 @@
<?php
namespace Webkul\Shop\Http\Controllers;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Webkul\Category\Models\CategoryTranslation;
use Webkul\Product\Models\ProductFlat;
use Webkul\Category\Repositories\CategoryRepository;
use Webkul\Product\Repositories\ProductRepository;
class ProductsCategoriesProxyController extends Controller
{
/**
* Contains route related configuration
*
* @var array
*/
protected $_config;
/**
* CategoryRepository object
*
* @var CategoryRepository
*/
protected $categoryRepository;
/**
* ProductRepository object
*
* @var ProductRepository
*/
protected $productRepository;
/**
* Create a new controller instance.
*
* @param CategoryRepository $categoryRepository
* @param ProductRepository $productRepository
*
* @return void
*/
public function __construct(CategoryRepository $categoryRepository, ProductRepository $productRepository)
{
$this->categoryRepository = $categoryRepository;
$this->productRepository = $productRepository;
$this->_config = request('_config');
}
/**
* Display a listing of the resource which can be a category or a product.
*
*
* @param string $slug
*
* @return \Illuminate\View\View
*/
public function index(string $slug)
{
$slug = rtrim($slug, '/ ');
if (preg_match('/^([a-z-]+\/?)+$/', $slug)) {
if (DB::table(app(CategoryTranslation::class)->getTable())
->where('url_path', '=', $slug)
->exists()
) {
$category = $this->categoryRepository->findByPathOrFail($slug);
return view($this->_config['category_view'], compact('category'));
}
if (DB::table(app(ProductFlat::class)->getTable())
->where('url_key', '=', $slug)
->exists()
) {
$product = $this->productRepository->findBySlugOrFail($slug);
$customer = auth()->guard('customer')->user();
return view($this->_config['product_view'], compact('product', 'customer'));
}
} else {
throw new NotFoundHttpException();
}
}
}

View File

@ -13,11 +13,6 @@ Route::group(['middleware' => ['web', 'locale', 'theme', 'currency']], function
//unsubscribe
Route::get('/unsubscribe/{token}', 'Webkul\Shop\Http\Controllers\SubscriptionController@unsubscribe')->name('shop.unsubscribe');
//Store front header nav-menu fetch
Route::get('/categories/{slug}', 'Webkul\Shop\Http\Controllers\CategoryController@index')->defaults('_config', [
'view' => 'shop::products.index'
])->name('shop.categories.index');
//Store front search
Route::get('/search', 'Webkul\Shop\Http\Controllers\SearchController@index')->defaults('_config', [
'view' => 'shop::search.search'
@ -89,11 +84,6 @@ Route::group(['middleware' => ['web', 'locale', 'theme', 'currency']], function
//Shop buynow button action
Route::get('move/wishlist/{id}', 'Webkul\Shop\Http\Controllers\CartController@moveToWishlist')->name('shop.movetowishlist');
//Show Product Details Page(For individually Viewable Product)
Route::get('/products/{slug}', 'Webkul\Shop\Http\Controllers\ProductController@index')->defaults('_config', [
'view' => 'shop::products.view'
])->name('shop.products.index');
Route::get('/downloadable/download-sample/{type}/{id}', 'Webkul\Shop\Http\Controllers\ProductController@downloadSample')->name('shop.downloadable.download_sample');
// Show Product Review Form
@ -307,5 +297,13 @@ Route::group(['middleware' => ['web', 'locale', 'theme', 'currency']], function
Route::get('page/{slug}', 'Webkul\CMS\Http\Controllers\Shop\PagePresenterController@presenter')->name('shop.cms.page');
Route::get('{slug}', \Webkul\Shop\Http\Controllers\ProductsCategoriesProxyController::class . '@index')
->defaults('_config', [
'product_view' => 'shop::products.view',
'category_view' => 'shop::products.index'
])
->where('slug', '^([a-z-]+\/?)+$')
->name('shop.productOrCategory.index');
Route::fallback('Webkul\Shop\Http\Controllers\HomeController@notFound');
});

View File

@ -25,6 +25,8 @@ class ShopServiceProvider extends ServiceProvider
*/
public function boot(Router $router)
{
$this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
$this->loadRoutesFrom(__DIR__ . '/../Http/routes.php');
$this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', 'shop');

View File

@ -18,7 +18,7 @@
<ul class="list-group">
@foreach ($categories as $key => $category)
<li>
<a href="{{ route('shop.categories.index', $category->slug) }}">{{ $category->name }}</a>
<a href="{{ route('shop.productOrCategory.index', $category->slug) }}">{{ $category->name }}</a>
</li>
@endforeach
</ul>

View File

@ -13,7 +13,7 @@
@endif
<div class="product-image">
<a href="{{ route('shop.products.index', $product->url_key) }}" title="{{ $product->name }}">
<a href="{{ route('shop.productOrCategory.index', $product->url_key) }}" title="{{ $product->name }}">
<img src="{{ $productBaseImage['medium_image_url'] }}" onerror="this.src='{{ asset('vendor/webkul/ui/assets/images/product/meduim-product-placeholder.png') }}'"/>
</a>
</div>
@ -21,7 +21,7 @@
<div class="product-information">
<div class="product-name">
<a href="{{ url()->to('/').'/products/' . $product->url_key }}" title="{{ $product->name }}">
<a href="{{ route('shop.productOrCategory.index', $product->url_key) }}" title="{{ $product->name }}">
<span>
{{ $product->name }}
</span>

View File

@ -15,13 +15,13 @@
<?php $productBaseImage = $productImageHelper->getProductBaseImage($product); ?>
<div class="product-image">
<a href="{{ route('shop.products.index', $product->url_key) }}" title="{{ $product->name }}">
<a href="{{ route('shop.productOrCategory.index', $product->url_key) }}" title="{{ $product->name }}">
<img src="{{ $productBaseImage['medium_image_url'] }}" />
</a>
</div>
<div class="product-name mt-20">
<a href="{{ url()->to('/').'/products/'.$product->url_key }}" title="{{ $product->name }}">
<a href="{{ route('shop.productOrCategory.index', $product->url_key) }}" title="{{ $product->name }}">
<span>{{ $product->name }}</span>
</a>
</div>

View File

@ -17,13 +17,13 @@
<div class="product-info">
<div class="product-image">
<a href="{{ route('shop.products.index', $product->url_key) }}" title="{{ $product->name }}">
<a href="{{ route('shop.productOrCategory.index', $product->url_key) }}" title="{{ $product->name }}">
<img src="{{ $productBaseImage['medium_image_url'] }}" />
</a>
</div>
<div class="product-name mt-20">
<a href="{{ url()->to('/').'/products/'.$product->url_key }}" title="{{ $product->name }}">
<a href="{{ ('shop.productOrCategory.index', $product->url_key) }}" title="{{ $product->name }}">
<span>{{ $product->name }}</span>
</a>
</div>

View File

@ -41,7 +41,7 @@ class ProductCategoryTest extends DuskTestCase
}
$this->browse(function (Browser $browser) use($testSlug, $testProduct) {
$browser->visit(route('shop.categories.index', $testSlug));
$browser->visit(route('shop.productOrCategory.index', $testSlug));
$browser->assertSeeLink($testProduct[0]['name']);
$browser->pause(5000);
});