- 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
- Add rate limiting (5 submissions per IP per 10 min, per share link)
to prevent abuse of shared submission endpoints
- Replace all plain die() error responses with styled flash messages
and redirects (invalid slug, disabled link, expired link, wrong password,
rate limit exceeded, CSRF failure)
- Add dedicated error page renderer for disabled/expired links with
home page link
- Password gate now uses flash message via session redirect instead
of inline error variable
- add hidden student_mode field in add.php form
- pass mode=student through redirect to thanks.php in formulaire.php
- thanks.php renders clean student thank-you page (no header, centered button)
- add CSS for .thanks-student-page, .btn-new-form, .thanks-success, .thanks-error
- admin auth always required; student mode is purely UI variant on the physical machine
script-src 'self' 'unsafe-inline' added to admin Content-Security-Policy.
default-src 'self' was blocking OverType editor init block and
the dev live-reload poller. Admin section is auth-gated so
unsafe-inline is acceptable.
Silence mkdir() with @ operator; guard file_put_contents with
is_writable() check. When storage/cache/rate_limit is not writable
by php-fpm, requests are allowed through instead of throwing
warnings that flood the nginx error log.
The SVG icon in the admin nav's public-site link had two inline styles:
style="vertical-align:middle;margin-right:0.4em"
Moved to a new CSS rule:
.admin-body header nav > a svg { vertical-align: middle; margin-right: 0.4em; }
templates/header.php now contains zero style= attributes.
The only remaining inline styles project-wide are:
- dynamic gradient (hsl computed from $item['id']) in public/index.php — legitimately dynamic
- --disk-pct/--disk-color custom properties in system.php — carry PHP runtime values
admin/thanks.php:
- <div style="margin-top:1.5rem;display:flex;gap:.75rem;flex-wrap:wrap;"> → class="admin-action-bar"
- <p style="color:var(--text-secondary);"> → class="admin-muted"
admin/pages.php:
- Éditer button style="font-size:.8rem;padding:.3rem .75rem;" → class="admin-btn admin-btn--sm"
admin.css (Thesis info sections block):
- Added .admin-action-bar { margin-top:1.5rem; display:flex; gap:0.75rem; flex-wrap:wrap }
- Added .admin-muted { color: var(--text-secondary) }
The only remaining inline style in any admin PHP file is the dynamic
--disk-pct/--disk-color custom properties on the disk bar in system.php,
which carry PHP runtime values and cannot be moved to static CSS.
Scope: variables.css, search.css, todo/04-accessibility.md
- variables.css: add @media (prefers-color-scheme: dark) block scoped to
body:not(.admin-body); overrides all semantic tokens with dark equivalents:
--bg-* (#111→#333 range), --text-* (#eee/aaa/777),
--border-* (#333/#444), --accent-primary lightened to #b87fd4
(4.5:1 contrast on #111 background), --accent-secondary stays #9557b5,
--accent-foreground flipped to #111111 for dark buttons,
--accent-muted adjusted to rgba(184,127,212,0.15),
status colours muted for dark (success #4db886, error #e05555,
warning #d4a830); new --search-error-{bg,border,color} tokens added
to :root (light: #fff0f0/#c00) and overridden in dark (#2a1515/#e05555)
- search.css: replace three hardcoded hex values in .search-error rule
with var(--search-error-bg/border/color) so dark mode applies cleanly
- Admin pages are entirely unaffected: .admin-body body class is excluded
from the dark-mode selector; system.css already has its own dark palette
Consolidate action handlers into controller methods (todo/02-php-components.md).
src/ThesisCreateController.php (new, 435 lines)
Mirrors ThesisEditController for the add-thesis flow.
make() — factory; instantiates Database via new Database()
loadFormData() — returns all lookup tables needed by admin/add.php
(orientations, apPrograms, finalityTypes, languages,
formatTypes, licenseTypes)
submit(post, files) — full new-thesis creation pipeline:
1. validateAndSanitise() — trims/strips HTML, validates required fields,
year range, orientation/ap/finality IDs, language selection, max-10
keywords, URL format; throws named Exception on failure
2. findOrCreateAuthor() — reuses existing DB method
3. Transaction: createThesis + setThesisJury + setThesisLanguages +
setThesisFormats + setThesisTags; rolls back on any failure
4. File uploads outside transaction: cover image (JPG/PNG only, stored in
storage/covers/), banner via handleBannerUpload(), thesis files
(PDF/JPG/PNG/MP4/ZIP/VTT, stored in storage/theses/YEAR/IDENT/,
file_type auto-detected: caption/annex/main/other)
autofocusFieldForError() — static; maps exception messages to field names
for WCAG 3.3.1 autofocus on re-render (same contract as
ThesisEditController::autofocusFieldForError)
admin/actions/formulaire.php 346 → 45 lines
Now: bootstrap + CSRF guard + ThesisCreateController::make()->submit() +
flash/redirect on error. All validation, DB logic, and file handling removed.
admin/add.php
Lookup-table block (new Database() + 6 individual DB calls) replaced with
ThesisCreateController::make()->loadFormData() + extract().
src/Database.php — two new methods added
setPublished(int , bool ): void
UPDATE theses SET is_published = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
bulkSetPublished(int[] , bool ): void
Same but with an IN (...) clause for multiple IDs
admin/actions/publish.php 100 → 65 lines
Raw SQL (->prepare('UPDATE theses SET is_published = ?...')) replaced
with ->setPublished() / ->bulkSetPublished(). No raw PDO calls remain
in any action handler file.
Move all data-fetching and view-variable assembly out of public/index.php
into a new src/HomeController.php, following the same pattern as
SearchController, TfeController, SystemController, and ThesisEditController.
HomeController::create() builds the Database singleton dependency.
HomeController::handle() encapsulates:
- GET param parsing (page, year) with safe type coercion
- Display-mode detection: default random-latest view / year-filtered /
paginated-all theses
- All DB calls: getLatestPublishedYear, getLatestYearTheses, searchTheses,
countSearchResults, getPublishedTheses, countPublishedTheses,
getCoverPathsForTheses, getAvailableYears
- Batch cover-image loading for theses without a banner_path
- baseParams assembly for the pagination partial
- OG / meta tag array construction
- Graceful error handling (logs exception, returns safe empty state)
- Returns a flat array of view variables
public/index.php is now a 6-line dispatcher (require + create + handle +
extract) followed by a pure view template. Reduced from 100 to 71 lines.
All error-handling and data logic removed from the view layer entirely.