diff --git a/.gitignore b/.gitignore index 024971a..fa5197b 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ backup/*.sql /public/images/**/*.webp /public/images/*/*.png /public/pdf/**/*.pdf +/public/seo/*.xml diff --git a/ansible/playbook.yml b/ansible/playbook.yml index db0694e..05b613f 100644 --- a/ansible/playbook.yml +++ b/ansible/playbook.yml @@ -109,6 +109,7 @@ - "{{ path }}/public/media" # For uploads - "{{ path }}/public/images" # For uploads - "{{ path }}/public/pdf" # For uploads + - "{{ path }}/public/seo" # For uploads - "{{ path }}/public/tmp-sign" # For upload - "{{ path }}/sauvegarde" @@ -248,5 +249,6 @@ - "{{ path }}/sauvegarde" - "{{ path }}/public/images" # For uploads - "{{ path }}/public/pdf" + - "{{ path }}/public/seo" - "{{ path }}/public/tmp-sign" # For uploads diff --git a/config/packages/messenger.yaml b/config/packages/messenger.yaml index a6ea6fe..7772ce6 100644 --- a/config/packages/messenger.yaml +++ b/config/packages/messenger.yaml @@ -5,4 +5,4 @@ framework: async: "%env(MESSENGER_TRANSPORT_DSN)%" routing: - + Presta\SitemapBundle\Messenger\DumpSitemapMessage: async diff --git a/config/packages/presta_sitemap.yaml b/config/packages/presta_sitemap.yaml index be8e2b5..ae36997 100644 --- a/config/packages/presta_sitemap.yaml +++ b/config/packages/presta_sitemap.yaml @@ -1,2 +1,3 @@ presta_sitemap: sitemap_file_prefix: 'sitemap' + dump_directory: '%kernel.project_dir%/public/seo' diff --git a/config/routes.yaml b/config/routes.yaml index 5ab0dbc..db1dd29 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -6,17 +6,7 @@ controllers: type: attribute -PrestaSitemapBundle_index: - path: "/sa2/%presta_sitemap.sitemap_file_prefix%.xml" - defaults: { _controller: Presta\SitemapBundle\Controller\SitemapController::indexAction } - requirements: - _format: xml -PrestaSitemapBundle_section: - path: "/sa2/%presta_sitemap.sitemap_file_prefix%.{name}.{_format}" - defaults: { _controller: Presta\SitemapBundle\Controller\SitemapController::sectionAction } - requirements: - _format: xml 2fa_login: path: /2fa diff --git a/public/seo/.gitignore b/public/seo/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/src/Command/BackupCommand.php b/src/Command/BackupCommand.php index 5e9f7da..bae967f 100644 --- a/src/Command/BackupCommand.php +++ b/src/Command/BackupCommand.php @@ -41,6 +41,7 @@ class BackupCommand extends Command $sqlDumpPath = $projectDir . '/var/temp_db.sql'; $imagesDir = $projectDir . '/public/images'; $pdfDir = $projectDir . '/public/pdf'; + $seoDif = $projectDir . '/public/seo'; try { if (!is_dir($backupDir)) { @@ -85,7 +86,10 @@ class BackupCommand extends Command $io->text('Compression du dossier pdf...'); $this->addFolderToZip($pdfDir, $zip, 'pdf'); } - + if (is_dir($seoDif)) { + $io->text('Compression du dossier seo...'); + $this->addFolderToZip($seoDif, $zip, 'seo'); + } $zip->close(); } else { throw new \Exception("Impossible d'initialiser le fichier ZIP."); diff --git a/src/Controller/Dashboard/ProductController.php b/src/Controller/Dashboard/ProductController.php index b511525..5d0cc09 100644 --- a/src/Controller/Dashboard/ProductController.php +++ b/src/Controller/Dashboard/ProductController.php @@ -18,10 +18,12 @@ use App\Service\Mailer\Mailer; use App\Service\Stripe\Client; use Doctrine\ORM\EntityManagerInterface; use Knp\Component\Pager\PaginatorInterface; +use Presta\SitemapBundle\Messenger\DumpSitemapMessage; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -61,7 +63,8 @@ class ProductController extends AbstractController EntityManagerInterface $entityManager, AppLogger $appLogger, Client $client, - Request $request + Request $request, + MessageBusInterface $messageBus, ): Response { $appLogger->record('VIEW', 'Consultation page création d\'un produit'); @@ -85,6 +88,7 @@ class ProductController extends AbstractController // Message flash de succès $this->addFlash('success', sprintf('Le produit "%s" a été ajouté au catalogue avec succès.', $productName)); + $messageBus->dispatch(new DumpSitemapMessage()); // Redirection vers le listing des produits return $this->redirectToRoute('app_crm_product'); } diff --git a/src/Controller/ReserverController.php b/src/Controller/ReserverController.php index 043cb9b..1162195 100644 --- a/src/Controller/ReserverController.php +++ b/src/Controller/ReserverController.php @@ -4,6 +4,7 @@ namespace App\Controller; use App\Entity\Account; use App\Entity\AccountResetPasswordRequest; +use App\Entity\Product; use App\Form\RequestPasswordConfirmType; use App\Form\RequestPasswordRequestType; use App\Logger\AppLogger; @@ -40,7 +41,7 @@ class ReserverController extends AbstractController $robots->disallow('/payment'); $robots->crawlDelay(60); $robots->allow('/reservation'); - $robots->sitemap($request->getSchemeAndHttpHost().$this->generateUrl('PrestaSitemapBundle_index',['_format' => 'xml'])); + $robots->sitemap($request->getSchemeAndHttpHost().'/seo/sitemap.xml'); return new Response($robots->toString(),Response::HTTP_OK,[ 'Content-Type' => 'text/plain' @@ -63,10 +64,30 @@ class ReserverController extends AbstractController ]); } #[Route('/reservation/produit/{id}', name: 'reservation_product_show')] - public function revervationShowProduct(ProductRepository $productRepository): Response + public function revervationShowProduct(string $id, ProductRepository $productRepository): Response { + // 1. Extraction de l'ID (ex: "15-chateau-fort" -> 15) + $parts = explode('-', $id); + $realId = $parts[0]; // Récupère le tout premier élément (l'index 0) + // 2. Récupération du produit par son ID numérique + $product = $productRepository->find($realId); + if (!$product) { + throw $this->createNotFoundException('Produit introuvable'); + } + + // 3. Logique des suggestions (inchangée) + $allInCat = $productRepository->findBy(['category' => $product->getCategory()], [], 5); + + $otherProducts = array_filter($allInCat, function($p) use ($product) { + return $p->getId() !== $product->getId(); + }); + + return $this->render('revervation/produit.twig', [ + 'product' => $product, + 'otherProducts' => array_slice($otherProducts, 0, 4) + ]); } #[Route('/reservation/contact', name: 'reservation_contact')] public function revervationContact(Request $request, Mailer $mailer): Response diff --git a/src/Entity/Product.php b/src/Entity/Product.php index 8d0b140..ec223ae 100644 --- a/src/Entity/Product.php +++ b/src/Entity/Product.php @@ -3,6 +3,7 @@ namespace App\Entity; use App\Repository\ProductRepository; +use Cocur\Slugify\Slugify; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -72,6 +73,12 @@ class Product $this->devisLines = new ArrayCollection(); $this->productReserves = new ArrayCollection(); } + public function slug() + { + $s = new Slugify(); + + return$s->slugify($this->id."-".$this->name); + } public function getId(): ?int { diff --git a/src/Security/SiteMapListener.php b/src/Security/SiteMapListener.php index ac72c62..d38fec4 100644 --- a/src/Security/SiteMapListener.php +++ b/src/Security/SiteMapListener.php @@ -2,6 +2,7 @@ namespace App\Security; +use App\Repository\ProductRepository; use Presta\SitemapBundle\Event\SitemapPopulateEvent; use Presta\SitemapBundle\Sitemap\Url\UrlConcrete; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; @@ -20,6 +21,10 @@ use Twig\Environment; class SiteMapListener implements EventSubscriberInterface { + public function __construct(private readonly ProductRepository $productRepository) + { + } + public static function getSubscribedEvents() { @@ -35,20 +40,24 @@ class SiteMapListener implements EventSubscriberInterface $t = new \DateTime(); $rv = new UrlConcrete($urlGenerator->generate('reservation',[], UrlGeneratorInterface::ABSOLUTE_URL),$t,UrlConcrete::CHANGEFREQ_DAILY,1); - $urlContainer->addUrl($rv,'revervation'); + $urlContainer->addUrl($rv,'reservation'); $rv = new UrlConcrete($urlGenerator->generate('reservation_contact',[], UrlGeneratorInterface::ABSOLUTE_URL),$t,UrlConcrete::CHANGEFREQ_DAILY,1); - - $urlContainer->addUrl($rv,'revervation'); + $urlContainer->addUrl($rv,'reservation'); $rv = new UrlConcrete($urlGenerator->generate('reservation_cookies',[], UrlGeneratorInterface::ABSOLUTE_URL),$t,UrlConcrete::CHANGEFREQ_WEEKLY,0.5); - $urlContainer->addUrl($rv,'revervation'); + $urlContainer->addUrl($rv,'reservation'); $rv = new UrlConcrete($urlGenerator->generate('reservation_mentions-legal',[], UrlGeneratorInterface::ABSOLUTE_URL),$t,UrlConcrete::CHANGEFREQ_WEEKLY,0.5); - $urlContainer->addUrl($rv,'revervation'); + $urlContainer->addUrl($rv,'reservation'); $rv = new UrlConcrete($urlGenerator->generate('reservation_rgpd',[], UrlGeneratorInterface::ABSOLUTE_URL),$t,UrlConcrete::CHANGEFREQ_WEEKLY,0.5); - $urlContainer->addUrl($rv,'revervation'); + $urlContainer->addUrl($rv,'reservation'); $rv = new UrlConcrete($urlGenerator->generate('reservation_hosting',[], UrlGeneratorInterface::ABSOLUTE_URL),$t,UrlConcrete::CHANGEFREQ_WEEKLY,0.5); - $urlContainer->addUrl($rv,'revervation'); + $urlContainer->addUrl($rv,'reservation'); $rv = new UrlConcrete($urlGenerator->generate('reservation_cgv',[], UrlGeneratorInterface::ABSOLUTE_URL),$t,UrlConcrete::CHANGEFREQ_WEEKLY,0.5); - $urlContainer->addUrl($rv,'revervation'); + $urlContainer->addUrl($rv,'reservation'); + + foreach ($this->productRepository->findAll() as $product) { + $rv = new UrlConcrete($urlGenerator->generate('reservation_product_show',['id'=>$product->slug()], UrlGeneratorInterface::ABSOLUTE_URL),$t,UrlConcrete::CHANGEFREQ_WEEKLY,0.5); + $urlContainer->addUrl($rv,'reservation_product'); + } } } diff --git a/templates/revervation/catalogue.twig b/templates/revervation/catalogue.twig index ec4697d..19aec7c 100644 --- a/templates/revervation/catalogue.twig +++ b/templates/revervation/catalogue.twig @@ -13,7 +13,7 @@
{# --- HEADER --- #} -
+
- {# --- BARRE DE RECHERCHE / FILTRES (STICKY) --- #} + {# --- FILTRES (STICKY) --- #}
-
+
- - {# Bouton Tout voir #} - {# Liste des catégories en dur #} {% set categories_list = [ {'id': '3-15 ans', 'label': '3-15 ANS', 'hover': 'hover:border-blue-600 hover:text-blue-600'}, {'id': '2-7 ans', 'label': '2-7 ANS', 'hover': 'hover:border-amber-500 hover:text-amber-500'}, @@ -55,52 +52,57 @@
- {# --- GRILLE DE PRODUITS (5 COLONNES) --- #} -
-
+ {# --- GRILLE DE PRODUITS (4 COLONNES) --- #} +
+ {# Mobile: 2 col | Tablette: 3 col | Desktop: 4 col #} +
{% for product in products %}
- + - {# Image avec arrondi et badge prix #} -
+ {# IMAGE #} + {% endblock %} diff --git a/templates/revervation/home.twig b/templates/revervation/home.twig index 442864b..a00345d 100644 --- a/templates/revervation/home.twig +++ b/templates/revervation/home.twig @@ -104,7 +104,7 @@
Ludik Event + class="w-50 h-50 object-contain opacity-20 group-hover:opacity-100 group-hover:scale-110 transition-all duration-500 grayscale group-hover:grayscale-0">
{% endif %} diff --git a/templates/revervation/produit.twig b/templates/revervation/produit.twig new file mode 100644 index 0000000..cb9b2ea --- /dev/null +++ b/templates/revervation/produit.twig @@ -0,0 +1,171 @@ +{% extends 'revervation/base.twig' %} + +{% block title %}{{ product.name }} - Location Ludikevent{% endblock %} +{% block breadcrumb_json %} + ,{ + "@type": "ListItem", + "position": 1, + "name": "Catalogue", + "item": "{{ path('reservation_catalogue') }}" + },{ + "@type": "ListItem", + "position": 2 + "name": "Catalogue", + "item": "{{ path('reservation_product_show',{id:product.id}) }}" + } +{% endblock %} + +{% block body %} +
+ {# --- HEADER --- #} +
+ +
+ {# --- FIL D'ARIANE / RETOUR --- #} + + +
+
+ + {# --- COLONNE GAUCHE : IMAGE --- #} +
+
+ {% if product.imageName %} + {{ product.name }} + {% else %} + {# FALLBACK : Image par défaut si vide #} +
+ Ludik Event +
+ {% endif %} + + {# Badge catégorie flottant #} +
+ + {{ product.category }} + +
+
+
+ + {# --- COLONNE DROITE : INFOS --- #} +
+
+ + Référence : {{ product.ref }} + +

+ {{ product.name }} +

+ +
+ {{ product.priceDay }}€ + TTC / Journée +
+
+ + {# --- DESCRIPTION / CARACTERISTIQUES --- #} +
+

+ Louez ce produit pour vos événements. Qualité professionnelle, sécurité certifiée et plaisir garanti. +

+ +
+
+ Âge conseillé + {{ product.category }} +
+
+ Disponibilité + En stock ⚡ +
+
+
+ + {# --- ACTION --- #} +
+ + Réserver maintenant + +

+ Réponse rapide sous 24h • Ludikevent +

+
+
+ +
+
+ + {# --- SECTION SUGGESTIONS DISCRETE --- #} + {# --- SECTION SUGGESTIONS --- #} +
+
+
+ Vous pourriez aussi aimer +

D'autres idées ?

+
+ +
+ + {# Grille de suggestions (on réutilise le style épuré) #} +
+ {% for other in otherProducts %} + + {% else %} + {# Cas où il n'y a pas d'autres produits à suggérer #} +

Plus de surprises arrivent bientôt...

+ {% endfor %} +
+
+ +
+{% endblock %}