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

34
TODO.md
View File

@@ -38,16 +38,34 @@
- [ ] Update Dispatcher to handle all routes directly (no require APP_ROOT/public/*.php)
### Phase 2: Single entry point
- [ ] Create app/public/index.php as front controller
- [ ] Bootstrap + Dispatcher invocation
- [ ] Remove direct-access public/*.php (index.php, search.php, tfe.php, apropos.php, licence.php)
- [ ] Rename old entry points so they can't be hit directly (e.g., prefix with underscore or delete)
- [x] Create app/public/index.php as front controller
- [x] Move bootstrap logic into entry point (bootstrap.php stays for admin)
- [x] Load and invoke Dispatcher
- [x] Move old public/*.php views into templates/public/
- [x] search.php → templates/public/search.php
- [x] tfe.php → templates/public/tfe.php
- [x] apropos.php → templates/public/about.php
- [x] repertoire.php → templates/public/repertoire.php
- [x] Delete old direct-access public/*.php files
- [x] Delete public/index.php (replaced by front controller)
- [x] Delete public/search.php
- [x] Delete public/tfe.php
- [x] Delete public/apropos.php
- [x] Delete public/licence.php
- [x] Delete public/repertoire.php
- [x] Update Dispatcher.render to use templates/public/ views
- [x] Update Dispatcher to render full pages (head + header + view + footer) instead of requiring bootstrap
- [x] Ensure admin/index.php bootstraps its own path (not affected by front controller)
### Phase 3: Server config
- [ ] Update router.php — route all PHP requests to Dispatcher
- [ ] Update nginx config — point all public routes to index.php via try_files
- [ ] Replace per-file `location ~ \.php$` with front-controller pattern
### Phase 4: Cleanup
- [ ] Delete app/public/live-reload.php (already handled by LiveReloadController)
- [ ] Test all routes (/, search.php, tfe, repertoire, apropos, licence, media, live-reload)
- [x] Clean URL updates
- [x] Remove .php from all internal links (header, views, controllers)
- [x] Add clean routes to Dispatcher (/search, /tfe, /media)
- [x] Update og:url tags in controllers to use clean URLs
- [x] Update TfeController redirect to /
- [x] Update header.php action URLs
- [x] Commit current state
- [ ] Test all routes (/, /search, /tfe, /repertoire, /apropos, /licence, /media, /live-reload)

View File

@@ -1,152 +0,0 @@
<?php
require_once __DIR__ . '/../bootstrap.php';
require_once APP_ROOT . '/src/Database.php';
require_once APP_ROOT . '/src/Parsedown.php';
$currentNav = 'apropos';
define('APROPOS_STATIC_CONTENT', "Ce site POSTERG a été créé pour répertorier et valoriser les mémoires de l'erg École de Recherches Graphique de Bruxelles.\n\nL'objectif est à la fois d'offrir une vitrine aux projets des anciennes étudiantes et de mettre en lumière la diversité des disciplines et des parcours qui façonnent l'histoire de l'école à travers les âges, depuis près de 100 ans.");
/**
* 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];
// Join all but last two with ", ", join last two with " & "
$prefix = implode(', ', array_slice($parts, 0, $count - 2));
$suffix = implode(' & ', array_slice($parts, -2));
return $prefix !== '' ? $prefix . ', ' . $suffix : $suffix;
}
try {
$db = Database::getInstance();
// Intro text from pages table
$aboutPage = $db->getPage('about');
$rawContent = $aboutPage ? $aboutPage['content'] : '';
if (empty(trim($rawContent)) || trim($rawContent) === 'Contenu à venir') {
$rawContent = APROPOS_STATIC_CONTENT;
}
// Contacts and credits from apropos_contents table
$contacts = $db->getAproposContent('contacts');
$credits = $db->getAproposContent('credits');
// Apply defaults if DB returns empty
$contacts = is_array($contacts) && !empty($contacts) ? $contacts : null;
$credits = is_array($credits) && !empty($credits) ? $credits : null;
} catch (Exception $e) {
error_log("Error loading about page: " . $e->getMessage());
$rawContent = APROPOS_STATIC_CONTENT;
$contacts = null;
$credits = null;
}
$pd = new Parsedown();
$pd->setSafeMode(true);
$aboutHtml = $pd->text($rawContent);
$pageTitle = 'À Propos Posterg';
$metaDescription = 'À propos de Posterg, le répertoire des mémoires de fin d\'études de l\'erg École de Recherches Graphiques de Bruxelles.';
$ogTags = [
'type' => 'website',
'title' => $pageTitle,
'description' => $metaDescription,
'url' => 'https://posterg.erg.be/apropos.php',
'site_name' => 'Posterg ERG',
];
$extraCss = ['/assets/css/apropos.css'];
$bodyClass = 'apropos-body';
?>
<?php include APP_ROOT . '/templates/head.php'; ?>
<?php include APP_ROOT . '/templates/header.php'; ?>
<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
// Show the email from the first entry (or any that has one on separate line)
$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>
<?php include APP_ROOT . '/templates/footer.php'; ?>

View File

@@ -1,70 +1,12 @@
<?php
// Load configuration
/**
* Front Controller
*
* Single entry point for all public-facing page requests.
* Bootstraps the application and delegates routing to the Dispatcher.
*/
require_once __DIR__ . '/../bootstrap.php';
require_once APP_ROOT . '/src/Controllers/HomeController.php';
require_once APP_ROOT . '/src/Dispatcher.php';
$controller = HomeController::create();
$vars = $controller->handle();
extract($vars);
?>
<?php include APP_ROOT . '/templates/head.php'; ?>
<?php include APP_ROOT . '/templates/header.php'; ?>
<?php if ($year): ?>
<p class="filter-info" role="status">
Année : <?= htmlspecialchars($year) ?>
<a href="index.php" 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>
<?php endif; ?>
<main class="home-main" id="main-content">
<h1 class="sr-only">Mémoires de l'ERG</h1>
<ul class="cards-container">
<?php foreach ($itemsToLoad as $item): ?>
<li class="card">
<a href="tfe.php?id=<?= (int)$item["id"] ?>">
<?php
// Resolve thumbnail: banner_path → cover file → gradient placeholder
$thumb = null;
// 1. Banner path (dedicated home thumbnail)
if (!empty($item['banner_path'])) {
$thumb = $item['banner_path'];
}
// 2. Cover image from thesis_files (batch-loaded above)
if (!$thumb && isset($coverMap[$item['id']])) {
$thumb = $coverMap[$item['id']];
}
// 3. Fall through to gradient
?>
<?php if ($thumb): ?>
<figure>
<img src="/media.php?path=<?= urlencode($thumb) ?>"
alt="Couverture — <?= htmlspecialchars($item['title']) ?> par <?= htmlspecialchars($item['authors'] ?? '') ?>"
loading="lazy">
</figure>
<?php else: ?>
<div class="card__media--gradient"
aria-hidden="true">
<span class="card__gradient-author"><?= htmlspecialchars($item['authors'] ?? '') ?></span>
<span class="card__gradient-title"><?= htmlspecialchars($item['title']) ?></span>
</div>
<?php endif; ?>
<p><?= htmlspecialchars($item["authors"] ?? '') ?><?php if (!empty($item['authors']) && !empty($item['title'])): ?> <?php endif; ?><?= htmlspecialchars($item["title"]) ?><?php if (!empty($item['year'])): ?><span class="sr-only">, <?= (int)$item['year'] ?></span><?php endif; ?></p>
</a>
</li>
<?php endforeach; ?>
<?php if (empty($itemsToLoad)): ?>
<li class="cards-empty">Aucun mémoire trouvé.</li>
<?php endif; ?>
</ul>
<?php include APP_ROOT . '/templates/partials/pagination.php'; ?>
</main>
<?php include APP_ROOT . '/templates/footer.php'; ?>
$dispatcher = new Dispatcher();
$dispatcher->dispatch();

View File

@@ -1,48 +0,0 @@
<?php
require_once __DIR__ . '/../bootstrap.php';
require_once APP_ROOT . '/src/Database.php';
require_once APP_ROOT . '/src/Parsedown.php';
$currentNav = 'licence';
try {
$db = Database::getInstance();
$dbPage = $db->getPage('licenses');
$content = $dbPage ? $dbPage['content'] : '';
$licencePageTitle = $dbPage ? $dbPage['title'] : 'Licences';
} catch (Exception $e) {
error_log("Error loading licence page: " . $e->getMessage());
$content = '';
$licencePageTitle = 'Licences';
}
$pd = new Parsedown();
$pd->setSafeMode(true);
$html = $pd->text($content);
$pageTitle = $licencePageTitle . ' Posterg';
$metaDescription = 'Informations sur les licences d\'utilisation des mémoires publiés sur Posterg, le répertoire des TFE de l\'erg.';
$ogTags = [
'type' => 'website',
'title' => $pageTitle,
'description' => $metaDescription,
'url' => 'https://posterg.erg.be/licence.php',
'site_name' => 'Posterg ERG',
];
$extraCss = ['/assets/css/apropos.css'];
$bodyClass = 'apropos-body';
?>
<?php include APP_ROOT . '/templates/head.php'; ?>
<?php include APP_ROOT . '/templates/header.php'; ?>
<main class="apropos-main" id="main-content">
<div class="prose apropos-single">
<?php if (!empty(trim($content))): ?>
<?= $html ?>
<?php else: ?>
<p>Contenu à venir.</p>
<?php endif; ?>
</div>
</main>
<?php include APP_ROOT . '/templates/footer.php'; ?>

View File

@@ -1,21 +0,0 @@
<?php
require_once __DIR__ . '/../bootstrap.php';
require_once APP_ROOT . '/src/Controllers/SearchController.php';
// Build controller (performs rate-limit check; exits with HTTP 429 if exceeded)
$ctrl = SearchController::create();
// Collect all view variables for the répertoire index page
extract($ctrl->handleRepertoire());
?>
<?php include APP_ROOT . '/templates/head.php'; ?>
<?php include APP_ROOT . '/templates/header.php'; ?>
<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>
<?php include APP_ROOT . '/templates/footer.php'; ?>

View File

@@ -1,82 +0,0 @@
<?php
require_once __DIR__ . '/../bootstrap.php';
require_once APP_ROOT . '/src/Controllers/SearchController.php';
// Build controller (performs rate-limit check; exits with HTTP 429 if exceeded)
$ctrl = SearchController::create();
// Collect all view variables for the search results page
extract($ctrl->handleSearch());
?>
<?php include APP_ROOT . '/templates/head.php'; ?>
<?php include APP_ROOT . '/templates/header.php'; ?>
<?php if ($validationError): ?>
<div class="search-error">⚠ <?= htmlspecialchars($validationError) ?></div>
<?php endif; ?>
<!-- Filter controls -->
<form class="search-controls" method="GET" action="search.php">
<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.php?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.php?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>
<?php include APP_ROOT . '/templates/footer.php'; ?>

View File

@@ -1,263 +0,0 @@
<?php
require_once __DIR__ . '/../bootstrap.php';
require_once APP_ROOT . '/src/Controllers/TfeController.php';
// Build controller (loads thesis, enforces visibility, builds OG tags; redirects on 404)
$ctrl = TfeController::create();
// Collect all view variables
extract($ctrl->handle());
?>
<?php include APP_ROOT . '/templates/head.php'; ?>
<?php include APP_ROOT . '/templates/header.php'; ?>
<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.php?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.php?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.php?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.php?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.php?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.php?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.php?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.php?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.php?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.php?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
// $isInterdit and $captionFiles are resolved by TfeController::handle()
$_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));
// VTT caption files are consumed inline by <video>; skip standalone rendering.
if ($ext === 'vtt') continue;
?>
<figure>
<?php if ($ext === 'pdf'): ?>
<embed src="/media.php?path=<?= urlencode($file['file_path']) ?>"
type="application/pdf" width="100%" height="700px">
<p class="tfe-pdf-fallback">
<a href="/media.php?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.php?path=<?= urlencode($file['file_path']) ?>"
alt="<?= htmlspecialchars(
!empty($file['description'])
? $file['description']
: ($data['title'] . ' — ' . ($data['authors'] ?? ''))
) ?>">
<?php elseif ($ext === 'mp4'): ?>
<?php
// Pair this video with the N-th VTT file (if one was uploaded).
$_vttPath = $captionFiles[$_videoIndex] ?? null;
$_videoIndex++;
?>
<video width="100%" controls>
<source src="/media.php?path=<?= urlencode($file['file_path']) ?>" type="video/mp4">
<?php if ($_vttPath): ?>
<track kind="captions"
src="/media.php?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>
<?php include APP_ROOT . '/templates/footer.php'; ?>

View File

@@ -2,25 +2,19 @@
/**
* Router script for PHP built-in development server (php -S).
*
* Routes /partage/<slug> to public/partage/index.php, since the built-in
* server has no URL rewriting like nginx's try_files.
* All non-asset requests go through the front controller (public/index.php).
* Static files (CSS, JS, images, fonts) are served directly.
*/
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// Route /partage/<slug> and /partage/<slug>/<action> to the partage entry
if (preg_match('#^/partage(/.*)?$#', $uri)) {
$_SERVER['SCRIPT_NAME'] = '/partage/index.php';
require __DIR__ . '/public/partage/index.php';
return true;
// Static assets: let the dev server handle them directly
$ext = pathinfo($uri, PATHINFO_EXTENSION);
if (in_array($ext, ['css', 'js', 'png', 'jpg', 'jpeg', 'gif', 'ico', 'svg', 'woff', 'woff2', 'ttf', 'eot', 'map'], true)) {
return false; // serve directly
}
// Route /tfe/<...> to tfe.php
if (preg_match('#^/tfe(/.*)?$#', $uri)) {
$_SERVER['SCRIPT_NAME'] = '/tfe.php';
require __DIR__ . '/public/tfe.php';
return true;
}
// Default: serve static files if they exist
return false;
// Send everything else through the front controller
$_SERVER['SCRIPT_NAME'] = '/index.php';
require __DIR__ . '/public/index.php';
return true;

View File

@@ -30,7 +30,7 @@ class AboutController {
$pd->setSafeMode(true);
return [
'nav' => 'apropos',
'currentNav' => 'apropos',
'aboutHtml' => $pd->text($rawContent),
'contacts' => $contacts,
'credits' => $credits,

View File

@@ -124,7 +124,7 @@ class SearchController
'type' => 'website',
'title' => 'Recherche Posterg',
'description' => "Résultats de recherche dans le répertoire des TFE de l'erg.",
'url' => 'https://posterg.erg.be/search.php',
'url' => 'https://posterg.erg.be/search',
'site_name' => 'Posterg ERG',
],
'currentNav' => 'repertoire',
@@ -174,7 +174,7 @@ class SearchController
'type' => 'website',
'title' => 'Répertoire Posterg',
'description' => "Parcourez le répertoire des mémoires de fin d'études (TFE) de l'erg École de Recherches Graphiques de Bruxelles.",
'url' => 'https://posterg.erg.be/repertoire.php',
'url' => 'https://posterg.erg.be/repertoire',
'site_name' => 'Posterg ERG',
],
'currentNav' => 'repertoire',

View File

@@ -168,7 +168,7 @@ class TfeController
'type' => 'article',
'title' => $title,
'description' => $metaDescription,
'url' => self::BASE_URL . '/tfe.php?id=' . $thesisId,
'url' => self::BASE_URL . '/tfe?id=' . $thesisId,
'image' => $ogImage,
'image_alt' => $imageAlt,
'site_name' => 'Posterg ERG',
@@ -241,7 +241,7 @@ class TfeController
*/
private function redirectHome(): never
{
header('Location: index.php');
header('Location: /');
exit;
}
}

View File

@@ -22,9 +22,11 @@ class Dispatcher {
'' => ['controller' => 'HomeController', 'action' => 'handle', 'view' => 'public/home'],
'/' => ['controller' => 'HomeController', 'action' => 'handle', 'view' => 'public/home'],
'/index.php' => ['controller' => 'HomeController', 'action' => 'handle', 'view' => 'public/home'],
'/search.php' => ['controller' => 'SearchController', 'action' => 'handle', 'view' => 'public/search'],
'/search' => ['controller' => 'SearchController', 'action' => 'handleSearch', 'view' => 'public/search'],
'/search.php' => ['controller' => 'SearchController', 'action' => 'handleSearch', 'view' => 'public/search'],
'/repertoire' => ['controller' => 'SearchController', 'action' => 'handleRepertoire', 'view' => 'public/repertoire'],
'/repertoire.php' => ['controller' => 'SearchController', 'action' => 'handleRepertoire', 'view' => 'public/repertoire'],
'/tfe' => ['controller' => 'TfeController', 'action' => 'handle', 'view' => 'public/tfe'],
'/tfe.php' => ['controller' => 'TfeController', 'action' => 'handle', 'view' => 'public/tfe'],
'/apropos' => ['controller' => 'AboutController', 'action' => 'handle', 'view' => 'public/about'],
'/apropos.php' => ['controller' => 'AboutController', 'action' => 'handle', 'view' => 'public/about'],
@@ -127,16 +129,17 @@ class Dispatcher {
return self::ROUTES[$path];
}
// /tfe?id= pattern (TFeController handles the id param internally)
// /tfe?id= pattern (TfeController handles the id param internally)
if (preg_match('#^/tfe$#', $path) && isset($_GET['id'])) {
return self::ROUTES['/tfe.php'];
return self::ROUTES['/tfe'];
}
return null;
}
/**
* Render a view template, passing controller data through extract().
* Render a view template wrapped in the full page layout.
* Includes head.php, header.php, the view, and footer.php.
*/
private function render(string $view, array $vars): void {
$viewPath = APP_ROOT . '/templates/' . $view . '.php';
@@ -146,6 +149,9 @@ class Dispatcher {
return;
}
extract($vars);
include APP_ROOT . '/templates/head.php';
include APP_ROOT . '/templates/header.php';
include $viewPath;
include APP_ROOT . '/templates/footer.php';
}
}

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"

View File

@@ -148,23 +148,28 @@ server {
try_files $uri $uri/ =404;
}
# Share-link (partage) — rewrite pretty URLs to index.php
# Share-link (partage) — handled by front controller
location /partage/ {
try_files $uri /partage/index.php$is_args$args;
try_files $uri /index.php$is_args$args;
}
# Search endpoint - rate limiting
location = /search.php {
limit_req zone=search burst=10 nodelay;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
# /media — served by front controller (MediaController validates + streams)
location = /media {
try_files $uri /index.php$is_args$args;
}
# PHP files handler (public pages)
location ~ \.php$ {
# Rate limiting for general PHP requests
limit_req zone=general burst=20 nodelay;
# /live-reload — served by front controller
location = /live-reload {
try_files $uri /index.php$is_args$args;
}
# Maintenance page
location = /maintenance {
try_files $uri /index.php$is_args$args;
}
# Front controller — all PHP requests routed through index.php
location = /index.php {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
@@ -176,6 +181,16 @@ server {
fastcgi_send_timeout 120;
}
# All other clean URLs — fall through to front controller
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
# Block all other direct PHP access (security)
location ~ \.php$ {
deny all;
}
# Static files caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|otf)$ {
expires 30d;
@@ -190,18 +205,13 @@ server {
add_header Content-Disposition "inline";
}
# Silence favicon.ico 404s — browsers that ignore <link rel="icon"> still request this
# Silence favicon.ico 404s
location = /favicon.ico {
return 204;
access_log off;
log_not_found off;
}
# Root location - try files or 404
location / {
try_files $uri $uri/ =404;
}
# Deny access to .htaccess files
location ~ /\.ht {
deny all;