docs: semantic HTML audit — add section I–VII to TODO.md

Full analysis of every public-facing page and partial against semantic HTML.
Currently only one semantic element exists across the entire public frontend (<nav>).

Key findings mapped to concrete replacements:

nav.php: <div class=site-nav__links> → <ul>/<li>; active class → aria-current=page
         search <form> needs role=search, aria-label, hidden SVG icon

index.php: <div class=cards-container> → <ul>; card <div>s → <li>; <a> wraps directly
           card__media → <figure> for image cards; pagination divs → <nav><ul>
           disabled pagination links need aria-disabled + tabindex=-1, not just a class

search.php: filter label+div groups → <label> wrapping <select> (removes 2 classes per group)
            .search-results-view wrapper → remove (redundant inside <main>)
            results-grid <div> → <ul>; result-card__meta <span> → <small>
            repertoire columns <div> → <section>; link lists → <ul>/<li>
            active links → aria-current=page

tfe.php: heading hierarchy is backwards — author is h1, title is h2; should be reversed
         .tfe-layout → <article>; .tfe-left → <header> inside article
         .tfe-meta-list div+span soup → <dl>/<dt>/<dd> (removes ~30 wrapper divs + 5 classes)
         .tfe-right → <aside>; .tfe-media-block → <figure>; caption → <figcaption>
         .tfe-synopsis-text <div> → <p>; back link wrapper div → remove

apropos.php: .apropos-right <div> → <aside>; contact divs → <address>
             section wrapper divs → <section>; two CSS classes → strong + a[href^=mailto:]
             double-class .apropos-description.apropos-page-content → single .prose

licence.php: remove always-empty right column and two-column layout entirely

Summary table: 25+ classes that become deletable once semantic elements carry the meaning
This commit is contained in:
Pontoporeia
2026-03-26 22:53:47 +01:00
parent 7d836c165c
commit bc5c50f1fb

195
TODO.md
View File

@@ -562,3 +562,198 @@ Goal: rename the tables and column to the canonical M2M pattern (`tags`, `thesis
in PHP. When a filter is active the stats reflect only filtered rows, which is misleading.
Add `Database::getThesesStats(): array` returning three counts from SQL
(`COUNT(*)`, `SUM(is_published)`, `SUM(1-is_published)`) so they always reflect the full DB.
---
## Semantic HTML audit (2026-03-26)
Goal: replace presentational wrappers with the element that already carries the correct meaning,
removing classes where the element name itself is sufficient as a CSS selector.
The design does **not** need to change — only the vocabulary of the markup.
### I — `templates/nav.php` & `templates/search-bar.php`
- [ ] **`<div class="site-nav__links">`** wraps the centre nav links purely for flex grouping.
Replace with `<ul>` + `<li>` children (links inside a nav belong in a list — standard
pattern). The `<a>` elements stay; CSS targets `nav ul` / `nav li` / `nav a` directly,
removing `.site-nav__links`, `.site-nav__link`, `.site-nav__right` classes entirely.
`nav a[aria-current="page"]` replaces the missing `.site-nav__link--active` rule and is
self-documenting.
- [ ] **`<form class="site-search">`** is already a `<form>` — good. Add
`role="search"` and `aria-label="Recherche"`. The SVG icon should get
`aria-hidden="true"` (it's decorative). The `<input>` should have an associated
`<label>` (visually hidden via `.sr-only` is fine, or `aria-label` on the input).
### II — `public/index.php`
- [ ] **`<div class="filter-info">`** is a status/notice banner. Use `<p role="status">` or
`<output>` — both carry live-region semantics for screen readers without extra ARIA.
- [ ] **`<div class="cards-container">`** is a list of navigable items. Replace with `<ul>`
removing the wrapper div and making each card an `<li>`. `.cards-container` → target `main > ul`
or a single class on `<ul>`.
- [ ] **`<a class="card-link"><div class="card">…</div></a>`** — the outer `<a>` wrapping a `<div>`
makes the div redundant. The `<a>` is already a block element (set `display:block`). The
`.card` div can be removed; CSS targets `ul li a` directly. The `<li>` inside the `<ul>`
becomes the card container.
- [ ] **`<div class="card__media">`** — this is the image/media wrapper inside each card.
When it contains an `<img>`, use `<figure>` (a self-contained media unit). When it shows
the gradient placeholder (no real image), a plain `<div>` is fine since it's presentational.
- [ ] **`<div class="card__info"><p class="authors">…</p></div>`** — the `.card__info` wrapper
exists only to add padding. Move the padding to the `<p>` or `<li>` directly; remove the
div. The `<p>` stays. `.authors` class → either keep it or target `li > p`.
- [ ] **`<div class="pagination-wrap">`** with `<a class="pagination-btn">` and
`<span class="pagination-info">` — replace with `<nav aria-label="Pagination"><ul>…</ul></nav>`.
Each button becomes an `<li>`. The disabled state uses `aria-disabled="true"` +
`tabindex="-1"` instead of a `.disabled` class alone (which has no keyboard semantics).
`<span class="pagination-info">``<li aria-current="page">1 / 5</li>`.
### III — `public/search.php`
- [ ] **`<div class="search-filter-group">`** wraps each label+select pair. Replace with
`<label>` directly wrapping `<select>` — one element instead of two, and the label/control
association is implicit. Remove `.search-filter-group` and `.search-filter-label` (the
`<label>` element is the label). CSS targets `form label` and `form select`.
- [ ] **`<span class="search-filter-label">`** inside the filter group — deleted once the `<label>`
approach is taken (see above).
- [ ] **`<div class="search-results-view">`** is unnecessary nesting inside `<main>`. `<main>` is
already the landmark. Remove the wrapper; apply padding directly to `<main>` or its direct
children.
- [ ] **`<div class="results-grid">`** is a list of search results. Replace with `<ul class="results-grid">`.
Each `<a class="result-card">` becomes a `<li><a>` — the link text is made up of child `<span>`s
which is correct. However `.result-card__authors` and `.result-card__title` `<span>`s would be
better as `<strong>` (author, emphasis) and the title as plain text or `<span>`. The year/meta
`<span class="result-card__meta">``<small>` (ancillary metadata).
- [ ] **Répertoire index: `<div class="repertoire-index">`** — replace with `<div>` kept but its
four children are semantic candidates: each `.repertoire-col` is an independent index with a
heading. Replace `<div class="repertoire-col">` with `<section>`. The heading
(`<h2 class="repertoire-col__header">`) is already correct — `<h2>` is right. Remove
`.repertoire-col__header`; CSS targets `section > h2` scoped inside `.repertoire-index`.
- [ ] **`.year-index-item`, `.cat-index-item`, `.student-index-item`, `.keyword-index-item`** — all
four are sequences of `<a>` links with `display:block`. They are lists. Wrap each group in
`<ul>`; each link becomes `<li><a>`. The four custom classes collapse to a single `ul a`
selector per column (or no class at all, scoped via `section`). The `.active` class on links
`aria-current="page"` on the `<a>`.
- [ ] **`<p class="search-results-header">`** count line — remove `.search-results-header`; this is
a plain `<p>` styled with `.search-main p:first-child` or just keep a lightweight class. Or use
`<output>` since it is a computed result count.
### IV — `public/tfe.php`
- [ ] **`<div class="tfe-layout">`** — the two-column grid container. Replace with `<article>`
a single thesis is genuinely a self-contained piece of content. Remove `.tfe-layout`;
CSS targets `main > article` for the grid. Remove `.tfe-main`; CSS targets `main` directly.
- [ ] **`<div class="tfe-left">`** — the info/metadata column. Replace with `<header>` of the
article (it contains the author name, title, and all metadata — the article header). Or
simply remove and target `article > :first-child` if that is too strong. Actually
`<header>` is semantically correct here: it is the identifying header of the article.
- [ ] **`<div class="tfe-meta-list">`** with `<div class="tfe-meta-item"><span class="label">…</span><span class="value">…</span></div>` — this is
a description list by definition. Replace with `<dl>` / `<dt>` / `<dd>`:
- `<dl class="tfe-meta-list">` → just `<dl>` (class optional)
- `<div class="tfe-meta-item">` → remove; `<dt>`+`<dd>` are direct children of `<dl>`
(or grouped in `<div>` inside `<dl>` which is valid HTML — the spec allows it for styling)
- `<span class="label">``<dt>`
- `<span class="value">``<dd>`
- CSS: `.tfe-meta-list``dl`; `.label``dl dt`; `.value``dl dd`
This removes ~5 classes and ~30 wrapper divs from the metadata section.
- [ ] **`<div class="tfe-synopsis-text">`** — the synopsis paragraph(s). Replace with `<p>` (or
keep as `<section class="synopsis">` if multi-paragraph, but a single `<p>` suffices for
most cases). Remove the wrapper div.
- [ ] **`<div style="margin-top:1.5rem;"><a href="index.php" style="…">← Retour</a></div>`** —
remove the wrapper div; move margin to the `<a>` itself as a class. The back link is
better as `<a rel="up" href="index.php" class="back-link">← Retour</a>` (no wrapper needed).
- [ ] **`<div class="tfe-right">`** — the media column. Replace with `<aside>` — it contains
supplementary files (media, PDFs) that are related but secondary to the descriptive content.
Remove `.tfe-right`; CSS targets `article > aside`.
- [ ] **`<div class="tfe-media-block">`** — each file display unit. Replace with `<figure>`.
Image and video files become `<figure><img></figure>` and `<figure><video></video></figure>`.
The existing `<p class="tfe-file-caption">``<figcaption>`. PDF `<embed>` stays in a
`<figure>` (valid). Remove `.tfe-media-block`; CSS targets `aside figure`.
- [ ] **`<h1 class="tfe-author">` and `<h2 class="tfe-title">`** — the heading hierarchy makes
the author the primary heading and the title secondary, which is backwards semantically.
The *title* of the work is the `<h1>`; the *author* is metadata (could be a `<p>` or a `<dt>`
in the `<dl>` above). Swap: `<h1>` = title, author moves into the `<dl>`. Keeps the visual
design (CSS controls size) but fixes the document outline.
### V — `public/apropos.php`
- [ ] **`<div class="apropos-layout">`** — two-column grid. Replace with `<div>` kept but the
children are semantic: left is the main content, right is supplementary.
Left `<div class="apropos-left">` → remove (redundant wrapper around already-styled content).
Right `<div class="apropos-right">``<aside>` (contacts, credits = supplementary info).
- [ ] **`<div class="apropos-description apropos-page-content">`** inside the left col —
the Parsedown output already generates `<p>`, `<h1>``<h3>`, `<ul>` etc.
The wrapping `<div>` is only needed for the `.apropos-page-content` scoped CSS rules.
Keep it but as a single class — `<div class="prose">` — and scope all Markdown content
styles under `.prose`. This is the standard prose-container pattern.
- [ ] **`<div class="apropos-contact">`** — each contact entry. Replace with `<address>`:
the HTML spec defines `<address>` for contact information related to the document or section.
Each contact is literally an address entry. `<span class="apropos-contact-name">`
`<strong>`, `<span class="apropos-contact-role">` → plain text or `<span>`,
`<span class="apropos-contact-email">``<a href="mailto:…">`. Three classes removed.
- [ ] **Outer `<div>` wrappers around each section in `.apropos-right`** (`<div><h2>…</h2></div>`,
`<div><h2>Contacts</h2>…</div>`, `<div><h2>Crédits</h2>…</div>`) — replace each with
`<section>`. Remove the anonymous `<div>` wrappers; CSS targets `aside section > h2`.
### VI — `public/licence.php`
- [ ] **`<div class="apropos-right"></div>`** — always-empty right column. Remove entirely; the
`licence.php` page is full-width content. Update `licence.php` to not use `.apropos-layout`
at all — just `<main class="apropos-main"><div class="prose">…</div></main>`.
No class changes needed to `apropos.css`; the layout simply is not applied.
### VII — Summary of class deletions enabled by semantic changes
Once the above is applied, the following classes become deletable (element name carries the meaning):
| Class removed | Replaced by |
|---|---|
| `.site-nav__links` | `nav ul` |
| `.site-nav__link` | `nav li a` |
| `.site-nav__right` | `nav li:last-child a` (or `[aria-label]` target) |
| `.site-nav__link--active` | `[aria-current="page"]` |
| `.card-link` | `ul li a` (block `<a>` inside `<li>`) |
| `.card` | `ul li` |
| `.tfe-layout` | `main > article` |
| `.tfe-left` | `article > header` |
| `.tfe-right` | `article > aside` |
| `.tfe-meta-list` | `dl` |
| `.tfe-meta-item` | `div` inside `dl` (or removed) |
| `.label` / `.value` | `dt` / `dd` |
| `.tfe-media-block` | `figure` |
| `.tfe-file-caption` | `figcaption` |
| `.tfe-synopsis-text` | `p` (direct child of `article > header`) |
| `.search-filter-label` | `label` |
| `.search-filter-group` | `label` (wrapping approach) |
| `.repertoire-col` | `section` |
| `.repertoire-col__header` | `section > h2` |
| `.year-index-item` etc. | `ul a` (scoped per section) |
| `.result-card__meta` | `small` |
| `.results-grid` | `ul.results-grid` (only class needed) |
| `.apropos-left` | removed (direct child of grid) |
| `.apropos-right` | `aside` |
| `.apropos-contact` | `address` |
| `.apropos-contact-name` | `strong` inside `address` |
| `.apropos-contact-email` | `a[href^="mailto:"]` inside `address` |
| `.apropos-description apropos-page-content` | `.prose` (single class) |