Story 3.6: optimisation images

This commit is contained in:
2026-02-04 17:17:51 +01:00
parent d3e699d00e
commit d8fd2d9c6c
6 changed files with 98 additions and 53 deletions

View File

@@ -2,7 +2,7 @@
## Status
Ready for Dev
In Progress
## Story
@@ -21,28 +21,28 @@ Ready for Dev
## Tasks / Subtasks
- [] **Task 1 : Organiser le dossier images** (AC: 1)
- [] Créer `assets/img/projects/`
- [] Définir la convention de nommage : `{slug}-{type}.{ext}`
- [] Exemple : `ecommerce-xyz-thumb.webp`, `ecommerce-xyz-screen-1.webp`
- [x] **Task 1 : Organiser le dossier images** (AC: 1)
- [x] Créer `assets/img/projects/`
- [x] Définir la convention de nommage : `{slug}-{type}.{ext}`
- [x] Exemple : `ecommerce-xyz-thumb.webp`, `ecommerce-xyz-screen-1.webp`
- [] **Task 2 : Implémenter le lazy loading** (AC: 2)
- [] Ajouter `loading="lazy"` sur toutes les images projets
- [] Image principale above-the-fold sans lazy loading (false)
- [x] **Task 2 : Implémenter le lazy loading** (AC: 2)
- [x] Ajouter `loading="lazy"` sur toutes les images projets
- [x] Image principale above-the-fold sans lazy loading (false)
- [] **Task 3 : Ajouter les dimensions explicites** (AC: 3)
- [] Définir les tailles standards : thumbnail (400x225), screenshot (800x450), hero (1200x675)
- [] Ajouter `width` et `height` sur toutes les `<img>`
- [x] **Task 3 : Ajouter les dimensions explicites** (AC: 3)
- [x] Définir les tailles standards : thumbnail (400x225), screenshot (800x450), hero (1200x675)
- [x] Ajouter `width` et `height` sur toutes les `<img>`
- [] **Task 4 : Implémenter WebP avec fallback** (AC: 4)
- [] Utiliser `<picture>` avec `<source type="image/webp">`
- [] Fallback vers JPG
- [x] **Task 4 : Implémenter WebP avec fallback** (AC: 4)
- [x] Utiliser `<picture>` avec `<source type="image/webp">`
- [x] Fallback vers JPG
- [] **Task 5 : Documenter les tailles recommandées** (AC: 5)
- [] Thumbnails : 400x225, qualité 80%
- [] Screenshots : 800x450, qualité 85%
- [] Hero : 1200x675, qualité 85%
- [] Documentation dans Dev Notes
- [x] **Task 5 : Documenter les tailles recommandées** (AC: 5)
- [x] Thumbnails : 400x225, qualité 80%
- [x] Screenshots : 800x450, qualité 85%
- [x] Hero : 1200x675, qualité 85%
- [x] Documentation dans Dev Notes
- [ ] **Task 6 : Tester les performances** (AC: 6)
- [ ] Audit Lighthouse sur la page projets (requiert images réelles)
@@ -168,7 +168,7 @@ done
## Dev Agent Record
### Agent Model Used
Claude Opus 4.5 (claude-opus-4-5-20251101)
GPT-5 Codex
### File List
| File | Action | Description |
@@ -176,6 +176,8 @@ Claude Opus 4.5 (claude-opus-4-5-20251101)
| `includes/functions.php` | Modified | Ajout fonction projectImage() |
| `templates/project-card.php` | Modified | Utilise projectImage() |
| `pages/project-single.php` | Modified | Utilise projectImage() pour hero et galerie |
| `tests/images.test.php` | Created | Tests helper image |
| `tests/run.ps1` | Modified | Ajout tests image |
### Completion Notes
- Fonction `projectImage()` créée avec support `<picture>` WebP + fallback JPG
@@ -183,14 +185,15 @@ Claude Opus 4.5 (claude-opus-4-5-20251101)
- Lazy loading activé par défaut, désactivé pour images above-the-fold
- Fallback onerror vers default-project.svg
- Templates mis à jour: project-card.php, project-single.php
- Task 6 (Lighthouse) non testable sans images réelles
- Tests: `powershell -ExecutionPolicy Bypass -File tests/run.ps1`
- Task 6 (Lighthouse) à faire quand images réelles dispo
### Debug Log References
Aucun problème rencontré.
Aucun problème bloquant.
## Change Log
| Date | Version | Description | Author |
|------|---------|-------------|--------|
| 2026-01-22 | 0.1 | Création initiale | Sarah (PO) |
| 2026-01-23 | 1.0 | Implémentation complète | James (Dev) |
| 2026-02-04 | 1.0 | Implémentation partielle (sans Lighthouse) | Amelia |

View File

@@ -78,4 +78,33 @@ function getAllTechnologies(): array
}
sort($technologies);
return $technologies;
}
/**
* Helper pour afficher une image projet optimisée
*/
function projectImage(string $filename, string $alt, int $width, int $height, bool $lazy = true): string
{
$webp = $filename;
$fallback = str_replace('.webp', '.jpg', $filename);
$lazyAttr = $lazy ? 'loading="lazy"' : '';
$altEsc = htmlspecialchars($alt, ENT_QUOTES, 'UTF-8');
$webpEsc = htmlspecialchars($webp, ENT_QUOTES, 'UTF-8');
$fallbackEsc = htmlspecialchars($fallback, ENT_QUOTES, 'UTF-8');
return <<<HTML
<picture>
<source srcset="/assets/img/projects/{$webpEsc}" type="image/webp">
<img
src="/assets/img/projects/{$fallbackEsc}"
alt="{$altEsc}"
width="{$width}"
height="{$height}"
{$lazyAttr}
class="w-full h-full object-cover"
onerror="this.onerror=null;this.src='/assets/img/projects/default-project.svg';"
>
</picture>
HTML;
}

View File

@@ -71,13 +71,13 @@ include_template('navbar', compact('currentPage'));
<?php if (!empty($project['thumbnail'])): ?>
<div class="mb-12 rounded-lg overflow-hidden">
<img
src="/assets/img/projects/<?= htmlspecialchars($project['thumbnail'], ENT_QUOTES, 'UTF-8') ?>"
alt="<?= htmlspecialchars($project['title'], ENT_QUOTES, 'UTF-8') ?>"
class="w-full"
loading="lazy"
onerror="this.onerror=null;this.src='/assets/img/projects/default-project.svg';"
>
<?= projectImage(
$project['thumbnail'],
$project['title'],
1200,
675,
false
) ?>
</div>
<?php endif; ?>
@@ -115,13 +115,13 @@ include_template('navbar', compact('currentPage'));
<h2 class="text-heading mb-4">Captures d'écran</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<?php foreach ($project['screenshots'] as $screenshot): ?>
<img
src="/assets/img/projects/<?= htmlspecialchars($screenshot, ENT_QUOTES, 'UTF-8') ?>"
alt="Capture d'écran - <?= htmlspecialchars($project['title'], ENT_QUOTES, 'UTF-8') ?>"
class="rounded-lg"
loading="lazy"
onerror="this.onerror=null;this.src='/assets/img/projects/default-project.svg';"
>
<?= projectImage(
$screenshot,
"Capture d'écran - {$project['title']}",
800,
450,
true
) ?>
<?php endforeach; ?>
</div>
</section>

View File

@@ -6,7 +6,7 @@
$title = $project['title'] ?? 'Sans titre';
$slug = $project['slug'] ?? '#';
$thumbnail = $project['thumbnail'] ?? 'default-project.svg';
$thumbnail = $project['thumbnail'] ?? 'default-project.webp';
$technologies = $project['technologies'] ?? [];
$maxTechs = 4;
?>
@@ -14,21 +14,13 @@ $maxTechs = 4;
<article class="card-interactive group">
<a href="/projet/<?= htmlspecialchars($slug, ENT_QUOTES, 'UTF-8') ?>" class="block">
<div class="aspect-thumbnail overflow-hidden">
<picture>
<source
srcset="/assets/img/projects/<?= htmlspecialchars($thumbnail, ENT_QUOTES, 'UTF-8') ?>"
type="image/webp"
>
<img
src="/assets/img/projects/<?= htmlspecialchars(str_replace('.webp', '.jpg', $thumbnail), ENT_QUOTES, 'UTF-8') ?>"
alt="Aperçu du projet <?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?>"
width="400"
height="225"
loading="lazy"
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
onerror="this.onerror=null;this.src='/assets/img/projects/default-project.svg';"
>
</picture>
<?= projectImage(
$thumbnail,
"Aperçu du projet {$title}",
400,
225,
true
) ?>
</div>
<div class="card-body">

20
tests/images.test.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
require_once __DIR__ . '/../includes/functions.php';
function assertTrue($cond, $msg) {
if (!$cond) {
fwrite(STDERR, $msg . PHP_EOL);
exit(1);
}
}
$htmlLazy = projectImage('ecommerce-xyz-thumb.webp', 'Test', 400, 225, true);
assertTrue(strpos($htmlLazy, 'loading="lazy"') !== false, 'lazy attr missing');
assertTrue(strpos($htmlLazy, 'type="image/webp"') !== false, 'webp source missing');
assertTrue(strpos($htmlLazy, 'width="400"') !== false, 'width missing');
assertTrue(strpos($htmlLazy, 'height="225"') !== false, 'height missing');
$htmlNoLazy = projectImage('ecommerce-xyz-thumb.webp', 'Test', 400, 225, false);
assertTrue(strpos($htmlNoLazy, 'loading="lazy"') === false, 'lazy should be absent');
fwrite(STDOUT, "OK\n");

View File

@@ -14,4 +14,5 @@ php (Join-Path $here 'router.test.php')
php (Join-Path $here 'projects-list.test.php')
php (Join-Path $here 'project-single.test.php')
php (Join-Path $here 'projects-secondary.test.php')
php (Join-Path $here 'images.test.php')
'OK'