# Image Block: Upload, Processing & Serving **Date:** 2026-04-04 **Status:** Approved ## Overview Add image upload support to the EditorJS admin editor, with on-the-fly format conversion and resizing via govips (libvips Go bindings). Uploaded images are stored as originals; processed variants are cached alongside them. Images are served publicly at `/media/.` with optional `?w=` resizing. ## Architecture A new `internal/media` package owns all image logic: - `handler.go` — `MediaHandler` struct; upload endpoint; serve endpoint; cache key logic - `process.go` — govips wrapper: resize + format conversion, lazy init guard `MediaHandler` is wired into the Gin engine in `main.go` (or equivalent startup): | Route | Auth | Purpose | |---|---|---| | `POST /admin/upload/image` | admin Basic Auth | EditorJS ImageTool upload endpoint | | `GET /media/*filepath` | public | Serve originals or processed variants | Storage root: `assets/media/` Cache key scheme: - With width: `_w.` (e.g. `abc123_800w.webp`) - Format only (no width): `.` — served directly if original matches, else cached as `_orig.` ## Data Flow ### Upload (`POST /admin/upload/image`) 1. Enforce 20 MB body limit via `http.MaxBytesReader` 2. Parse `multipart/form-data`, field name `image` 3. Validate MIME type — accept `image/jpeg`, `image/png`, `image/gif`, `image/webp`; reject others with 400 4. Generate filename: `.` (UUID v4, no user-controlled paths) 5. Write original to `assets/media/.` 6. Return EditorJS-compatible JSON: ```json { "success": 1, "file": { "url": "/media/.webp" } } ``` The returned URL points at the WebP variant so the editor preview shows a converted image immediately on next serve. ### Serve (`GET /media/.?w=800`) 1. Parse filename → base (``), requested format (`.webp` or `.jpg`/`.jpeg`) 2. Parse optional `?w` query param: - Must be a positive integer ≤ 3000; return 400 otherwise - Absent means no resize, format conversion only 3. Build cache path: `assets/media/_800w.webp` (or `.webp` if no width) 4. Cache hit → serve file directly with correct `Content-Type` 5. Cache miss: a. Locate original by globbing `assets/media/.*` b. Open with govips c. Resize to requested width (maintain aspect ratio) if `?w` given d. Export to requested format (WebP or JPEG) e. Write to cache path atomically (write temp, rename) f. Serve result 6. Original not found → 404 ## Error Handling & Edge Cases | Scenario | Behaviour | |---|---| | Upload > 20 MB | 400 before reading body | | Invalid MIME type | 400 with message | | `?w` non-integer or ≤ 0 | 400 | | `?w` > 3000 | Clamped to 3000 (no error) | | Concurrent cache miss for same variant | Write-to-temp-then-rename; last writer wins; both requests served correctly | | govips decode failure | 500 | | Original not found during serve | 404 | | Requested format matches original, no resize | Serve original directly, skip cache write | | govips not started | Handler panics at startup (fail fast) | ## govips Lifecycle - `vips.Startup(nil)` called once in `main.go` before routes are registered - `defer vips.Shutdown()` in `main.go` for clean teardown - `process.go` exposes `ConvertAndResize(src string, width int, format string) ([]byte, error)` — takes absolute path to original, returns processed bytes ## EditorJS Integration In `internal/admin/templates/form.html`, update the image tool registration: ```js image: { class: ImageTool, config: { endpoints: { byFile: '/admin/upload/image' } } } ``` `renderImage` in `internal/builder/editorjs.go` already reads `file.URL` from the block data — no changes needed. The stored URL is whatever the upload endpoint returns (defaults to `.webp`). ## Files Changed / Created | File | Change | |---|---| | `internal/media/handler.go` | New — `MediaHandler`, upload + serve handlers | | `internal/media/process.go` | New — govips resize/convert wrapper | | `cmd/nebbet/main.go` | Wire `MediaHandler` routes, add govips init/shutdown | | `internal/admin/templates/form.html` | Add `endpoints` config to ImageTool | | `go.mod` / `go.sum` | Add `github.com/davidbyttow/govips/v2` | `internal/builder/editorjs.go` — no changes required. ## Out of Scope - Image deletion (originals and cached variants remain until manually removed) - Alt text editing beyond what EditorJS ImageTool provides - Animated GIF preservation through resize - AVIF or other formats beyond WebP/JPEG