Merge branch 'search' into 'develop'

feat: ✨ set search page with meilisearch

See merge request ldc-web/projets/ldc!111
This commit is contained in:
karl.muller 2026-01-15 13:47:31 +00:00
commit 811cdb7089
35 changed files with 1382 additions and 352 deletions

View file

@ -69,4 +69,9 @@ SAML2_CONTACT_TECHNICAL_EMAIL=
SAML2_CONTACT_SUPPORT_EMAIL=
# GOOGLE
GOOGLE_MAPS_KEY=
GOOGLE_MAPS_KEY=
# MEILISEARCH
SCOUT_PREFIX=
MEILISEARCH_HOST=
MEILISEARCH_KEY=

View file

@ -0,0 +1,78 @@
<?php
namespace App\Console\Commands;
use App\Models\Document;
use App\Models\DynamicPage;
use App\Models\News;
use App\Models\SearchIndex;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
use Meilisearch\Client;
class MeilisearchUpdate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'meilisearch:update';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Configure Meilisearch and rebuilt all indexes in all locales.';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
// Get new instance of Meilisearch client.
$client = new Client(
config('scout.meilisearch.host'),
config('scout.meilisearch.key')
);
// Get all supported locales defined in laravel localization config.
$locales = LaravelLocalization::getSupportedLocales();
collect(array_keys($locales))->map(function (string $locale) use ($client) {
$localeName = Str::upper($locale);
$indexName = config('scout.prefix') . $locale;
$index = $client->index($indexName);
$index->updateSearchableAttributes(['title', 'content']);
$index->updateFilterableAttributes(['type']);
$this->info("Meilisearch indexes configured for locale: {$localeName}");
app()->setLocale($locale);
SearchIndex::removeAllFromSearch();
collect([
News::class,
DynamicPage::class,
Document::class,
])->map(function(string $class) {
$class::chunk(100, function ($items) {
$items
->map
->toSearchDocument()
->filter()
->each
->searchable();
});
});
$this->info("Meilisearch indexes rebuilt for locale: {$localeName}");
});
}
}

View file

@ -17,7 +17,7 @@ class HomeController extends Controller
/**
* unsubscribeNewsletter method
*
* @param \App\Http\Requests\Fo\UnsubscribeNewsletterRequest $request
* @param \App\Http\Requests\Front\UnsubscribeNewsletterRequest $request
* @return \Illuminate\Contracts\View\View
*/
public function unsubscribeNewsletter(UnsubscribeNewsletterRequest $request): \Illuminate\Contracts\View\View

View file

@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers\Front;
use App\Enums\StaticPageType;
use App\Http\Requests\Front\SearchRequest;
use App\Models\StaticPage;
use App\Models\SearchIndex;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Uwil\LaravelCommon\Controllers\Controller;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
class SearchController extends Controller
{
/**
* Search page.
*
* @param \App\Http\Requests\Front\SearchRequest $request
* @return \Illuminate\Contracts\View\View
*/
public function search(SearchRequest $request): \Illuminate\Contracts\View\View
{
$queryKey = trans('front.search.query');
$pageKey = trans('front.search.page');
$searchTerm = $request->validated()[$queryKey];
$currentPage = $request->validated()[$pageKey];
$perPage = 10;
$searchQuery = $this->buildExactSearchQuery($searchTerm);
$resultModels = SearchIndex::search($searchQuery)
->paginateRaw(perPage: $perPage, page: $currentPage);
$resultModels->appends([
$queryKey => $searchTerm,
$pageKey => $currentPage,
'query' => null,
]);
return view('front.pages.search', compact('searchTerm', 'resultModels'));
}
/**
* Set strict search term.
*
* @return string
*/
private function buildExactSearchQuery(string $searchTerm): string
{
$words = preg_split('/\s+/', trim($searchTerm), -1, PREG_SPLIT_NO_EMPTY);
if (count($words) === 1) {
return '"' . $words[0] . '"';
}
$exactTerms = array_map(function($word) {
return $word . '*';
}, $words);
return '+' . implode(' +', $exactTerms);
}
}

View file

@ -21,6 +21,7 @@ use Diglactic\Breadcrumbs\Breadcrumbs;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Inertia\Middleware;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
use Uwil\LaravelBackend\Enums\RoleEnum;
use Uwil\LaravelBackend\Models\User;
use Uwil\LaravelCommon\Enums\ItemsPerPaginationEnum;
@ -53,6 +54,8 @@ class HandleInertiaRequests extends Middleware
]),
]);
$locales = LaravelLocalization::getSupportedLocales();
$props = [
...parent::share($request),
'appEnv' => [
@ -60,7 +63,7 @@ class HandleInertiaRequests extends Middleware
'name' => config('app.name'),
'url' => config('app.url'),
'locale' => app()->getLocale(),
'locales' => config('app.locales'),
'locales' => array_keys($locales),
'fallbackLocale' => app()->getFallbackLocale(),
],
'auth' => [

View file

@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests\Front;
use Illuminate\Foundation\Http\FormRequest;
class SearchRequest extends FormRequest
{
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->merge(['page' => (int) $this->page ?: 1]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$queryKey = trans('front.search.query');
$pageKey = trans('front.search.page');
return [
$queryKey => 'required|string|between:3,255',
$pageKey => 'required|integer|min:1',
];
}
}

View file

@ -2,19 +2,21 @@
namespace App\Models;
use Carbon\Carbon;
use App\Enums\YearsEnum;
use Illuminate\Support\Arr;
use App\Traits\Models\HasCache;
use App\Helpers\TranslationHelper;
use Illuminate\Support\Facades\App;
use App\Contracts\TranslatableModel;
use App\Traits\Models\HasUwilImages;
use App\Traits\Models\HasTranslations;
use Illuminate\Database\Eloquent\Model;
use App\Enums\Document\DocumentStatusEnum;
use Uwil\LaravelActivityLogs\Traits\ActivityLog;
use App\Enums\YearsEnum;
use App\Helpers\TranslationHelper;
use App\Traits\Models\HasCache;
use App\Traits\Models\HasMeilisearchIndex;
use App\Traits\Models\HasTranslations;
use App\Traits\Models\HasUwilImages;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Str;
use Uwil\LaravelActivityLogs\Traits\ActivityLog;
class Document extends Model implements TranslatableModel
{
@ -25,6 +27,7 @@ class Document extends Model implements TranslatableModel
use HasUwilImages {
HasUwilImages::inertiaFill as traitInertiaFill;
}
use HasMeilisearchIndex;
/**
* Champs contenant des images dynamiques à gérer.
@ -96,6 +99,8 @@ class Document extends Model implements TranslatableModel
});
}
// * METHODS
/**
* Adjusts model status using current status and publish dates.
* Case 1: publish model when all null -> set published_at date.
@ -164,7 +169,6 @@ class Document extends Model implements TranslatableModel
}
return DocumentStatusEnum::unpublished;
}
// * METHODS
/**
* Manage model fields.
@ -212,6 +216,29 @@ class Document extends Model implements TranslatableModel
return sprintf('/%s', $targetModel->path);
}
/**
* Set value of meilisearch indexes.
*
* @return \App\Models\SearchIndex|null
*/
public function toSearchDocument(): ?\App\Models\SearchIndex
{
$locale = app()->getLocale();
// Keep only published models.
if (!$this->status->equals(DocumentStatusEnum::published) || !$this->hasTranslation('path', $locale)) {
return null;
}
return new SearchIndex([
'type' => Str::snake(class_basename($this)),
'model_id' => $this->getKey(),
'slug' => $this->getTranslation('path', $locale),
'title' => $this->getTranslation('name', $locale),
'content' => null,
]);
}
// * RELATIONSHIPS
/**

View file

@ -4,11 +4,14 @@ namespace App\Models;
use App\Contracts\TranslatableModel;
use App\Enums\DynamicFieldTypes;
use App\Enums\DynamicPages;
use App\Enums\DynamicPages\PageStatusEnum;
use App\Helpers\DynamicPageHelper;
use App\Helpers\RouteHelper;
use App\Helpers\TranslationHelper;
use App\Models\SearchIndex;
use App\Traits\Models\HasCache;
use App\Traits\Models\HasMeilisearchIndex;
use App\Traits\Models\HasTranslations;
use App\Traits\Models\HasUwilImages;
use Illuminate\Database\Eloquent\Collection;
@ -44,6 +47,7 @@ class DynamicPage extends Model implements TranslatableModel
use HasTranslations;
use HasUwilImages;
use SoftDeletes;
use HasMeilisearchIndex;
/**
* The attributes that are mass assignable.
@ -316,6 +320,72 @@ class DynamicPage extends Model implements TranslatableModel
return parse_url($url, PHP_URL_PATH) ?? "/";
}
/**
* Set value of meilisearch indexes.
*
* @return \App\Models\SearchIndex|null
*/
public function toSearchDocument(): ?\App\Models\SearchIndex
{
$locale = app()->getLocale();
// Keep only published models.
if (!$this->status->equals(PageStatusEnum::published) || !$this->hasTranslation('slug', $locale)) {
return null;
}
// Clean content (no tags, no paths, no numbers, only string).
$content = strip_tags(collect($this->getTranslation('content', $locale))
->flatten()
->filter(fn($value, $key) =>
is_string($value) &&
!empty($value) &&
!is_numeric($value) &&
!in_array($key, ['order', 'id']) &&
!str_starts_with($key, 'image_') &&
!preg_match('/storage.*\.(?:png|jpe?g|gif|pdf|webp)/i', $value)
)
->values()
->implode(' '));
return new SearchIndex([
'type' => Str::snake(class_basename($this)),
'model_id' => $this->getKey(),
'id_key' => $this->id_key,
'slug' => $this->getTranslation('slug', $locale),
'title' => $this->getTranslation('title', $locale),
'content' => $content
]);
}
/**
* Get localized path from the locale.
*
* @param string $locale
* @return string|null
*/
public function getLocalizedPath(string $locale): ?string
{
$slug = $this->getTranslation('slug', $locale);
if (! $slug) {
return null;
}
$pages = static::getModelsCached()
->where('status', PageStatusEnum::published)
->load('parentPage')
->keyBy('id');
$segments = DynamicPageHelper::getAllDynamicPageSegments($this, $pages, $locale);
$segmentsPath = implode('/', $segments);
return $segmentsPath
? "{$segmentsPath}/{$slug}"
: $slug;
}
// * RELATIONSHIPS
/**
@ -347,34 +417,4 @@ class DynamicPage extends Model implements TranslatableModel
{
return $this->belongsTo(PageConfig::class, 'config_id');
}
/**
* Get localized path from the locale
*
* @param string $locale
* @return string|null
*/
public function getLocalizedPath(string $locale): ?string
{
$slug = $this->getTranslation('slug', $locale);
if (! $slug) {
return null;
}
// Récupère toutes les pages publiées (cache déjà existant chez toi)
$pages = static::getModelsCached()
->where('status', PageStatusEnum::published)
->load('parentPage')
->keyBy('id');
$segments = DynamicPageHelper::getAllDynamicPageSegments($this, $pages, $locale);
$segmentsPath = implode('/', $segments);
return $segmentsPath
? "{$segmentsPath}/{$slug}"
: $slug;
}
}

View file

@ -2,23 +2,25 @@
namespace App\Models;
use Carbon\Carbon;
use App\Models\NewsTag;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use App\Traits\Models\HasCache;
use App\Contracts\TranslatableModel;
use App\Enums\News\NewsStatusEnum;
use App\Helpers\TranslationHelper;
use Illuminate\Support\Facades\App;
use App\Contracts\TranslatableModel;
use App\Traits\Models\HasUwilImages;
use App\Models\NewsTag;
use App\Models\SearchIndex;
use App\Traits\Models\HasCache;
use App\Traits\Models\HasMeilisearchIndex;
use App\Traits\Models\HasTranslations;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Uwil\LaravelActivityLogs\Traits\ActivityLog;
use App\Traits\Models\HasUwilImages;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Str;
use Mcamara\LaravelLocalization\Interfaces\LocalizedUrlRoutable;
use Uwil\LaravelActivityLogs\Traits\ActivityLog;
/**
* @property string $seo_title SEO title of the news.
@ -48,6 +50,7 @@ class News extends Model implements TranslatableModel, LocalizedUrlRoutable
use HasUwilImages {
HasUwilImages::inertiaFill as traitInertiaFill;
}
use HasMeilisearchIndex;
/**
* Get the route key for the model.
@ -317,6 +320,44 @@ class News extends Model implements TranslatableModel, LocalizedUrlRoutable
TranslationHelper::validateFallbackTranslations($model);
}
/**
* Set value of meilisearch indexes.
*
* @return \App\Models\SearchIndex|null
*/
public function toSearchDocument(): ?\App\Models\SearchIndex
{
$locale = app()->getLocale();
// Keep only published models.
if (!$this->status->equals(NewsStatusEnum::published) || !$this->hasTranslation('slug', $locale)) {
return null;
}
return new SearchIndex([
'type' => Str::snake(class_basename($this)),
'model_id' => $this->getKey(),
'slug' => $this->getTranslation('slug', $locale),
'title' => $this->getTranslation('title', $locale),
'content' => strip_tags($this->getTranslation('content', $locale)),
]);
}
/**
* Get the url to access a specific resource.
*
* @param integer $id Id field of the target model.
* @return string
*/
public static function getAccessModel(int $id): string
{
$targetModel = self::getModelsCached()->firstWhere('id', $id);
if (!$targetModel) {
return null;
}
return route('front.news.show', $targetModel);
}
// * RELATIONSHIPS
/**
@ -324,7 +365,7 @@ class News extends Model implements TranslatableModel, LocalizedUrlRoutable
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function categories(): BelongsToMany
public function categories(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{
return $this->morphedByMany(
NewsCategory::class,
@ -340,7 +381,7 @@ class News extends Model implements TranslatableModel, LocalizedUrlRoutable
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function tags(): BelongsToMany
public function tags(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{
return $this->morphedByMany(
NewsTag::class,

152
app/Models/SearchIndex.php Normal file
View file

@ -0,0 +1,152 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
use Illuminate\Support\Str;
class SearchIndex extends Model
{
use Searchable;
/** @var string|null $table */
protected $table = null;
/** @var boolean $timestamps */
public $timestamps = false;
/** @var array $guarded */
protected $guarded = [];
// * METHODS
/**
* Unique name of the Meilisearch index.
*
* @return string
*/
public function searchableAs(): string
{
return config('scout.prefix') . app()->getLocale();
}
/**
* Unique primary key.
*
* @return string
*/
public function getScoutKey(): mixed
{
return "{$this->type}_{$this->model_id}";
}
/**
* Indexed data.
*
* @return array
*/
public function toSearchableArray(): array
{
return [
'id' => $this->getScoutKey(),
'type' => $this->type,
'model_id' => $this->model_id,
'id_key' => $this->id_key ?? null,
'slug' => $this->slug,
'title' => $this->title,
'content' => $this->content,
];
}
/**
* Highlights all search terms in a text.
*
* @param string $text
* @param string $searchTerm
* @return string
*/
public static function highlightTerms(string $text, string $searchTerm, int $contextLength = 150): string
{
if (trim($searchTerm) === '') {
return $text;
}
// Clean terms list.
$words = preg_split('/\s+/', $searchTerm, -1, PREG_SPLIT_NO_EMPTY);
if (empty($words)) {
return $text;
}
$fullText = strip_tags($text);
$textLength = strlen($fullText);
$ranges = [];
// Collection of relevant ranges around each word.
foreach ($words as $word) {
$pattern = '/(?<!\w)' . preg_quote($word, '/') . '(?!\w)/iu';
// Check if current word match exactly with terms.
if (preg_match_all($pattern, $fullText, $matches, PREG_OFFSET_CAPTURE)) {
foreach ($matches[0] as $match) {
$matchPos = $match[1];
$matchLen = strlen($match[0]);
$ranges[] = [
'start' => max(0, $matchPos - $contextLength),
'end' => min($textLength, $matchPos + $matchLen + $contextLength),
];
}
}
}
// Return the entire text if there isn't matches.
if (empty($ranges)) {
return $text;
}
// Merging overlapping intervals.
usort($ranges, fn($a, $b) => $a['start'] <=> $b['start']);
$merged = [];
foreach ($ranges as $r) {
if (empty($merged) || $r['start'] > $merged[array_key_last($merged)]['end']) {
$merged[] = $r;
} else {
$merged[array_key_last($merged)]['end'] = max(
$merged[array_key_last($merged)]['end'],
$r['end']
);
}
}
$snippets = [];
foreach ($merged as $r) {
$snippet = substr($fullText, $r['start'], $r['end'] - $r['start']);
// Cut cleanly without breaking the words
if ($r['start'] > 0) {
$snippet = preg_replace('/^\S*\s/', '…', $snippet, 1);
}
if ($r['end'] < $textLength) {
$snippet = preg_replace('/\s\S*$/', '…', $snippet, 1);
}
// Highlighting searched words
foreach ($words as $word) {
$pattern = '/(?<!\w)' . preg_quote($word, '/') . '(?!\w)/iu';
$snippet = preg_replace(
$pattern,
'<span class="search-highlight">$0</span>',
$snippet
);
}
$snippets[] = trim($snippet);
if (count($snippets) >= 3) break;
}
$result = implode('… ', $snippets);
$result = preg_replace('/\s*…\s*(?:…\s*)+/', ' … ', $result);
return trim($result);
}
}

View file

@ -0,0 +1,113 @@
<?php
namespace App\Traits\Models;
use App\Models\SearchIndex;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
/**
* If there is a static function in a trait (named boot[TraitName]),
* it will be executed as the boot() function would on an Eloquent model.
* Futhermore, event in the Model observer doesn't erase actual events in the model,
* but adds them.
*/
trait HasMeilisearchIndex
{
/**
* Perform any actions required after the model boots.
*
* @return void
*/
public static function bootHasMeilisearchIndex(): void
{
$keyPrefix = Str::snake(class_basename(static::class));
$usesSoftDeletes = in_array(SoftDeletes::class, class_uses_recursive(static::class));
// Event executed avec saving a model.
static::saved(function ($model) use ($keyPrefix, $usesSoftDeletes) {
// Update ms index depending on status which is a global field non translatable.
$statusChanged = $model->wasChanged('status');
$originalLocale = app()->getLocale();
$locales = $statusChanged
? array_keys(LaravelLocalization::getSupportedLocales())
: [$originalLocale];
// Loop on all locales to update indexes when global fields like status is updated,
// Otherwise, on current locale.
collect($locales)->map(function($locale) use ($keyPrefix, $model, $usesSoftDeletes) {
// Set the locale to alter the right ms index.
app()->setLocale($locale);
$indexId = "{$keyPrefix}_{$model->getKey()}";
// Remove model from meilisearch index.
if ($usesSoftDeletes && method_exists($model, 'trashed') && $model->trashed()) {
SearchIndex::make([
'type' => $keyPrefix,
'model_id' => $model->getKey(),
])->unsearchable();
return;
}
$msIndex = $model->toSearchDocument();
// Add model to the meilisearch index if conditions are true, remove from it if not.
if ($msIndex) {
$msIndex->searchable();
} else {
SearchIndex::make([
'type' => $keyPrefix,
'model_id' => $model->getKey(),
])->unsearchable();
}
});
// Reset the locale with the original.
app()->setLocale($originalLocale);
});
static::deleted(function ($model) use ($keyPrefix) {
$originalLocale = app()->getLocale();
$locales = array_keys(LaravelLocalization::getSupportedLocales());
// Loop on all locales to update indexes when model is deleted (softdeleted or not).
collect($locales)->map(function($locale) use ($keyPrefix, $model) {
// Set the locale to alter the right ms index.
app()->setLocale($locale);
$indexId = "{$keyPrefix}_{$model->getKey()}";
// Remove model from meilisearch index.
SearchIndex::make([
'type' => $keyPrefix,
'model_id' => $model->getKey(),
])->unsearchable();
});
// Reset the locale with the original.
app()->setLocale($originalLocale);
});
if ($usesSoftDeletes) {
static::restored(function ($model) {
$originalLocale = app()->getLocale();
$locales = array_keys(LaravelLocalization::getSupportedLocales());
// Loop on all locales to update indexes when model is restored.
collect($locales)->map(function($locale) use ($model) {
// Set the locale to alter the right ms index.
app()->setLocale($locale);
// Add model to the meilisearch index if conditions are true.
$msIndex = $model->toSearchDocument();
if ($msIndex) {
$msIndex->searchable();
}
});
// Reset the locale with the original.
app()->setLocale($originalLocale);
});
}
}
}

View file

@ -12,12 +12,15 @@
"php": "^8.2",
"24slides/laravel-saml2": "^2.4",
"diglactic/laravel-breadcrumbs": "^10.0",
"http-interop/http-factory-guzzle": "^1.2",
"inertiajs/inertia-laravel": "^2.0",
"laravel/framework": "^12.0",
"laravel/scout": "^10.23",
"laravel/telescope": "^5.7",
"laravel/tinker": "^2.10.1",
"ldc/ldccrmv4client": "^4.1",
"mcamara/laravel-localization": "^2.3.0",
"meilisearch/meilisearch-php": "^1.16",
"sentry/sentry-laravel": "^4.18",
"spatie/laravel-translatable": "^6.11",
"tightenco/ziggy": "^2.5",

379
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f9cb87942c7b3c93516b54b61c4bf66d",
"content-hash": "b1d9d1034b8cd27b5f5de54e9fce077a",
"packages": [
{
"name": "24slides/laravel-saml2",
@ -1311,6 +1311,64 @@
],
"time": "2025-08-22T14:27:06+00:00"
},
{
"name": "http-interop/http-factory-guzzle",
"version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/http-interop/http-factory-guzzle.git",
"reference": "c2c859ceb05c3f42e710b60555f4c35b6a4a3995"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/http-interop/http-factory-guzzle/zipball/c2c859ceb05c3f42e710b60555f4c35b6a4a3995",
"reference": "c2c859ceb05c3f42e710b60555f4c35b6a4a3995",
"shasum": ""
},
"require": {
"guzzlehttp/psr7": "^1.7||^2.0",
"php": ">=7.3",
"psr/http-factory": "^1.0"
},
"provide": {
"psr/http-factory-implementation": "^1.0"
},
"require-dev": {
"http-interop/http-factory-tests": "^0.9",
"phpunit/phpunit": "^9.5"
},
"suggest": {
"guzzlehttp/psr7": "Includes an HTTP factory starting in version 2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Http\\Factory\\Guzzle\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "An HTTP Factory using Guzzle PSR7",
"keywords": [
"factory",
"http",
"psr-17",
"psr-7"
],
"support": {
"issues": "https://github.com/http-interop/http-factory-guzzle/issues",
"source": "https://github.com/http-interop/http-factory-guzzle/tree/1.2.1"
},
"time": "2025-12-15T11:28:16+00:00"
},
{
"name": "inertiajs/inertia-laravel",
"version": "v2.0.10",
@ -1925,6 +1983,86 @@
},
"time": "2025-09-19T13:47:56+00:00"
},
{
"name": "laravel/scout",
"version": "v10.23.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/scout.git",
"reference": "fb6d94cfc5708e4202dc00d46e61af0b9f35b03c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/scout/zipball/fb6d94cfc5708e4202dc00d46e61af0b9f35b03c",
"reference": "fb6d94cfc5708e4202dc00d46e61af0b9f35b03c",
"shasum": ""
},
"require": {
"illuminate/bus": "^9.0|^10.0|^11.0|^12.0",
"illuminate/contracts": "^9.0|^10.0|^11.0|^12.0",
"illuminate/database": "^9.0|^10.0|^11.0|^12.0",
"illuminate/http": "^9.0|^10.0|^11.0|^12.0",
"illuminate/pagination": "^9.0|^10.0|^11.0|^12.0",
"illuminate/queue": "^9.0|^10.0|^11.0|^12.0",
"illuminate/support": "^9.0|^10.0|^11.0|^12.0",
"php": "^8.0",
"symfony/console": "^6.0|^7.0"
},
"conflict": {
"algolia/algoliasearch-client-php": "<3.2.0|>=5.0.0"
},
"require-dev": {
"algolia/algoliasearch-client-php": "^3.2|^4.0",
"meilisearch/meilisearch-php": "^1.0",
"mockery/mockery": "^1.0",
"orchestra/testbench": "^7.31|^8.36|^9.15|^10.8",
"php-http/guzzle7-adapter": "^1.0",
"phpstan/phpstan": "^1.10",
"typesense/typesense-php": "^4.9.3"
},
"suggest": {
"algolia/algoliasearch-client-php": "Required to use the Algolia engine (^3.2).",
"meilisearch/meilisearch-php": "Required to use the Meilisearch engine (^1.0).",
"typesense/typesense-php": "Required to use the Typesense engine (^4.9)."
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Scout\\ScoutServiceProvider"
]
},
"branch-alias": {
"dev-master": "10.x-dev"
}
},
"autoload": {
"psr-4": {
"Laravel\\Scout\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel Scout provides a driver based solution to searching your Eloquent models.",
"keywords": [
"algolia",
"laravel",
"search"
],
"support": {
"issues": "https://github.com/laravel/scout/issues",
"source": "https://github.com/laravel/scout"
},
"time": "2025-12-16T15:43:03+00:00"
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.6",
@ -2800,6 +2938,86 @@
],
"time": "2025-02-26T06:38:01+00:00"
},
{
"name": "meilisearch/meilisearch-php",
"version": "v1.16.1",
"source": {
"type": "git",
"url": "https://github.com/meilisearch/meilisearch-php.git",
"reference": "f9f63e0e7d12ffaae54f7317fa8f4f4dfa8ae7b6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/meilisearch/meilisearch-php/zipball/f9f63e0e7d12ffaae54f7317fa8f4f4dfa8ae7b6",
"reference": "f9f63e0e7d12ffaae54f7317fa8f4f4dfa8ae7b6",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^7.4 || ^8.0",
"php-http/discovery": "^1.7",
"psr/http-client": "^1.0",
"symfony/polyfill-php81": "^1.33"
},
"require-dev": {
"http-interop/http-factory-guzzle": "^1.2.0",
"php-cs-fixer/shim": "^3.59.3",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^9.5 || ^10.5",
"symfony/http-client": "^5.4|^6.0|^7.0"
},
"suggest": {
"guzzlehttp/guzzle": "Use Guzzle ^7 as HTTP client",
"http-interop/http-factory-guzzle": "Factory for guzzlehttp/guzzle",
"symfony/http-client": "Use Symfony Http client"
},
"type": "library",
"autoload": {
"psr-4": {
"MeiliSearch\\": "src/",
"Meilisearch\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Clémentine Urquizar",
"email": "clementine@meilisearch.com"
},
{
"name": "Bruno Casali",
"email": "bruno@meilisearch.com"
},
{
"name": "Laurent Cazanove",
"email": "lau.cazanove@gmail.com"
},
{
"name": "Tomas Norkūnas",
"email": "norkunas.tom@gmail.com"
}
],
"description": "PHP wrapper for the Meilisearch API",
"keywords": [
"api",
"client",
"instant",
"meilisearch",
"php",
"search"
],
"support": {
"issues": "https://github.com/meilisearch/meilisearch-php/issues",
"source": "https://github.com/meilisearch/meilisearch-php/tree/v1.16.1"
},
"time": "2025-09-18T10:15:45+00:00"
},
{
"name": "mews/purifier",
"version": "3.4.3",
@ -3527,6 +3745,85 @@
],
"time": "2025-05-25T14:28:00+00:00"
},
{
"name": "php-http/discovery",
"version": "1.20.0",
"source": {
"type": "git",
"url": "https://github.com/php-http/discovery.git",
"reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d",
"reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.0|^2.0",
"php": "^7.1 || ^8.0"
},
"conflict": {
"nyholm/psr7": "<1.0",
"zendframework/zend-diactoros": "*"
},
"provide": {
"php-http/async-client-implementation": "*",
"php-http/client-implementation": "*",
"psr/http-client-implementation": "*",
"psr/http-factory-implementation": "*",
"psr/http-message-implementation": "*"
},
"require-dev": {
"composer/composer": "^1.0.2|^2.0",
"graham-campbell/phpspec-skip-example-extension": "^5.0",
"php-http/httplug": "^1.0 || ^2.0",
"php-http/message-factory": "^1.0",
"phpspec/phpspec": "^5.1 || ^6.1 || ^7.3",
"sebastian/comparator": "^3.0.5 || ^4.0.8",
"symfony/phpunit-bridge": "^6.4.4 || ^7.0.1"
},
"type": "composer-plugin",
"extra": {
"class": "Http\\Discovery\\Composer\\Plugin",
"plugin-optional": true
},
"autoload": {
"psr-4": {
"Http\\Discovery\\": "src/"
},
"exclude-from-classmap": [
"src/Composer/Plugin.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com"
}
],
"description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations",
"homepage": "http://php-http.org",
"keywords": [
"adapter",
"client",
"discovery",
"factory",
"http",
"message",
"psr17",
"psr7"
],
"support": {
"issues": "https://github.com/php-http/discovery/issues",
"source": "https://github.com/php-http/discovery/tree/1.20.0"
},
"time": "2024-10-02T11:20:13+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.4",
@ -6344,6 +6641,86 @@
],
"time": "2025-01-02T08:10:11+00:00"
},
{
"name": "symfony/polyfill-php81",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php81\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php83",
"version": "v1.33.0",

View file

@ -80,6 +80,7 @@ return [
'locale' => env('APP_LOCALE', 'fr'),
// FIXME: need this because of uwil packages
'locales' => [
'en',
'fr',

110
config/scout.php Normal file
View file

@ -0,0 +1,110 @@
<?php
use App\Models\DynamicPage;
use App\Models\News;
return [
/*
|--------------------------------------------------------------------------
| Default Search Engine
|--------------------------------------------------------------------------
|
| This option controls the default search connection that gets used while
| using Laravel Scout. This connection is used when syncing all models
| to the search service. You should adjust this based on your needs.
|
| Supported: "algolia", "meilisearch", "typesense",
| "database", "collection", "null"
|
*/
'driver' => env('SCOUT_DRIVER', 'meilisearch'),
/*
|--------------------------------------------------------------------------
| Index Prefix
|--------------------------------------------------------------------------
|
| Here you may specify a prefix that will be applied to all search index
| names used by Scout. This prefix may be useful if you have multiple
| "tenants" or applications sharing the same search infrastructure.
|
*/
'prefix' => env('SCOUT_PREFIX', ''),
/*
|--------------------------------------------------------------------------
| Queue Data Syncing
|--------------------------------------------------------------------------
|
| This option allows you to control if the operations that sync your data
| with your search engines are queued. When this is set to "true" then
| all automatic data syncing will get queued for better performance.
|
*/
'queue' => env('SCOUT_QUEUE', false),
/*
|--------------------------------------------------------------------------
| Database Transactions
|--------------------------------------------------------------------------
|
| This configuration option determines if your data will only be synced
| with your search indexes after every open database transaction has
| been committed, thus preventing any discarded data from syncing.
|
*/
'after_commit' => true,
/*
|--------------------------------------------------------------------------
| Chunk Sizes
|--------------------------------------------------------------------------
|
| These options allow you to control the maximum chunk size when you are
| mass importing data into the search engine. This allows you to fine
| tune each of these chunk sizes based on the power of the servers.
|
*/
'chunk' => [
'searchable' => 500,
'unsearchable' => 500,
],
/*
|--------------------------------------------------------------------------
| Soft Deletes
|--------------------------------------------------------------------------
|
| This option allows to control whether to keep soft deleted records in
| the search indexes. Maintaining soft deleted records can be useful
| if your application still needs to search for the records later.
|
*/
'soft_delete' => true,
/*
|--------------------------------------------------------------------------
| Meilisearch Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your Meilisearch settings. Meilisearch is an open
| source search engine with minimal configuration. Below, you can state
| the host and key information for your own Meilisearch installation.
|
| See: https://www.meilisearch.com/docs/learn/configuration/instance_options#all-instance-options
|
*/
'meilisearch' => [
'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
'key' => env('MEILISEARCH_KEY'),
'index-settings' => [],
],
];

View file

@ -21,7 +21,6 @@ return [
"mieux-vivre-ensemble" => "living and working together",
"respecter-la-terre" => "respecting the nature",
"bien-nourrir" => "providing healthy food",
'search' => 'search',
"news" => [
"index" => "news",
"read_article" => "read more",
@ -124,5 +123,19 @@ return [
"3_clicks" => 'I apply in 3 clicks',
"contact_details" => 'Your contact details',
'contact_our' => 'contact our',
'press_department' => 'press department'
'press_department' => 'press department',
'search' => [
'seo_title' => 'You searched for :terms',
'seo_description' => 'Search for a word on the entire LDC.fr website',
'placeholder' => 'Your search',
'results_total' => ':nbr results for ":terms"',
'no_results' => 'No results found.',
'trans' => 'search',
'query' => 'text',
'page' => 'page',
'previous' => 'Previous page',
'next' => 'Next page',
],
'cookies_management' => 'Cookies management',
'recaptcha_protect' => 'This site is protected by reCAPTCHA and the <a href="https://policies.google.com/privacy">privacy policy</a> and the <a href="https://policies.google.com/terms">terms of use</a> Google\'s terms apply.',
];

View file

@ -3,5 +3,6 @@
return [
'contact' => 'contact-us/{slug}',
'contact_index' => 'contact-us',
'news' => 'news/{news}'
'news' => 'news/{news}',
'search' => 'search',
];

View file

@ -34,7 +34,6 @@ return [
'order' => 'afficher',
],
"finance" => "finance",
'search' => 'rechercher',
'espace-presse' => 'espace presse',
'next_events' => 'prochains évènements',
'buttons' => [
@ -125,5 +124,19 @@ return [
"3_clicks" => 'Je postule en 3 clics',
"contact_details" => 'Vos coordonnées',
'contact_our' => 'contactez notre',
'press_department' => 'service presse'
'press_department' => 'service presse',
'search' => [
'seo_title' => 'Vous avez cherché :terms',
'seo_description' => 'Recherche de mot sur la globalité du site LDC.fr',
'placeholder' => 'Votre recherche',
'results_total' => ':nbr résultats pour ":terms"',
'no_results' => 'Aucun résultat trouvé.',
'trans' => 'rechercher',
'query' => 'texte',
'page' => 'page',
'previous' => 'Page précédente',
'next' => 'Page suivante',
],
'cookies_management' => 'Gestion des cookies',
'recaptcha_protect' => 'Ce site est protégé par reCAPTCHA et la <a href="https://policies.google.com/privacy">politique de confidentialité</a> et les <a href="https://policies.google.com/terms">conditions d\'utilisation</a> de Google s\'appliquent.',
];

View file

@ -3,5 +3,6 @@
return [
'contact' => 'nous-contacter/{slug}',
'contact_index' => 'nous-contacter',
'news' => 'actualites/{news}'
'news' => 'actualites/{news}',
'search' => 'recherche',
];

View file

@ -33,7 +33,9 @@ data:
GOOGLE_RECAPTCHA_PUBLIC: "${APP_GOOGLE_RECAPTCHA_PUBLIC}"
GOOGLE_RECAPTCHA_VERSION: "${APP_GOOGLE_RECAPTCHA_VERSION}"
GOOGLE_MAPS_KEY: "${APP_GOOGLE_MAPS_KEY}"
SCOUT_PREFIX: "${APP_SCOUT_PREFIX}"
MEILISEARCH_HOST: "meilisearch.common-services:7700"
MEILISEARCH_KEY: "${APP_MEILISEARCH_KEY}"
SAML2_SP_CERT_x509: "${APP_SAML2_SP_CERT_x509}"
SAML2_SP_CERT_PRIVATEKEY: "${APP_SAML2_SP_CERT_PRIVATEKEY}"
SAML2_CONTACT_TECHNICAL_EMAIL: "${APP_SAML2_CONTACT_TECHNICAL_EMAIL}"

View file

@ -2027,6 +2027,10 @@
margin: 0 -15px 40px;
padding: 30px 20px;
}
.search-form__wrapper {
display: flex;
flex-direction: column;
}
.search-form__btn {
width: 100%;
padding: 15px;

View file

@ -371,6 +371,18 @@ iframe {
margin-bottom: 0;
}
.no-top-padding-important {
padding-top: 0 !important;
}
.no-bottom-padding-important {
padding-bottom: 0 !important;
}
.no-bottom-margin-important {
margin-bottom: 0 !important;
}
.half-bottom-padding {
padding-bottom: 50px;
}
@ -688,14 +700,19 @@ form .wpcf7-form-control-wrap .wpcf7-list-item {
.search-form {
display: flex;
flex-direction: column;
background: #333333;
padding: 30px;
margin-bottom: 50px;
}
.search-form__wrapper {
display: flex;
flex-direction: row;
}
.search-form__field {
width: 890px;
max-width: 100%;
width: 100%;
}
input.search-form__input {
@ -1331,11 +1348,14 @@ textarea:focus {
.breadcrumbs {
font-size: 13px;
padding: 15px 0;
font-weight: 500;
color: #6a6a6a;
margin-bottom: 5px;
}
.breadcrumbs_base_margined {
padding: 15px 0;
margin-top: -60px;
margin-bottom: 5px;
}
.breadcrumbs_margined {
@ -3232,7 +3252,7 @@ a.copyright__item:hover {
/* page search START */
.search-page .breadcrumbs {
.search-page > .breadcrumbs {
margin-top: 60px;
margin-bottom: 70px;
}
@ -3266,7 +3286,7 @@ div.search-results {
}
.search-results__item-bc {
margin: 0 0 10px !important;
margin: 0 0 10px;
}
.search-noresults__title {
@ -3275,6 +3295,11 @@ div.search-results {
margin-bottom: 50px;
}
.search-highlight {
color: #ff4e4e;
font-weight: 700;
}
/* page search END */
/* pagination START */

View file

@ -102,10 +102,8 @@ export default function AppSidebar() {
},
{
title: useTrans("back.your_website"),
link: route().has("front.dynamic-pages.accueil." + appEnv.locale)
? route("front.dynamic-pages.accueil." + appEnv.locale)
: "",
show: route().has("front.dynamic-pages.accueil." + appEnv.locale),
link: "/",
show: true,
icon: SquareArrowOutUpRightIcon,
},
{

View file

@ -108,7 +108,7 @@
{{-- # FIXME --}}
<span>Pour toute information complémentaire concernant vos données personnelles et vos
droits, merci de consulter notre <a class="cn-link"
href="{{ \App\Helpers\RouteHelper::routeNameChecker('front.dynamique-pages.politique-de-confidentialite') }}">Charte de
href="{{ \App\Helpers\RouteHelper::routeNameChecker('front.dynamic-pages.politique-de-confidentialite') }}">Charte de
confidentialité</a>.</span>
</div>
</form>

View file

@ -0,0 +1,28 @@
@if ($models->lastPage() > 1)
<div class="pgntn-page-pagination pgntn-bottom">
<div class="pgntn-page-pagination-block">
{{-- Page précédente --}}
@if (!$models->onFirstPage())
<a class="page-numbers prev" href="{{ $models->previousPageUrl() }}">
{{ trans('front.search.previous')}}
</a>
@endif
{{-- Pages --}}
@for ($page = 1; $page <= $models->lastPage(); $page++)
@if ($page == $models->currentPage())
<span aria-current="page" class="page-numbers current">{{ $page }}</span>
@else
<a class="page-numbers" href="{{ $models->url($page) }}">
{{ $page }}
</a>
@endif
@endfor
{{-- Page suivante --}}
@if ($models->hasMorePages())
<a class="page-numbers next" href="{{ $models->nextPageUrl() }}">
{{ trans('front.search.next')}}
</a>
@endif
</div>
</div>
@endif

View file

@ -1,19 +0,0 @@
{{-- # FIXME: ajouter une class pour remplacer le style inline sur les concordances de résultats --}}
<a class="search-results__item" href="{{ $url }}"
title="Le Gaulois, Maître Coq, Loué… il est le champion du <span style='color: #ff4e4e; font-weight: 700;'>poulet</span> toutes catégories">
@if (isset($title))
<div class="search-results__item-title">{!! $title !!}</div>
@endif
{{-- # FIXME: voir comment seront passés et récpérés les breadcrumb --}}
<div class="search-results__item-bc breadcrumbs">
<span>LDC</span>
&gt; <span>Espace presse</span>
&gt; <span>Le Gaulois, Maître Coq, Loué… il est le champion du poulet toutes catégories</span>
</div>
@if (isset($description))
<div class="search-results__item-text description">
<p>{!! $description !!}</p>
</div>
@endif
</a>

View file

@ -1,5 +1,5 @@
@if ($breadcrumbs->isNotEmpty() && request()->route()->getName() !== 'front.dynamic-pages.accueil.' . app()->getLocale())
<div class="breadcrumbs fw">
<div class="breadcrumbs breadcrumbs_base_margined fw">
<div class="breadcrumbs__wrapper wrapper">
@foreach ($breadcrumbs as $breadcrumb)
@if ($breadcrumb->url && !$loop->last)

View file

@ -58,9 +58,11 @@
</a>
</li>
@endif
<li id="menu-item-199" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-199">
<a href="{{ \App\Helpers\RouteHelper::routeNameChecker('front.finance') }}">Finance</a>
</li>
@if (isset($pageFinance))
<li id="menu-item-199" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-199">
<a href="{{ \App\Helpers\RouteHelper::routeNameChecker('front.finance') }}">{{ $pageFinance->title }}</a>
</li>
@endif
@if (isset($pageNosMetiers))
<li id="menu-item-6107" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-6107">
<a href="{{ \App\Helpers\RouteHelper::routeNameChecker('front.dynamic-pages.nos-metiers') }}">{{ $pageNosMetiers->title }}</a>
@ -68,33 +70,40 @@
@endif
<li id="menu-item-6108" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-6108">
<a href="{{ LaravelLocalization::getLocalizedURL(app()->getLocale(), __('routes.contact_index')) }}">
{{ ucfirst(__('front.contact.index')) }}</a>
{{ ucfirst(__('front.contact.index')) }}
</a>
</li>
</ul>
<div class="footer__social footer-social">
<div class="footer-social__title">{{ ucwords(__('front.follow_us')) }}</div>
<div class="footer-social__list">
<a class="footer-social__list-item" href="https://www.linkedin.com/company/767509" target="_blank" rel="noreferer noopener" title="Linked In"><img src="/assets/img/icon__linkedin.png" alt="LinkedIn" /></a>
<a class="footer-social__list-item" href="https://www.linkedin.com/company/767509" target="_blank" rel="noreferer noopener" title="Linked In">
<img src="/assets/img/icon__linkedin.png" alt="LinkedIn" />
</a>
</div>
</div>
</div>
<div class="footer__copyright copyright wrapper">
<a class="copyright__item" href="/mentions-legales/" title="Mentions légales">Mentions légales et CGU</a>
@if (isset($pageMentionsLegales))
<a class="copyright__item" href="{{ \App\Helpers\RouteHelper::routeNameChecker('front.dynamic-pages.mentions-legales') }}"
title="{{ $pageMentionsLegales->title }}">
{{ $pageMentionsLegales->title }}
</a>
@endif
<span class="copyright__item">-</span>
<a class="copyright__item" href="{{ \App\Helpers\RouteHelper::routeNameChecker('front.dynamique-pages.politique-de-confidentialite') }}"
title="Politique de confidentialité">Politique de confidentialité</a>
@if (isset($pagePolitiqueDeConfidentialite))
<a class="copyright__item" href="{{ \App\Helpers\RouteHelper::routeNameChecker('front.dynamic-pages.politique-de-confidentialite') }}"
title="{{ $pagePolitiqueDeConfidentialite->title }}">
{{ $pagePolitiqueDeConfidentialite->title }}
</a>
@endif
<span class="copyright__item">-</span>
<a class="copyright__item cn-cookie-link" href="#" title="Gestion des cookies">Gestion des cookies</a>
<a class="copyright__item cn-cookie-link" href="#" title="{{ trans('front.cookies_management') }}">{{ trans('front.cookies_management') }}</a>
<span class="copyright__item">-</span>
<span class="copyright__item">Copyright LDC &copy; {{ date('Y') }}</span>
<br><br>
<span class="copyright__item">
Ce site est protégé par reCAPTCHA et la
<a href="https://policies.google.com/privacy">politique de confidentialité</a> et les
<a href="https://policies.google.com/terms">conditions d'utilisation</a> de Google s'appliquent.
</span>
<span class="copyright__item">{!! nl2br(trans('front.recaptcha_protect')) !!}</span>
</div>
</footer>

View file

@ -298,9 +298,9 @@
</ul>
<div class="header__search-btn"></div>
<div class="header__search header-search sub-menu">
<form class="header__search-form header-search-form" method="get" action="/">
<input class="header-search-form__input form__input" type="text" name="s"
placeholder="Votre recherche" value="">
<form class="header__search-form header-search-form" method="get" action="{{ route('front.search') }}">
<input class="header-search-form__input form__input" type="text" name="{{ trans('front.search.query') }}"
placeholder="{{ trans('front.search.placeholder') }}" value="">
<button class="header-search-form__btn" type="submit"></button>
</form>
</div>

View file

@ -7,9 +7,9 @@
<div class="header__mobile-icon-block">
<div class="header__search-btn"></div>
<div class="header__search header-search sub-menu">
<form class="header__search-form header-search-form" method="get" action="/">
<input class="header-search-form__input form__input" type="text" name="s"
placeholder="Votre recherche" value="{{ request()->input('s') }}">
<form class="header__search-form header-search-form" method="get" action="{{ route('front.search') }}">
<input class="header-search-form__input form__input" type="text" name="{{ trans('front.search.query') }}"
placeholder="{{ trans('front.search.placeholder') }}" value="">
<button class="header-search-form__btn" type="submit"></button>
</form>
</div>
@ -18,9 +18,7 @@
@if (!$loop->first)
<span class="langs__separator"> / </span>
@endif
@php
$url = \App\Helpers\RouteHelper::getLocalizedUrl($localeCode);
@endphp
@php $url = \App\Helpers\RouteHelper::getLocalizedUrl($localeCode); @endphp
<a rel="alternate" hreflang="{{ $localeCode }}" href="{{ $url }}"
class="langs__item {{ app()->getLocale() === $localeCode ? 'active' : '' }}">
{{ strtoupper($localeCode) }}
@ -87,16 +85,20 @@
</a>
</li>
@endif
@if (isset($pageFinance))
<li class="mobile-menu__item">
<a class="mobile-menu__link"
href="{{ \App\Helpers\RouteHelper::routeNameChecker('front.dynamic-pages.finance') }}">{{ $pageFinance->title }}</a>
<a class="menu-arrow-box mobile-menu__link_hassub" data-submenu="submenu-2943" href="#">
<div class="menu-submenu-arrow"></div>
</a>
</li>
@endif
<li class="mobile-menu__item">
<a class="mobile-menu__link"
href={{ \App\Helpers\RouteHelper::routeNameChecker('front.dynamic-pages.finance') }}">Finance</a>
<a class="menu-arrow-box mobile-menu__link_hassub" data-submenu="submenu-2943" href="#">
<div class="menu-submenu-arrow"></div>
<a class="mobile-menu__link" href="{{ LaravelLocalization::getLocalizedURL(app()->getLocale(), __('routes.contact_index')) }}">
{{ trans('front.contact.index') }}
</a>
</li>
<li class="mobile-menu__item">
<a class="mobile-menu__link" href="/nous-contacter/">Nous contacter</a>
</li>
</ul>
<div class="mobile-menu-footer">
<div class="link-title">PRATIQUE</div>
@ -109,9 +111,13 @@
</div>
@endif
<!-- <div class="mobile-menu__item"><a class="mobile-menu__link" href="/sitemap.xml">Plan du site</a></div>-->
<div class="mobile-menu__item">
<a class="mobile-menu__link" href="/mentions-legales/">Mentions légales</a>
</div>
@if (isset($pageMentionsLegales))
<div class="mobile-menu__item">
<a class="mobile-menu__link" href="{{ \App\Helpers\RouteHelper::routeNameChecker('front.dynamic-pages.mentions-legales') }}">
{{ $pageMentionsLegales->title }}
</a>
</div>
@endif
<div class="mobile-menu-footer-black">
<div class="footer__social footer-social">
<div class="footer-social__title">Suivez-nous</div>
@ -124,8 +130,12 @@
</div>
<div class="footer__copyright copyright wrapper">
<!-- <a class="copyright__item" href="/sitemap.xml" target="_blank" rel="noreferer noopener" title="Plan du site">Plan du site</a>-->
<a class="copyright__item" href="/mentions-legales/" title="Mentions légales">Mentions
légales</a>
@if (isset($pageMentionsLegales))
<a class="copyright__item" href="{{ \App\Helpers\RouteHelper::routeNameChecker('front.dynamic-pages.mentions-legales') }}"
title="{{ $pageMentionsLegales->title }}">
{{ $pageMentionsLegales->title }}
</a>
@endif
<span class="copyright__item">Copyright LDC © 2025</span>
</div>
</div>
@ -329,7 +339,7 @@
<span class="form__field-label-text">J’ai lu et accepté la politique de
confidentialité du site LDC, les conditions d’utilisation et son utilisation
des cookies.<br>Lire <a
href="{{ \App\Helpers\RouteHelper::routeNameChecker('front.dynamique-pages.politique-de-confidentialite') }}"
href="{{ \App\Helpers\RouteHelper::routeNameChecker('front.dynamic-pages.politique-de-confidentialite') }}"
target="_blank" rel="noreferer noopener">
la politique de confidentialité
</a>

View file

@ -12,7 +12,8 @@
<div class="single-news__tags tags">
<div class="single-news__tag tag" data-color="{{ $model->first_tag_data['color'] ?? '' }}"
data-color-target="border">
{{ $model['tag_labels'] }}</div>
{{ $model['tag_labels'] }}
</div>
</div>
@endif
<h1 class="single-news__title subtitle">

View file

@ -1,73 +1,71 @@
@extends('front.layouts.layout')
@section('title', 'Rechercher sur LDC.fr')
@section('description', 'Rechercher sur LDC.fr')
@section('title', trans('front.search.seo_title', ['terms' => $searchTerm]))
@section('description', trans('front.search.seo_description'))
@section('content')
{{-- #FIXME: rien n'est réellement dynamique ici tout sera à faire, il n'y qu'un template --}}
@php
$countResults = 2;
$results = [[], []];
$searchTerm = 'poulet';
@endphp
<div class="search-page fw">
@include('front.breadcrumbs.breadcrumb-body')
<div class="search-page__wrapper wrapper">
<h1 class="search-results-count subtitle">{{ $countResults }} résultats pour "{{$searchTerm}}"</h1>
<h2 class="search-results-perpage">Voir 1 - 10 de {{ $countResults }} résultats</h2>
<form class="form search-form" id="searchform" method="get" action="#FIXME">
<div class="search-form__field"><input class="search-form__input form__input" type="text" name="s"
placeholder="Rechercher" value="{{ $searchTerm }}"></div>
<button class="search-form__btn" type="submit"><img class="search-form__btn-icon"
src="/assets/img/search-icon_white.png" alt="Rechercher"></button>
</form>
<div class="search-results">
@forelse($results as $result)
@php
$title = nl2br(
__(
'Le Gaulois, Maître Coq, Loué… il est le champion du :spanStart poulet :spanEnd toutes catégories',
[
'spanStart' => '<span style="color: #ff4e4e; font-weight: 700;">',
'spanEnd' => '</span>',
],
),
);
$description = nl2br(
__(
'<span class="excerpt_part">https://www.capital.fr/entreprises-marches/le-gaulois-maitre-coq-loue-il-est-le-champion-du-:spanStart poulet:spanEnd-toutes-categories-1352376 1 https://www.capital.fr/entreprises-marches/le-gaulois-maitre-coq-loue-il-est-le-champion-du-:spanStartpoulet:spanEnd-toutes-categories-1352376 1 1 1 1 1…:spanEnd',
[
'spanStart' => '<span style="color: #ff4e4e; font-weight: 700;">',
'spanEnd' => '</span>',
],
),
);
@endphp
<x-front.search-result-card
url="https://www.ldc.fr/le-gaulois-maitre-coq-loue-il-est-le-champion-du-poulet-toutes-categories/"
:description="$description" :title="$title" />
@empty
<x-front.search-result-card url="" description="" title="" />
@endforelse
<div class="pgntn-page-pagination
pgntn-bottom ">
<div class="pgntn-page-pagination-block">
<span aria-current="page" class="page-numbers current">1</span>
<a class="page-numbers" href="https://www.ldc.fr/page/2/?s=poulet">2</a>
<a class="page-numbers" href="https://www.ldc.fr/page/3/?s=poulet">3</a>
<a class="next page-numbers" href="https://www.ldc.fr/page/2/?s=poulet">Page suivante</a>
</div>
<div class="clear"></div>
<h1 class="search-results-count subtitle">{{ trans('front.search.results_total', ['nbr' => $resultModels['totalHits'], 'terms' => $searchTerm]) }}</h1>
<form class="form search-form" id="searchform" method="get" action="{{ route('front.search') }}">
<div class="search-form__wrapper">
<div class="search-form__field"><input class="search-form__input form__input" type="text" name="{{ trans('front.search.query') }}"
placeholder="{{ trans('front.search.placeholder')}}" value="{{ $searchTerm }}"></div>
<button class="search-form__btn" type="submit"><img class="search-form__btn-icon"
src="/assets/img/search-icon_white.png" alt="{{ trans('front.search.trans')}}"></button>
</div>
@error(trans('front.search.query'))
<div class="form__error">{{ $message }}</div>
@enderror
</form>
<div class="search-results">
@forelse($resultModels['hits'] as $result)
@php
$locale = app()->getLocale();
$hasDescription = isset($result['content']) && !is_null($result['content']) && !empty($result['content']);
@endphp
@php
if ($result['type'] === \Illuminate\Support\Str::snake(class_basename(\App\Models\DynamicPage::class))) {
$url = \App\Models\DynamicPage::getAccessModel($result['model_id']);
$routeName = sprintf('front.dynamic-pages.%s.%s', $result['id_key'], $locale);
$customBreadcrumbs = false;
} elseif ($result['type'] === \Illuminate\Support\Str::snake(class_basename(\App\Models\News::class))) {
$url = \App\Models\News::getAccessModel($result['model_id']);
$routeName = 'front.news.show';
$customBreadcrumbs = true;
} elseif ($result['type'] === \Illuminate\Support\Str::snake(class_basename(\App\Models\Document::class))) {
$url = \App\Models\Document::getAccessModel($result['model_id']);
$routeName = 'front.documents.show';
$customBreadcrumbs = true;
}
@endphp
<a class="search-results__item" href="{{ $url }}">
<div class="search-results__item-title">{!! \App\Models\SearchIndex::highlightTerms($result['title'], $searchTerm) !!}</div>
<div @class([
'no-bottom-margin-important no-bottom-padding-important' => !$hasDescription,
'breadcrumbs breadcrumbs_base_margined search-results__item-bc' => true,
])>
@foreach(\Diglactic\Breadcrumbs\Breadcrumbs::generate($routeName)->toArray() as $breacrumb)
<span>{{ $breacrumb->title }}</span>
@if(!$loop->last)
&gt;
@endif
@endforeach
@if($customBreadcrumbs)
<span>&gt; {{ $result['title'] }}</span>
@endif
</div>
@if ($hasDescription)
<div class="search-results__item-text description">
<p>{!! \App\Models\SearchIndex::highlightTerms($result['content'], $searchTerm) !!}</p>
</div>
@endif
</a>
@empty
<p>{{ trans('front.search.no_results')}}</p>
@endforelse
<x-front.search-pagination :models="$resultModels" />
</div>
</div>
</div>

View file

@ -20,11 +20,7 @@ $strings = (object)[
'button_allow_all_title' => __('cookie-notice.button_allow_all_title'),
'button_validate_and_close' => str(__('cookie-notice.button_validate_and_close'))->upper(),
'banner_title' => __('cookie-notice.banner_title', ['site_name' => $site_name]),
'banner_text' => sprintf(
"%s<br><br>%s",
"Nous utilisons des cookies pour nous assurer du bon fonctionnement de notre site et à des fins analytiques. Vous pouvez changer d'avis à tout moment en cliquant sur l'icône présente sur chaque page de notre site. En autorisant ces services tiers, vous acceptez le dépôt et la lecture de cookies et l'utilisation de technologies de suivi nécessaires à leur bon fonctionnement.",
"<a href='/charte-de-confidentialite'>Charte de confidentialité</a>"
),
'banner_text' => __('cookie-notice.banner_text'),
'panel_title' => __('cookie-notice.panel_title'),
'panel_content' => __('cookie-notice.panel_content'),
'panel_subtitle' => __('cookie-notice.panel_subtitle'),
@ -33,6 +29,14 @@ $strings = (object)[
'navigation_title' => __('cookie-notice.navigation_title'),
];
if (isset($pagePolitiqueDeConfidentialite)) {
$strings->banner_text .=sprintf(
'<br><br><a href="%s">%s</a>',
\App\Helpers\RouteHelper::routeNameChecker('front.dynamic-pages.politique-de-confidentialite'),
$pagePolitiqueDeConfidentialite->title,
);
}
$cookieInline = function ($cookieName, $text, $subtext = '') {
return <<<SCRIPT
(() => {

View file

@ -12,24 +12,30 @@ use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
$currentLocale = app()->getLocale();
$supportedLocales = LaravelLocalization::getSupportedLanguagesKeys();
// * HOMEPAGE.
$pageHome = DynamicPage::getModelsCached()
$pageHome = DynamicPage::getModelsCached()
->where('status', PageStatusEnum::published)
->where('id_key', 'accueil')
->first();
if ($pageHome && !request()->routeIs("front.dynamic-pages.accueil.$currentLocale")) {
foreach ($supportedLocales as $key => $locale) {
Breadcrumbs::for("front.dynamic-pages.accueil.$locale", function (Generator $trail) use ($pageHome) {
$trail->push($pageHome->title, RouteHelper::routeNameChecker('front.dynamic-pages.accueil'));
});
}
}
// * CONTACT PAGE
// * DYNAMIC PAGES.
DynamicPage::getModelsCached()->map(function (DynamicPage $dynamicPageModel) use ($currentLocale, $pageHome) {
Breadcrumbs::for(
sprintf("front.dynamic-pages.%s.%s", $dynamicPageModel->id_key, $currentLocale),
function (Generator $trail) use ($currentLocale, $dynamicPageModel, $pageHome) {
if (!is_null($dynamicPageModel->page_id)) {
$trail->parent(sprintf("front.dynamic-pages.%s.%s", $dynamicPageModel->parentPage->id_key, $currentLocale));
} elseif ($dynamicPageModel->id_key !== $pageHome->id_key) {
$trail->parent(sprintf("front.dynamic-pages.%s.%s", $pageHome->id_key, $currentLocale));
}
$trail->push($dynamicPageModel->title, RouteHelper::routeNameChecker(sprintf("front.dynamic-pages.%s", $dynamicPageModel->id_key)));
}
);
});
// * CONTACT INDEX.
Breadcrumbs::for('front.contact.index', function (Generator $trail) use ($pageHome, $currentLocale) {
if ($pageHome) {
$trail->parent("front.dynamic-pages.accueil.$currentLocale");
$trail->parent(sprintf("front.dynamic-pages.%s.%s", $pageHome->id_key, $currentLocale));
}
$trail->push(
Str::of(trans('front.contact.index'))->ucfirst(),
@ -37,7 +43,7 @@ Breadcrumbs::for('front.contact.index', function (Generator $trail) use ($pageHo
);
});
// * CONTACT PAGE
// * CONTACT SHOW.
Breadcrumbs::for('front.contact.show', function (Generator $trail) {
$trail->parent('front.contact.index');
$slug = \request()->route()->parameters['slug'];
@ -52,151 +58,37 @@ Breadcrumbs::for('front.contact.show', function (Generator $trail) {
}
});
// * SEARCH PAGE
// * SEARCH INDEX.
Breadcrumbs::for('front.search', function (Generator $trail) use ($pageHome, $currentLocale) {
if ($pageHome) {
$trail->parent("front.dynamic-pages.accueil.$currentLocale");
}
$trail->push(
Str::of(trans('front.search'))->ucfirst(),
Str::of(trans('front.search.trans'))->ucfirst(),
route('front.search')
);
});
// * ESPACE PRESSE.
$pageEspacePresse = DynamicPage::getModelsCached()
->where('status', PageStatusEnum::published)
->where('id_key', 'espace-presse')
->first();
if ($pageEspacePresse) {
foreach ($supportedLocales as $key => $locale) {
Breadcrumbs::for("front.dynamic-pages.espace-presse.$locale", function (Generator $trail) use ($pageEspacePresse, $pageHome, $locale) {
if ($pageHome) {
$trail->parent("front.dynamic-pages.accueil.$locale");
}
$trail->push($pageEspacePresse->title);
});
}
}
// * ENGAGEMENTS PAGES.
$pageOurCommitments = DynamicPage::getModelsCached()
->where('status', PageStatusEnum::published)
->where('id_key', 'nos-engagements')
->first();
if ($pageOurCommitments) {
foreach ($supportedLocales as $key => $locale) {
Breadcrumbs::for("front.dynamic-pages.nos-engagements.$locale", function (Generator $trail) use ($pageOurCommitments, $pageHome, $locale) {
if ($pageHome) {
$trail->parent("front.dynamic-pages.accueil.$locale");
}
$trail->push($pageOurCommitments->title, RouteHelper::routeNameChecker('front.dynamic-pages.nos-engagements'));
});
foreach ($pageOurCommitments->childPages as $key => $childPage) {
Breadcrumbs::for("front.dynamic-pages.$childPage->id_key.$locale", function (Generator $trail) use ($childPage, $locale) {
$trail->parent("front.dynamic-pages.nos-engagements.$locale");
$trail->push($childPage->title);
});
}
}
}
// * KNOW US PAGES.
$pageKnowUs = DynamicPage::getModelsCached()
->where('status', PageStatusEnum::published)
->where('id_key', 'nous-connaitre')
->first();
if ($pageKnowUs) {
foreach ($supportedLocales as $key => $locale) {
Breadcrumbs::for("front.dynamic-pages.nous-connaitre.$locale", function (Generator $trail) use ($pageKnowUs, $pageHome, $locale) {
if ($pageHome) {
$trail->parent("front.dynamic-pages.accueil.$locale");
}
$trail->push($pageKnowUs->title);
});
}
}
// * OUR BRANDS PAGE.
$pageNosMarques = DynamicPage::getModelsCached()
->where('status', PageStatusEnum::published)
->where('id_key', 'nos-marques')
->first();
if ($pageNosMarques) {
foreach ($supportedLocales as $key => $locale) {
Breadcrumbs::for("front.dynamic-pages.nos-marques.$locale", function (Generator $trail) use ($pageNosMarques, $pageHome, $locale) {
if ($pageHome) {
$trail->parent("front.dynamic-pages.accueil.$locale");
}
$trail->push($pageNosMarques->title);
});
}
}
// * JOIN US.
$pageNousRejoindre = DynamicPage::getModelsCached()
->where('status', PageStatusEnum::published)
->where('id_key', 'nous-rejoindre')
->first();
if ($pageNousRejoindre) {
foreach ($supportedLocales as $key => $locale) {
Breadcrumbs::for("front.dynamic-pages.nous-rejoindre.$locale", function (Generator $trail) use ($pageNousRejoindre, $pageHome, $locale) {
if ($pageHome) {
$trail->parent("front.dynamic-pages.accueil.$locale");
}
$trail->push($pageNousRejoindre->title);
});
}
}
// * OUR JOBS.
$pageNosMetiers = DynamicPage::getModelsCached()
->where('status', PageStatusEnum::published)
->where('id_key', 'nos-metiers')
->first();
if ($pageNosMetiers) {
foreach ($supportedLocales as $key => $locale) {
Breadcrumbs::for("front.dynamic-pages.nos-metiers.$locale", function (Generator $trail) use ($pageNosMetiers, $pageHome, $locale) {
if ($pageHome) {
$trail->parent("front.dynamic-pages.accueil.$locale");
}
$trail->push($pageNosMetiers->title);
});
}
}
// * NEWS.
$pageNews = DynamicPage::getModelsCached()
// NEWS SHOW.
$pageActualites = DynamicPage::getModelsCached()
->where('status', PageStatusEnum::published)
->where('id_key', 'actualites')
->first();
if ($pageNews) {
foreach ($supportedLocales as $key => $locale) {
Breadcrumbs::for("front.dynamic-pages.actualites.$locale", function (Generator $trail) use ($pageNews, $pageHome, $currentLocale) {
if ($pageHome) {
$trail->parent("front.dynamic-pages.accueil.$currentLocale");
}
$trail->push($pageNews->title, RouteHelper::routeNameChecker('front.dynamic-pages.actualites'));
});
Breadcrumbs::for('front.news.show', function (Generator $trail) use ($currentLocale, $pageActualites) {
if ($pageActualites) {
$trail->parent(sprintf("front.dynamic-pages.%s.%s", $pageActualites->id_key, $currentLocale));
}
// SHOW
Breadcrumbs::for('front.news.show', function (Generator $trail) use ($locale) {
$trail->parent("front.dynamic-pages.actualites.$locale");
if (request()->news instanceof News) {
$trail->push(
Str::of(request()->news->title)->ucfirst()
);
}
});
}
if (request()->news instanceof News) {
$trail->push(
Str::of(request()->news->title)->ucfirst()
);
}
});
// * FINANCE.
Breadcrumbs::for('front.finance', function (Generator $trail) use ($pageHome, $currentLocale) {
// * DOCUMENT SHOW.
// Need it for the search page.
Breadcrumbs::for('front.documents.show', function (Generator $trail) use ($pageHome, $currentLocale) {
if ($pageHome) {
$trail->parent("front.dynamic-pages.accueil.$currentLocale");
}
$trail->push(
Str::of(trans('front.finance'))->ucfirst(),
route('front.finance')
);
});
});

View file

@ -1,38 +1,41 @@
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Front\ContactController;
use App\Http\Controllers\Front\HomeController;
use App\Http\Controllers\Front\NewsController;
use App\Http\Controllers\Front\PostsController;
use App\Http\Controllers\Front\ContactController;
use App\Http\Controllers\Front\SearchController;
use Illuminate\Support\Facades\Route;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
Route::group(
[
'prefix' => LaravelLocalization::setLocale(),
'as' => 'front.',
'as' => 'front.',
],
function () {
// Contact routes.
Route::name('contact.')->controller(ContactController::class)->group(function () {
Route::get(LaravelLocalization::transRoute('routes.contact_index'), 'index')->name('index');
Route::get(LaravelLocalization::transRoute('routes.contact'), 'show')->defaults('contactPage', 'contactPage')->name('show');
Route::post('/nous-contacter', 'send')->name('send');
});
// * Unsubscribe from newsletter
// Newsletter routes.
Route::get('/{unsubscribe}', [HomeController::class, 'unsubscribeNewsletter'])
->where('unsubscribe', 'unsubscribe|unsubscribe.php|unsubscribe.html|newsletter')
->name('unsubscribeNewsletter');
Route::get('search', function () {
return view('front.pages.search');
})->name('search');
// Search routes.
Route::get(LaravelLocalization::transRoute('routes.search'), [SearchController::class, 'search'])->name('search');
// Filters routes.
Route::prefix('posts')->group(function () {
Route::post('/load/news', [PostsController::class, 'ajaxLoadNews'])->name('posts.ajax.news');
Route::post('/load/documents', [PostsController::class, 'ajaxLoadDocuments'])->name('posts.ajax.documents');
});
// News routes.
Route::get(LaravelLocalization::transRoute('routes.news'), [NewsController::class, 'show'])->name('news.show');
}
);