✨ Story 3.6: optimisation images
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
20
tests/images.test.php
Normal 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");
|
||||
@@ -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'
|
||||
Reference in New Issue
Block a user