mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
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:
195
TODO.md
195
TODO.md
@@ -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.
|
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
|
Add `Database::getThesesStats(): array` returning three counts from SQL
|
||||||
(`COUNT(*)`, `SUM(is_published)`, `SUM(1-is_published)`) so they always reflect the full DB.
|
(`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) |
|
||||||
|
|||||||
Reference in New Issue
Block a user