|
| 1 | +# plain-assets: Automatic WebP Conversion |
| 2 | + |
| 3 | +**Status:** Core feature, optional based on `cwebp` availability |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +Automatically convert PNG/JPEG images to WebP during `plain build` to reduce file sizes by ~25-35%. |
| 8 | + |
| 9 | +**Enabled when:** User has `cwebp` binary installed (from libwebp package) |
| 10 | + |
| 11 | +**Disabled when:** `cwebp` not found (silent skip, no errors) |
| 12 | + |
| 13 | +## Installation (Optional) |
| 14 | + |
| 15 | +```bash |
| 16 | +# macOS |
| 17 | +brew install webp |
| 18 | + |
| 19 | +# Ubuntu/Debian |
| 20 | +sudo apt install webp |
| 21 | + |
| 22 | +# Alpine (Docker) |
| 23 | +apk add libwebp-tools |
| 24 | +``` |
| 25 | + |
| 26 | +## Key Design Decisions |
| 27 | + |
| 28 | +- **Optional core feature**: Built into plain-assets, no separate package |
| 29 | + - If `cwebp` available → WebP variants generated |
| 30 | + - If not → Original behavior (no WebP) |
| 31 | + - Zero Python dependencies |
| 32 | + |
| 33 | +- **Dual format strategy**: Generate both original and WebP versions |
| 34 | + - `hero.jpg` → `hero.abc123.jpg` + `hero.abc123.webp` |
| 35 | + - Both fingerprinted and added to manifest |
| 36 | + - No template changes required (backward compatible) |
| 37 | + |
| 38 | +- **Header-based serving**: Use `Accept: image/webp` header |
| 39 | + - Browser requests `hero.jpg` with `Accept: image/webp` |
| 40 | + - Server transparently serves `hero.webp` if available |
| 41 | + - Cleaner than `<picture>` elements |
| 42 | + - Works with existing `{% asset %}` tags |
| 43 | + |
| 44 | +- **Browser support**: 96%+ in 2025 |
| 45 | + - Supported: Chrome 32+, Firefox 65+, Safari 14+, Edge 18+ |
| 46 | + - Fallback via dual format for old browsers |
| 47 | + |
| 48 | +## Configuration (Optional) |
| 49 | + |
| 50 | +Probably don't need settings - sensible defaults work for everyone. But if needed: |
| 51 | + |
| 52 | +```python |
| 53 | +# settings.py |
| 54 | +ASSETS_WEBP_QUALITY = 85 # Default: 85 (0-100) |
| 55 | +ASSETS_WEBP_ONLY_IF_SMALLER = True # Default: True (skip if WebP is larger) |
| 56 | +``` |
| 57 | + |
| 58 | +## Build Integration |
| 59 | + |
| 60 | +```python |
| 61 | +# In plain/plain/assets/compile.py |
| 62 | + |
| 63 | +def has_webp_support(): |
| 64 | + """Check if cwebp binary is available.""" |
| 65 | + return shutil.which("cwebp") is not None |
| 66 | + |
| 67 | +def compile_assets(): |
| 68 | + webp_enabled = has_webp_support() |
| 69 | + |
| 70 | + for asset in assets: |
| 71 | + # Copy original |
| 72 | + copy_file(asset) |
| 73 | + fingerprint_file(asset) |
| 74 | + |
| 75 | + # Generate WebP if enabled and applicable |
| 76 | + if webp_enabled and is_image(asset): |
| 77 | + webp_path = convert_to_webp(asset, quality=85) |
| 78 | + if webp_path: |
| 79 | + fingerprint_file(webp_path) |
| 80 | +``` |
| 81 | + |
| 82 | +## Serving Logic |
| 83 | + |
| 84 | +```python |
| 85 | +# In plain/plain/assets/views.py |
| 86 | + |
| 87 | +def get(self, request, path): |
| 88 | + # Try to serve WebP variant if browser supports it |
| 89 | + if path.endswith(('.jpg', '.jpeg', '.png')): |
| 90 | + if 'image/webp' in request.headers.get('Accept', ''): |
| 91 | + webp_path = get_webp_variant(path) |
| 92 | + if webp_path and webp_path in manifest: |
| 93 | + path = webp_path |
| 94 | + |
| 95 | + return serve_file(path) |
| 96 | +``` |
| 97 | + |
| 98 | +## User Experience |
| 99 | + |
| 100 | +**Developer installs cwebp:** |
| 101 | + |
| 102 | +```bash |
| 103 | +brew install webp |
| 104 | +plain build |
| 105 | +# → Images automatically get WebP variants |
| 106 | +# → Browsers automatically get smaller files |
| 107 | +``` |
| 108 | + |
| 109 | +**Developer doesn't install cwebp:** |
| 110 | + |
| 111 | +```bash |
| 112 | +plain build |
| 113 | +# → Works exactly as before |
| 114 | +# → No WebP variants generated |
| 115 | +# → No errors or warnings |
| 116 | +``` |
| 117 | + |
| 118 | +**Optional: Check if WebP is working** |
| 119 | + |
| 120 | +```bash |
| 121 | +plain build --verbose |
| 122 | +# → "WebP: Converted 12 images (saved 2.3MB)" |
| 123 | +# OR |
| 124 | +# → "WebP: cwebp not found (install with 'brew install webp')" |
| 125 | +``` |
| 126 | + |
| 127 | +## Questions to Resolve |
| 128 | + |
| 129 | +1. **Verbosity**: Should we show a one-time hint if `cwebp` not found? |
| 130 | +2. **AVIF support**: Also support AVIF via `avifenc` (same pattern)? |
| 131 | +3. **Manifest format**: How to represent variants in fingerprint manifest? |
0 commit comments