feat: implement CSS sprite system for image optimization
Reduces HTTP requests from 44+ individual images to 3 sprite sheets (~93% reduction). Includes Go sprite generator tool, CSS classes, template integration, and E2E tests. - Add cmd/sprites/main.go for sprite generation (60x60px + 120x120px @2x) - Add _sprites.css with responsive sizing and retina support - Update templates to use sprites with logoIndex fallback - Add Makefile targets: sprites, sprites-clean - Add 9-test E2E suite for sprite functionality - Add doc/22-SPRITES.md with usage documentation
This commit is contained in:
@@ -3749,4 +3749,105 @@ if elapsed < 2000 { // Less than 2 seconds
|
||||
|
||||
---
|
||||
|
||||
### 16. CSS Sprites - Image Request Optimization
|
||||
|
||||
**Problem:** The CV page loads 44+ individual image files for company, project, and course logos. Each file requires a separate HTTP request, adding latency and overhead.
|
||||
|
||||
**Solution:** CSS sprites combine all icons into horizontal strips, dramatically reducing HTTP requests from 44+ to just 3 (6 including retina versions).
|
||||
|
||||
#### Architecture
|
||||
|
||||
```
|
||||
Source: Generated:
|
||||
static/images/ static/images/sprites/
|
||||
├── companies/ ├── sprite-companies.png (23 icons)
|
||||
│ ├── olympic-broadcasting.png ├── sprite-companies@2x.png (retina)
|
||||
│ ├── sap.png ├── sprite-projects.png (12 icons)
|
||||
│ └── ... (23 files) ├── sprite-projects@2x.png (retina)
|
||||
├── projects/ ├── sprite-courses.png (9 icons)
|
||||
│ └── ... (12 files) ├── sprite-courses@2x.png (retina)
|
||||
└── courses/ └── sprite-map.json (positions)
|
||||
└── ... (9 files)
|
||||
```
|
||||
|
||||
#### Go Sprite Generator
|
||||
|
||||
A custom Go tool (`cmd/sprites/main.go`) handles:
|
||||
- **Automatic normalization**: Any size image → 48x48px (1x) or 96x96px (2x)
|
||||
- **Aspect ratio preservation**: Icons are centered on transparent background
|
||||
- **High-quality scaling**: Uses CatmullRom interpolation for smooth results
|
||||
- **Sprite map generation**: JSON file documenting icon positions
|
||||
|
||||
#### CSS Implementation
|
||||
|
||||
```css
|
||||
.icon-sprite {
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: auto 48px;
|
||||
}
|
||||
|
||||
.icon-company {
|
||||
background-image: url('/static/images/sprites/sprite-companies.png');
|
||||
background-position-x: calc(var(--icon-index, 0) * -48px);
|
||||
}
|
||||
|
||||
/* Retina displays - automatic @2x sprite loading */
|
||||
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||||
.icon-company {
|
||||
background-image: url('/static/images/sprites/sprite-companies@2x.png');
|
||||
background-size: auto 48px; /* Display at 1x size */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Template Integration
|
||||
|
||||
```html
|
||||
{{if .LogoIndex}}
|
||||
<span class="icon-sprite icon-section icon-company"
|
||||
style="--icon-index: {{.LogoIndex}};"
|
||||
role="img"
|
||||
aria-label="{{.Company}} logo"></span>
|
||||
{{else if .CompanyLogo}}
|
||||
<img src="/static/images/companies/{{.CompanyLogo}}" alt="{{.Company}} logo">
|
||||
{{end}}
|
||||
```
|
||||
|
||||
#### Performance Impact
|
||||
|
||||
| Metric | Before | After | Improvement |
|
||||
|--------|--------|-------|-------------|
|
||||
| Image Requests | 44+ | 3-6 | ~93% reduction |
|
||||
| Total Image Size | Variable | Optimized | Single cache entry per category |
|
||||
| HTTP Overhead | 44 round-trips | 3-6 round-trips | Dramatic reduction |
|
||||
|
||||
#### Benefits
|
||||
|
||||
1. **Reduced HTTP Requests**: ~93% reduction in image requests
|
||||
2. **Simplified Caching**: Single cache invalidation per sprite category
|
||||
3. **Retina Support**: Automatic @2x sprites for high-DPI displays
|
||||
4. **Automatic Processing**: Drop any size image → automatic normalization
|
||||
5. **Zoom Compatible**: Works perfectly at 100%, 200%, and 300% zoom levels
|
||||
6. **Backward Compatible**: Falls back to individual images if logoIndex not set
|
||||
|
||||
#### Usage
|
||||
|
||||
```bash
|
||||
# Generate sprites
|
||||
make sprites
|
||||
|
||||
# Clean generated files
|
||||
make sprites-clean
|
||||
|
||||
# Visual QA
|
||||
open http://localhost:1999/static/sprite-showcase.html
|
||||
```
|
||||
|
||||
See `doc/22-SPRITES.md` for complete documentation.
|
||||
|
||||
---
|
||||
|
||||
*This document serves as both a technical reference and a demonstration of modern web development practices that prioritize web standards, performance, progressive enhancement, AI-era SEO, and superior user experience over JavaScript-heavy solutions.*
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
# CSS Sprites - Image Request Optimization
|
||||
|
||||
## Overview
|
||||
|
||||
The CV website uses CSS sprites to dramatically reduce HTTP requests for company, project, and course logos. Instead of loading 44+ individual image files, we load only 3 sprite sheets (6 files total including retina versions).
|
||||
|
||||
## Performance Impact
|
||||
|
||||
| Metric | Before | After | Improvement |
|
||||
|--------|--------|-------|-------------|
|
||||
| Image Requests | 44+ | 3-6 | ~93% reduction |
|
||||
| Cache Invalidation | Per image | Per sprite | Simplified |
|
||||
| HTTP Overhead | 44 round-trips | 3-6 round-trips | Dramatic reduction |
|
||||
|
||||
## Architecture
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
static/
|
||||
├── images/
|
||||
│ ├── companies/ # Source images (any size)
|
||||
│ ├── projects/ # Source images (any size)
|
||||
│ ├── courses/ # Source images (any size)
|
||||
│ └── sprites/ # Generated sprites
|
||||
│ ├── sprite-companies.png
|
||||
│ ├── sprite-companies@2x.png
|
||||
│ ├── sprite-projects.png
|
||||
│ ├── sprite-projects@2x.png
|
||||
│ ├── sprite-courses.png
|
||||
│ ├── sprite-courses@2x.png
|
||||
│ └── sprite-map.json
|
||||
├── sprite-showcase.html # Visual QA page
|
||||
└── css/
|
||||
└── 04-interactive/
|
||||
└── _sprites.css # Sprite CSS classes
|
||||
```
|
||||
|
||||
### Go Sprite Generator Tool
|
||||
|
||||
Located at `cmd/sprites/main.go`, this tool:
|
||||
|
||||
1. **Scans source directories** for PNG images
|
||||
2. **Normalizes images** to standard sizes (60x60px for 1x, 120x120px for 2x)
|
||||
3. **Maintains aspect ratio** and centers on transparent background
|
||||
4. **Combines into horizontal strips** for each category
|
||||
5. **Generates sprite-map.json** for documentation
|
||||
6. **Creates sprite-showcase.html** for visual QA
|
||||
|
||||
### Image Size Standards
|
||||
|
||||
- **Base size**: 60x60px (optimal for 80px display box with 10px padding)
|
||||
- **Retina size**: 120x120px (@2x for high-DPI displays)
|
||||
- **Section display**: 80x80px box (60px icon + 10px padding each side)
|
||||
|
||||
## Usage
|
||||
|
||||
### Makefile Targets
|
||||
|
||||
```bash
|
||||
# Generate sprites from source images
|
||||
make sprites
|
||||
|
||||
# Clean generated sprite files
|
||||
make sprites-clean
|
||||
```
|
||||
|
||||
### JSON Data Structure
|
||||
|
||||
Add `logoIndex` to entries in cv-en.json and cv-es.json:
|
||||
|
||||
```json
|
||||
{
|
||||
"company": "Olympic Broadcasting Services",
|
||||
"companyLogo": "olympic-broadcasting.png",
|
||||
"logoIndex": 15
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: Only add `logoIndex` when there's an actual PNG file. Entries without a logo file should not have `logoIndex`.
|
||||
|
||||
### Template Integration
|
||||
|
||||
Templates automatically use sprites when `logoIndex` is present:
|
||||
|
||||
```html
|
||||
{{if .LogoIndex}}
|
||||
<span class="icon-sprite icon-section icon-company"
|
||||
style="--icon-index: {{.LogoIndex}};"
|
||||
role="img"
|
||||
aria-label="{{.Company}} logo"></span>
|
||||
{{else if .CompanyLogo}}
|
||||
<img src="/static/images/companies/{{.CompanyLogo}}" alt="{{.Company}} logo">
|
||||
{{else}}
|
||||
<iconify-icon icon="mdi:office-building" width="80" height="80"></iconify-icon>
|
||||
{{end}}
|
||||
```
|
||||
|
||||
### CSS Classes
|
||||
|
||||
```css
|
||||
/* Base sprite class */
|
||||
.icon-sprite {
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: auto 50px;
|
||||
}
|
||||
|
||||
/* Category-specific classes */
|
||||
.icon-company { background-image: url('/static/images/sprites/sprite-companies.png'); }
|
||||
.icon-project { background-image: url('/static/images/sprites/sprite-projects.png'); }
|
||||
.icon-course { background-image: url('/static/images/sprites/sprite-courses.png'); }
|
||||
|
||||
/* Size variants */
|
||||
.icon-sprite.icon-section {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
padding: 10px;
|
||||
background-size: auto 60px;
|
||||
background-origin: content-box;
|
||||
background-clip: content-box;
|
||||
}
|
||||
.icon-sprite.icon-small { width: 32px; height: 32px; }
|
||||
.icon-sprite.icon-large { width: 64px; height: 64px; }
|
||||
```
|
||||
|
||||
## Adding New Icons
|
||||
|
||||
1. **Drop source image** into appropriate directory:
|
||||
- `static/images/companies/` for company logos
|
||||
- `static/images/projects/` for project logos
|
||||
- `static/images/courses/` for course logos
|
||||
|
||||
2. **Run sprite generation**:
|
||||
```bash
|
||||
make sprites
|
||||
```
|
||||
|
||||
3. **Update JSON files** with new `logoIndex` based on sprite-map.json
|
||||
|
||||
4. **Verify** in showcase page at `/static/sprite-showcase.html`
|
||||
|
||||
## Retina Display Support
|
||||
|
||||
The CSS automatically loads @2x sprites on retina displays:
|
||||
|
||||
```css
|
||||
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||||
.icon-company {
|
||||
background-image: url('/static/images/sprites/sprite-companies@2x.png');
|
||||
background-size: auto 60px; /* Display at 1x size */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sprite Map JSON
|
||||
|
||||
The `sprite-map.json` file documents icon positions:
|
||||
|
||||
```json
|
||||
{
|
||||
"companies": [
|
||||
{"index": 0, "name": "accenture.png"},
|
||||
{"index": 1, "name": "aena-long.png"},
|
||||
...
|
||||
],
|
||||
"projects": [...],
|
||||
"courses": [...]
|
||||
}
|
||||
```
|
||||
|
||||
This file is for documentation/debugging only - CSS calculates offset from index using `calc(var(--icon-index) * -60px)`.
|
||||
|
||||
## Verification
|
||||
|
||||
### Showcase Page
|
||||
|
||||
Visit `/static/sprite-showcase.html` to:
|
||||
- View full sprite sheets
|
||||
- See all individual icons with index labels
|
||||
- Test zoom levels (100%, 200%, 300%)
|
||||
- Verify retina rendering
|
||||
|
||||
### Network Verification
|
||||
|
||||
In browser DevTools (Network tab, filter Images):
|
||||
- **Should see**: sprite-companies.png, sprite-projects.png, sprite-courses.png
|
||||
- **Should NOT see**: individual logo files (unless fallback triggers)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Invalid PNG Warning
|
||||
|
||||
If you see "png: invalid format: not a PNG file", the source file is not a valid PNG. Check the file with `file <filename>` to verify format.
|
||||
|
||||
### Icon Not Displaying
|
||||
|
||||
1. Verify `logoIndex` is present in JSON
|
||||
2. Check sprite-map.json for correct index
|
||||
3. Verify CSS is loaded
|
||||
4. Check browser console for errors
|
||||
|
||||
### Wrong Icon Displayed
|
||||
|
||||
Verify the `logoIndex` value matches the icon's position in sprite-map.json (0-indexed).
|
||||
Reference in New Issue
Block a user