feat: single entry point routing — convert to front controller pattern

- Create app/public/index.php as front controller (bootstrap + Dispatcher)
- Rewrite app/router.php for PHP dev server → all non-asset requests to index.php
- Update Dispatcher to render full page layouts (head+header+view+footer)
- Move public view templates into templates/public/ (home, search, tfe, about, repertoire)
- Delete dead direct-access public/*.php files (apropos, search, tfe, licence, repertoire)
- Add clean URL routes to Dispatcher (/search, /tfe, /repertoire, /apropos, /licence, /media)
- Remove .php extensions from all internal links (header, views, templates, URLs)
- Update OG tags in controllers to use clean URLs
- Update nginx posterg.conf → front-controller try_files pattern, block direct .php access
- Update header.php and search-bar.php form actions to clean URLs
- Switch AboutController nav key from 'nav' to 'currentNav' for consistency
This commit is contained in:
Pontoporeia
2026-04-20 12:41:55 +02:00
parent 75f808bee4
commit de2e7a61ee
22 changed files with 515 additions and 695 deletions

View File

@@ -72,7 +72,7 @@
<?php if (php_sapi_name() === 'cli-server'): ?>
<script>
(function poll(){
fetch('/live-reload.php').then(r=>r.json()).then(d=>{
fetch('/live-reload').then(r=>r.json()).then(d=>{
if(d.changed) location.reload(); else setTimeout(poll,1000);
}).catch(()=>setTimeout(poll,2000));
})();

View File

@@ -35,21 +35,21 @@ $_thesisId = $_GET['id'] ?? null;
<nav aria-label="Navigation principale">
<div class="nav-left">
<a href="/index.php" class="nav-logo">Xamxam</a>
<a href="/" class="nav-logo">Xamxam</a>
<ul class="nav-left-links">
<li>
<a href="/repertoire.php"
<a href="/repertoire"
<?= ($_navCurrent === 'repertoire') ? 'aria-current="page"' : '' ?>>Répertoire</a>
</li>
</ul>
</div>
<ul class="nav-right-links">
<li>
<a href="/licence.php"
<a href="/licence"
<?= ($_navCurrent === 'licence') ? 'aria-current="page"' : '' ?>>Licences</a>
</li>
<li>
<a href="/apropos.php"
<a href="/apropos"
<?= ($_navCurrent === 'apropos') ? 'aria-current="page"' : '' ?>>À Propos</a>
</li>
</ul>
@@ -65,7 +65,7 @@ $_thesisId = $_GET['id'] ?? null;
$searchBarValue = $searchBarValue ?? $_GET['query'] ?? '';
?>
<div class="header-search-wrap">
<form method="GET" action="/search.php"
<form method="GET" action="/search"
role="search" aria-label="Recherche">
<label for="site-search-input" class="sr-only">Recherche</label>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"

View File

@@ -46,7 +46,7 @@ function repToggleUrl(array $sets, string $dim, string $value): string {
foreach ($sets['fi'] as $v) $params[] = 'fi[]=' . urlencode($v);
foreach ($sets['kw'] as $v) $params[] = 'kw[]=' . urlencode($v);
$qs = implode('&', $params);
return '/repertoire.php' . ($qs ? '?' . $qs : '');
return '/repertoire' . ($qs ? '?' . $qs : '');
}
$anyActive = !empty($activeSets['years']) || !empty($activeSets['ap'])
@@ -163,7 +163,7 @@ $hx = 'hx-target="#repertoire-index" hx-swap="outerHTML" hx-push-url="true" hx-i
<?php else: ?>
<?php foreach ($studentMap as $name => $id): ?>
<li>
<a href="tfe.php?id=<?= (int)$id ?>" class="rep-entry rep-entry--link">
<a href="/tfe?id=<?= (int)$id ?>" class="rep-entry rep-entry--link">
<?= htmlspecialchars($name) ?>
</a>
</li>

View File

@@ -0,0 +1,98 @@
<?php
/**
* Render a comma-separated list of entries with links.
* Entries joined with comma, last two joined with " & ".
*/
function renderEntries(array $entries): string {
if (empty($entries)) return '';
$parts = [];
foreach ($entries as $e) {
$text = htmlspecialchars($e['text'] ?? '');
$url = $e['url'] ?? '';
if (!empty($url)) {
$parts[] = '<span class="apropos-entry"><a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener">' . $text . '</a></span>';
} else {
$parts[] = '<span class="apropos-entry">' . $text . '</span>';
}
}
$count = count($parts);
if ($count === 1) return $parts[0];
$prefix = implode(', ', array_slice($parts, 0, $count - 2));
$suffix = implode(' & ', array_slice($parts, -2));
return $prefix !== '' ? $prefix . ', ' . $suffix : $suffix;
}
?>
<main class="apropos-main" id="main-content">
<div class="apropos-layout">
<!-- LEFT: sticky table of contents -->
<nav class="apropos-toc" aria-label="Sections de la page">
<p class="apropos-toc-label">Parties</p>
<ul>
<li><a href="#apropos-intro">À propos</a></li>
<?php if (!empty($contacts)): ?>
<li><a href="#apropos-contacts">Contacts</a></li>
<?php endif; ?>
<?php if (!empty($credits)): ?>
<li><a href="#apropos-credits">Crédits</a></li>
<?php endif; ?>
</ul>
<div class="apropos-toc-erg">
<a href="https://erg.be" target="_blank" rel="noopener">
Site de l'erg ↗
</a>
</div>
</nav>
<!-- MIDDLE: main prose + sections -->
<div class="apropos-content">
<!-- Intro text from DB -->
<section class="apropos-section" id="apropos-intro">
<div class="prose">
<?= $aboutHtml ?>
</div>
</section>
<?php if (!empty($contacts)): ?>
<!-- Contacts section -->
<section class="apropos-section" id="apropos-contacts">
<h2 class="apropos-section-title">Contacts</h2>
<div class="apropos-contacts-grid">
<?php foreach ($contacts as $group): ?>
<address class="apropos-contact-card">
<?= renderEntries($group['entries'] ?? []) ?>
<?php if (!empty($group['role'])): ?>
<span><?= htmlspecialchars($group['role']) ?></span>
<?php endif; ?>
<?php
$emails = array_filter(array_column($group['entries'] ?? [], 'email'), fn($e) => !empty($e));
foreach ($emails as $email):
?>
<a href="mailto:<?= htmlspecialchars($email) ?>"><?= htmlspecialchars($email) ?></a>
<?php endforeach; ?>
</address>
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
<?php if (!empty($credits)): ?>
<!-- Credits section -->
<section class="apropos-section" id="apropos-credits">
<h2 class="apropos-section-title">Crédits</h2>
<dl class="apropos-credits-list">
<?php foreach ($credits as $group): ?>
<div class="apropos-credit-row">
<dt><?= htmlspecialchars($group['label']) ?></dt>
<dd><?= renderEntries($group['entries'] ?? []) ?></dd>
</div>
<?php endforeach; ?>
</dl>
</section>
<?php endif; ?>
</div>
</div>
</main>

View File

@@ -1,7 +1,7 @@
<?php if ($year): ?>
<p class="filter-info" role="status">
Année : <?= htmlspecialchars($year) ?>
<a href="?<?= http_build_query(array_diff_key($vars ?? [], ['page' => 1, 'year' => 1])) ?>" class="clear-filter"><span aria-hidden="true">✕</span> Réinitialiser</a>
<a href="/" class="clear-filter"><span aria-hidden="true">✕</span> Réinitialiser</a>
</p>
<?php elseif ($isDefaultView): ?>
<p class="home-section-label" role="status">Publication récente</p>
@@ -24,7 +24,7 @@
?>
<?php if ($thumb): ?>
<figure>
<img src="/media.php?path=<?= urlencode($thumb) ?>"
<img src="/media?path=<?= urlencode($thumb) ?>"
alt="Couverture — <?= htmlspecialchars($item['title']) ?> par <?= htmlspecialchars($item['authors'] ?? '') ?>"
loading="lazy">
</figure>

View File

@@ -0,0 +1,6 @@
<main class="search-main" id="main-content">
<h1 class="sr-only">Répertoire</h1>
<span id="rep-indicator" class="rep-indicator htmx-indicator" aria-hidden="true"></span>
<?php include APP_ROOT . '/templates/partials/repertoire-index.php'; ?>
</main>
<script src="/assets/js/htmx.min.js"></script>

View File

@@ -0,0 +1,67 @@
<?php if ($validationError): ?>
<div class="search-error">⚠ <?= htmlspecialchars($validationError) ?></div>
<?php endif; ?>
<!-- Filter controls -->
<form class="search-controls" method="GET" action="/search">
<input type="hidden" name="query" value="<?= htmlspecialchars($_GET['query'] ?? '') ?>">
<label class="search-filter-label" for="filter-year">Année
<select class="search-filter-select" name="year" id="filter-year">
<option value="">Toutes</option>
<?php foreach ($years as $y): ?>
<option value="<?= (int)$y ?>" <?= (isset($_GET['year']) && $_GET['year'] == $y) ? 'selected' : '' ?>>
<?= (int)$y ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label class="search-filter-label" for="filter-orientation">Orientation
<select class="search-filter-select" name="orientation" id="filter-orientation">
<option value="">Toutes</option>
<?php foreach ($orientations as $o): ?>
<option value="<?= htmlspecialchars($o['name']) ?>"
<?= (isset($_GET['orientation']) && $_GET['orientation'] == $o['name']) ? 'selected' : '' ?>>
<?= htmlspecialchars($o['name']) ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label class="search-filter-label" for="filter-ap">AP
<select class="search-filter-select" name="ap_program" id="filter-ap">
<option value="">Tous</option>
<?php foreach ($apPrograms as $ap): ?>
<option value="<?= htmlspecialchars($ap['name']) ?>"
<?= (isset($_GET['ap_program']) && $_GET['ap_program'] == $ap['name']) ? 'selected' : '' ?>>
<?= htmlspecialchars($ap['name']) ?>
</option>
<?php endforeach; ?>
</select>
</label>
<button type="submit" class="search-apply-btn">Filtrer</button>
<a href="/search?query=<?= urlencode($_GET['query'] ?? '') ?>" class="search-reset-link">Réinitialiser</a>
</form>
<main class="search-main" id="main-content">
<output class="search-results-header" role="status"><?= $totalItems ?> résultat<?= $totalItems > 1 ? 's' : '' ?></output>
<?php if (!empty($results)): ?>
<ul class="results-grid">
<?php foreach ($results as $item): ?>
<li><a href="/tfe?id=<?= (int)$item['id'] ?>" class="result-card">
<span class="result-card__authors"><?= htmlspecialchars($item['authors'] ?? '') ?></span>
<span class="result-card__title"><?= htmlspecialchars($item['title']) ?></span>
<small class="result-card__meta"><?= htmlspecialchars($item['year']) ?><?php if (!empty($item['orientation'])): ?> · <?= htmlspecialchars($item['orientation']) ?><?php endif; ?></small>
</a></li>
<?php endforeach; ?>
</ul>
<?php include APP_ROOT . '/templates/partials/pagination.php'; ?>
<?php else: ?>
<p class="search-empty">Aucun résultat pour cette recherche.</p>
<?php endif; ?>
</main>

View File

@@ -0,0 +1,245 @@
<main class="tfe-main" id="main-content">
<article class="tfe-layout">
<!-- LEFT: info article header -->
<header class="tfe-left">
<!-- Author above title -->
<p class="tfe-author"><?= htmlspecialchars($data['authors'] ?? 'Auteur inconnu') ?></p>
<h1 class="tfe-title">
<?= htmlspecialchars($data['title']) ?>
<?php if (!empty($data['subtitle'])): ?>
<?= htmlspecialchars($data['subtitle']) ?>
<?php endif; ?>
</h1>
<dl>
<?php if (!empty($data['orientation'])): ?>
<div>
<dt>Orientation :</dt>
<dd><a href="/repertoire?or[]=<?= urlencode($data['orientation']) ?>"><?= htmlspecialchars($data['orientation']) ?></a></dd>
</div>
<?php endif; ?>
<?php if (!empty($data['ap_program'])): ?>
<div>
<dt>Atelier pluridisciplinaire :</dt>
<dd><a href="/repertoire?ap[]=<?= urlencode($data['ap_program']) ?>"><?= htmlspecialchars($data['ap_program']) ?></a></dd>
</div>
<?php endif; ?>
<?php if (!empty($data['year'])): ?>
<div>
<dt>Date :</dt>
<dd><a href="/repertoire?fy[]=<?= urlencode($data['year']) ?>"><?= htmlspecialchars($data['year']) ?></a></dd>
</div>
<?php endif; ?>
<?php if (!empty($data['languages'])): ?>
<div>
<dt>Langue :</dt>
<dd><?php
$langs = array_map('trim', explode(',', $data['languages']));
$langLinks = array_map(fn($l) => '<a href="/search?query=' . urlencode($l) . '">' . htmlspecialchars($l) . '</a>', $langs);
echo implode(', ', $langLinks);
?></dd>
</div>
<?php endif; ?>
<?php if (!empty($data['formats'])): ?>
<div>
<dt>Format :</dt>
<dd><?php
$fmts = array_map('trim', explode(',', $data['formats']));
$fmtLinks = array_map(fn($f) => '<a href="/search?query=' . urlencode($f) . '">' . htmlspecialchars($f) . '</a>', $fmts);
echo implode(', ', $fmtLinks);
?></dd>
</div>
<?php endif; ?>
<?php if (!empty($data['file_size_info'])): ?>
<div>
<dt>Durée :</dt>
<dd><?= htmlspecialchars($data['file_size_info']) ?></dd>
</div>
<?php endif; ?>
<?php if (!empty($data['keywords'])): ?>
<div>
<dt>Mots-clés :</dt>
<dd><?php
$kws = array_map('trim', explode(',', $data['keywords']));
$kwLinks = array_map(fn($k) => '<a href="/repertoire?kw[]=' . urlencode($k) . '">' . htmlspecialchars($k) . '</a>', $kws);
echo implode(', ', $kwLinks);
?></dd>
</div>
<?php endif; ?>
<?php if (!empty($promoteursInternes)): ?>
<div>
<dt>Promoteur·ice interne :</dt>
<dd><?php
$links = array_map(fn($n) => '<a href="/search?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $promoteursInternes);
echo implode(', ', $links);
?></dd>
</div>
<?php endif; ?>
<?php if (!empty($promoteursExternes)): ?>
<div>
<dt>Promoteur·ice externe :</dt>
<dd><?php
$links = array_map(fn($n) => '<a href="/search?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $promoteursExternes);
echo implode(', ', $links);
?></dd>
</div>
<?php endif; ?>
<?php if (!empty($juryPresidents)): ?>
<div>
<dt>Président·e du jury :</dt>
<dd><?php
$links = array_map(fn($n) => '<a href="/search?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $juryPresidents);
echo implode(', ', $links);
?></dd>
</div>
<?php endif; ?>
<?php if (!empty($juryLecteurs)): ?>
<div>
<dt>Lecteur·ices :</dt>
<dd><?php
$links = array_map(fn($n) => '<a href="/search?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $juryLecteurs);
echo implode(', ', $links);
?></dd>
</div>
<?php endif; ?>
<?php if (!empty($data['access_type'])): ?>
<div>
<dt>Accès :</dt>
<dd><?= htmlspecialchars($data['access_type']) ?></dd>
</div>
<?php endif; ?>
<?php if (!empty($data['license_type'])): ?>
<div>
<dt>Licence :</dt>
<dd><?= htmlspecialchars($data['license_type']) ?></dd>
</div>
<?php endif; ?>
<?php if (!empty($data['context_note'])): ?>
<div class="tfe-meta-note">
<dt>Note :</dt>
<dd class="tfe-note-value"><?= nl2br(htmlspecialchars($data['context_note'])) ?></dd>
</div>
<?php endif; ?>
<?php if (!empty($data['author_email']) && !empty($data['author_show_contact'])): ?>
<div>
<dt>Contact :</dt>
<dd>
<?php
$_contact = $data['author_email'];
$_isUrl = filter_var($_contact, FILTER_VALIDATE_URL) !== false;
$_isEmail = !$_isUrl && str_contains($_contact, '@');
if ($_isUrl):
?>
<a href="<?= htmlspecialchars($_contact) ?>" target="_blank" rel="noopener">
<?= htmlspecialchars(preg_replace('#^https?://#i', '', rtrim($_contact, '/'))) ?>
<span class="sr-only">(ouvre dans un nouvel onglet)</span>
</a>
<?php elseif ($_isEmail): ?>
<a href="mailto:<?= htmlspecialchars($_contact) ?>"><?= htmlspecialchars($_contact) ?></a>
<?php else: ?>
<?= htmlspecialchars($_contact) ?>
<?php endif; ?>
</dd>
</div>
<?php endif; ?>
<?php if (!empty($data['baiu_link'])): ?>
<?php
$_baiuHref = htmlspecialchars($data['baiu_link']);
$_baiuLabel = preg_replace('#^https?://#i', '', rtrim($data['baiu_link'], '/'));
?>
<div>
<dt>Lien :</dt>
<dd>
<a href="<?= $_baiuHref ?>" target="_blank" rel="noopener">
<?= htmlspecialchars($_baiuLabel) ?>
<span class="sr-only">(ouvre dans un nouvel onglet)</span>
</a>
</dd>
</div>
<?php endif; ?>
</dl>
<?php if (!empty($data['synopsis'])): ?>
<p class="tfe-synopsis-text">
<?= nl2br(htmlspecialchars($data['synopsis'])) ?>
</p>
<?php endif; ?>
</header>
<!-- RIGHT: media — supplementary aside -->
<aside class="tfe-right">
<?php
$_videoIndex = 0;
?>
<?php if ($isInterdit): ?>
<p class="tfe-restricted">
Ce TFE n'est pas disponible en ligne.
</p>
<?php elseif (!empty($data['files'])): ?>
<?php foreach ($data['files'] as $file): ?>
<?php
$ext = strtolower(pathinfo($file['file_path'], PATHINFO_EXTENSION));
if ($ext === 'vtt') continue;
?>
<figure>
<?php if ($ext === 'pdf'): ?>
<embed src="/media?path=<?= urlencode($file['file_path']) ?>"
type="application/pdf" width="100%" height="700px">
<p class="tfe-pdf-fallback">
<a href="/media?path=<?= urlencode($file['file_path']) ?>&download=1">
Télécharger le PDF
</a>
</p>
<?php elseif (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp'])): ?>
<img src="/media?path=<?= urlencode($file['file_path']) ?>"
alt="<?= htmlspecialchars(
!empty($file['description'])
? $file['description']
: ($data['title'] . ' — ' . ($data['authors'] ?? ''))
) ?>">
<?php elseif ($ext === 'mp4'): ?>
<?php
$_vttPath = $captionFiles[$_videoIndex] ?? null;
$_videoIndex++;
?>
<video width="100%" controls>
<source src="/media?path=<?= urlencode($file['file_path']) ?>" type="video/mp4">
<?php if ($_vttPath): ?>
<track kind="captions"
src="/media?path=<?= urlencode($_vttPath) ?>"
srclang="fr"
label="Sous-titres"
default>
<?php endif; ?>
</video>
<?php endif; ?>
<?php if (!empty($file['description'])): ?>
<figcaption><?= htmlspecialchars($file['description']) ?></figcaption>
<?php endif; ?>
</figure>
<?php endforeach; ?>
<?php else: ?>
<p class="tfe-no-files">Aucun fichier disponible pour ce TFE.</p>
<?php endif; ?>
</aside>
</article>
</main>

View File

@@ -3,7 +3,7 @@
// $searchValue: current search query (optional)
$_sbValue = $searchBarValue ?? $_GET['query'] ?? '';
?>
<form method="GET" action="/search.php"
<form method="GET" action="/search"
role="search" aria-label="Recherche">
<label for="site-search-input" class="sr-only">Recherche</label>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"