Add SEO, sitemap, robots, search, Meilisearch and security files

- JSON-LD: Organization, WebSite with SearchAction, BreadcrumbList
- SitemapController: sitemapindex with main + paginated events (images/videos)
- RobotsController: dynamic robots.txt with sitemap URL
- SearchController: /search with Meilisearch (TODO)
- Meilisearch added to dev and prod docker-compose
- Breadcrumbs added to all controllers
- .well-known: security.txt, humans.txt, dnt-policy.txt
- PGP public key in public/key.asc
- SecurityController: /mot-de-passe + .well-known/change-password route

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-18 21:59:22 +01:00
parent 04becc238b
commit 6173171778
18 changed files with 425 additions and 2 deletions

View File

@@ -138,6 +138,17 @@ services:
- ngrok
entrypoint: sh /sync.sh
meilisearch:
image: getmeili/meilisearch:latest
container_name: e_ticket_meilisearch
environment:
MEILI_MASTER_KEY: e_ticket
MEILI_ENV: development
ports:
- "7700:7700"
volumes:
- meilisearch-data:/meili_data
redisinsight:
image: redis/redisinsight:latest
container_name: e_ticket_redisinsight
@@ -153,3 +164,4 @@ volumes:
bun-modules:
vault-data:
minio-data:
meilisearch-data:

View File

@@ -106,7 +106,18 @@ services:
timeout: 5s
retries: 5
meilisearch:
image: getmeili/meilisearch:latest
restart: unless-stopped
environment:
MEILI_MASTER_KEY: e-ticket
MEILI_ENV: production
MEILI_NO_ANALYTICS: true
volumes:
- meilisearch-data:/meili_data
volumes:
db-master-data:
db-slave-data:
redis-data:
meilisearch-data:

51
key.asc Normal file
View File

@@ -0,0 +1,51 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=5F8Q
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -0,0 +1 @@
{"tracking": "N", "qualifiers": "adc", "controller": ["E-Cosplay", "contact@e-cosplay.fr"], "same-party": ["e-cosplay.fr", "ticket.e-cosplay.fr"], "policy": "https://ticket.e-cosplay.fr/rgpd"}

View File

@@ -0,0 +1,12 @@
/* TEAM */
Name: E-Cosplay
Contact: contact@e-cosplay.fr
Site: https://www.e-cosplay.fr
Location: Beautor, France
/* SITE */
Project: E-Ticket
Language: French
Standards: HTML5, CSS3, JavaScript
Components: Symfony 8, PHP 8.4, PostgreSQL, Redis, Tailwind CSS
Software: Caddy, Docker, Ansible, Meilisearch

View File

@@ -0,0 +1,6 @@
Contact: mailto:contact@e-cosplay.fr
Expires: 2027-03-18T00:00:00.000Z
Preferred-Languages: fr, en
Canonical: https://ticket.e-cosplay.fr/.well-known/security.txt
Policy: https://ticket.e-cosplay.fr/mentions-legales
Encryption: https://ticket.e-cosplay.fr/key.asc

51
public/key.asc Normal file
View File

@@ -0,0 +1,51 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=5F8Q
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -11,6 +11,10 @@ class HomeController extends AbstractController
#[Route('/', name: 'app_home')]
public function index(): Response
{
return $this->render('home/index.html.twig');
return $this->render('home/index.html.twig', [
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
],
]);
}
}

View File

@@ -18,6 +18,12 @@ class RedirectController extends AbstractController
return $this->redirectToRoute('app_home');
}
return $this->render('pages/external_redirect.twig');
return $this->render('pages/external_redirect.twig', [
'url' => $url,
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Redirection externe', 'url' => '/external-redirect'],
],
]);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class RobotsController extends AbstractController
{
#[Route('/robots.txt', name: 'app_robots', methods: ['GET'])]
public function index(UrlGeneratorInterface $urlGenerator): Response
{
$sitemapUrl = $urlGenerator->generate('app_sitemap', [], UrlGeneratorInterface::ABSOLUTE_URL);
$content = <<<TXT
User-agent: *
Allow: /
Disallow: /external-redirect
Disallow: /my-csp-report
Disallow: /track/
Disallow: /mentions-legales
Disallow: /cgu
Disallow: /cgv
Disallow: /hebergement
Disallow: /cookies
Disallow: /rgpd
Disallow: /unsubscribe/
Sitemap: {$sitemapUrl}
TXT;
return new Response($content, 200, [
'Content-Type' => 'text/plain',
]);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class SearchController extends AbstractController
{
#[Route('/search', name: 'app_search', methods: ['GET'])]
public function index(Request $request): Response
{
$query = $request->query->get('q', '');
return $this->render('search/index.html.twig', [
'query' => $query,
'results' => [],
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Recherche', 'url' => '/search'],
],
]);
}
}

View File

@@ -22,6 +22,20 @@ class SecurityController extends AbstractController
]);
}
#[Route('/mot-de-passe', name: 'app_change_password')]
#[Route('/.well-known/change-password')]
public function changePassword(): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
return $this->render('security/change_password.html.twig', [
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Modifier mon mot de passe', 'url' => '/mot-de-passe'],
],
]);
}
#[Route('/deconnexion', name: 'app_logout')]
public function logout(): void
{

View File

@@ -0,0 +1,75 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class SitemapController extends AbstractController
{
private const MAX_URLS_PER_SITEMAP = 50000;
#[Route('/sitemap.xml', name: 'app_sitemap', methods: ['GET'])]
public function index(): Response
{
// TODO: count events to determine number of event sitemap pages
$eventPages = 1;
$sitemaps = [
['loc' => $this->generateUrl('app_sitemap_main', [], UrlGeneratorInterface::ABSOLUTE_URL)],
];
for ($i = 1; $i <= $eventPages; $i++) {
$sitemaps[] = [
'loc' => $this->generateUrl('app_sitemap_events', ['page' => $i], UrlGeneratorInterface::ABSOLUTE_URL),
];
}
return new Response(
$this->renderView('sitemap/index.xml.twig', ['sitemaps' => $sitemaps]),
200,
['Content-Type' => 'text/xml'],
);
}
#[Route('/sitemap-main.xml', name: 'app_sitemap_main', methods: ['GET'])]
public function main(): Response
{
$urls = [
[
'loc' => $this->generateUrl('app_home', [], UrlGeneratorInterface::ABSOLUTE_URL),
'changefreq' => 'daily',
'priority' => '1.0',
],
[
'loc' => $this->generateUrl('app_search', [], UrlGeneratorInterface::ABSOLUTE_URL),
'changefreq' => 'weekly',
'priority' => '0.5',
],
];
return new Response(
$this->renderView('sitemap/urlset.xml.twig', ['urls' => $urls]),
200,
['Content-Type' => 'text/xml'],
);
}
#[Route('/sitemap-events-{page}.xml', name: 'app_sitemap_events', requirements: ['page' => '\d+'], methods: ['GET'])]
public function events(int $page = 1): Response
{
$offset = ($page - 1) * self::MAX_URLS_PER_SITEMAP;
// TODO: fetch events from DB with limit/offset
// $events = $eventRepository->findBy([], ['id' => 'ASC'], self::MAX_URLS_PER_SITEMAP, $offset);
$urls = [];
return new Response(
$this->renderView('sitemap/urlset.xml.twig', ['urls' => $urls]),
200,
['Content-Type' => 'text/xml'],
);
}
}

View File

@@ -23,12 +23,20 @@ class UnsubscribeController extends AbstractController
return $this->render('unsubscribe/confirmed.html.twig', [
'email' => $email,
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Desinscription', 'url' => '/unsubscribe/' . $token],
],
]);
}
return $this->render('unsubscribe/index.html.twig', [
'email' => $email,
'token' => $token,
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Desinscription', 'url' => '/unsubscribe/' . $token],
],
]);
}
}

View File

@@ -4,6 +4,65 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %}</title>
{% block meta %}
<meta name="description" content="{% block description %}E-Ticket - Plateforme de vente de tickets evenementiels pour associations{% endblock %}">
{% endblock %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "E-Ticket",
"url": "https://ticket.e-cosplay.fr",
"logo": "https://ticket.e-cosplay.fr/logo.png",
"email": "contact@e-cosplay.fr",
"telephone": "+33679348802",
"address": {
"@type": "PostalAddress",
"streetAddress": "42 rue de Saint-Quentin",
"addressLocality": "Beautor",
"postalCode": "02800",
"addressCountry": "FR"
},
"sameAs": [
"https://www.facebook.com/assocationecosplay",
"https://www.e-cosplay.fr"
]
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "E-Ticket",
"url": "https://ticket.e-cosplay.fr",
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://ticket.e-cosplay.fr/search?q={search_term_string}"
},
"query-input": "required name=search_term_string"
}
}
</script>
{% if breadcrumbs is defined and breadcrumbs is not empty %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{% for breadcrumb in breadcrumbs %}
{
"@type": "ListItem",
"position": {{ loop.index }},
"name": "{{ breadcrumb.name }}",
"item": "{{ breadcrumb.url }}"
}{% if not loop.last %},{% endif %}
{% endfor %}
]
}
</script>
{% endif %}
{% block stylesheets %}{% endblock %}
{% block javascripts %}
{{ vite_asset('app.js') }}

View File

@@ -0,0 +1,7 @@
{% extends 'base.html.twig' %}
{% block title %}Recherche - E-Ticket{% endblock %}
{% block body %}
{% endblock %}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% for sitemap in sitemaps %}
<sitemap>
<loc>{{ sitemap.loc }}</loc>
</sitemap>
{% endfor %}
</sitemapindex>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
{% for url in urls %}
<url>
<loc>{{ url.loc }}</loc>
{% if url.changefreq is defined %}<changefreq>{{ url.changefreq }}</changefreq>{% endif %}
{% if url.priority is defined %}<priority>{{ url.priority }}</priority>{% endif %}
{% if url.lastmod is defined %}<lastmod>{{ url.lastmod }}</lastmod>{% endif %}
{% if url.images is defined %}
{% for image in url.images %}
<image:image>
<image:loc>{{ image.loc }}</image:loc>
{% if image.title is defined %}<image:title>{{ image.title }}</image:title>{% endif %}
{% if image.caption is defined %}<image:caption>{{ image.caption }}</image:caption>{% endif %}
</image:image>
{% endfor %}
{% endif %}
{% if url.videos is defined %}
{% for video in url.videos %}
<video:video>
<video:thumbnail_loc>{{ video.thumbnail }}</video:thumbnail_loc>
<video:title>{{ video.title }}</video:title>
<video:description>{{ video.description }}</video:description>
{% if video.content_loc is defined %}<video:content_loc>{{ video.content_loc }}</video:content_loc>{% endif %}
{% if video.player_loc is defined %}<video:player_loc>{{ video.player_loc }}</video:player_loc>{% endif %}
{% if video.duration is defined %}<video:duration>{{ video.duration }}</video:duration>{% endif %}
</video:video>
{% endfor %}
{% endif %}
</url>
{% endfor %}
</urlset>