Examples
Go libraries. GOX components. Code for your next project.
Image
Resize images to the requested dimensions, such as 45 × 45 px, and generate WebP files.



Actual generated sizes, shown at their native dimensions. The original is scaled for comparison. Quality: 80. Center crop preserves proportions.
On the first request, the browser may reach the URL before the file exists and receive a 404. It will not automatically retry. Reload after generation. Conversion errors are logged on the server.
1. Add the Go libraries
Run these commands from your project root.
gox get github.com/disintegration/[email protected]
gox get github.com/gen2brain/[email protected]
2. Copy the component
From the Code tab, copy the complete implementation into pages/@components/image.gox. No additional Go helper or project-specific import is required.
3. Use it in a template
module index
import (
"github.com/gox-lang/gox"
optimized "@components/image.gox"
)
func GET(ctx *gox.Context) gox.Page {
return <main>
<&optimized.Image src="public/gox.png" width={{ 45 }} height={{ 45 }} alt="GOX" />
</main>
} src is a local path relative to the project root. Use trusted project files. PNG, JPEG and static WebP inputs are supported. The public/generated/images directory must be writable.
When you replace an image
This demo uses a fixed image, so it does not invalidate the cache on each request. In your application, call Invalidate after the new source file is fully written. The next render rebuilds its variants if needed.
// After successfully replacing the source file:
optimized.Invalidate("public/gox.png") Ready variants are looked up in memory without disk access. After replacing a source or deleting generated files, call optimized.Invalidate(src), or restart the server. Different dimensions use separate cache entries. Old variants remain on disk. Animations are not preserved.
pages/@components/image.gox
Everything lives in this GOX component. A synchronized map returns ready variants without file reads or os.Stat. On a memory miss, it checks the disk cache; only a missing file starts the protected conversion goroutine. The component returns its URL immediately; errors are logged and failed jobs can be retried on the next render.
module optimizedimage
import (
"bytes"
"crypto/sha256"
"fmt"
"image"
"io"
"log"
"os"
"path/filepath"
"sync"
"github.com/disintegration/imaging"
"github.com/gen2brain/webp"
"github.com/gox-lang/gox"
)
func Image(src string, width int, height int, alt string) gox.Component {
url, err := Build(src, width, height)
if err != nil {
log.Printf("Image: %v", err)
return <span role="img" aria-label={{ alt }}>Image unavailable</span>
}
return <img
src={{ url }}
width={{ width }}
height={{ height }}
alt={{ alt }}
loading="lazy"
decoding="async"
/>
}
type ImageKey struct {
Source string
Width int
Height int
}
type ImageJob struct {
URL string
Ready bool
}
// Pending and ready variants return immediately without filesystem access.
var compiled map[ImageKey]*ImageJob = make(map[ImageKey]*ImageJob)
var cacheMu sync.RWMutex
// Preparation deduplicates cold requests; conversion runs independently.
var preparation sync.Mutex
var conversion sync.Mutex
// Call after replacing a source or removing its generated files.
// The source must be fully written before invalidating its cached variants.
func Invalidate(src string) {
preparation.Lock()
defer preparation.Unlock()
cacheMu.Lock()
defer cacheMu.Unlock()
for key := range compiled {
if key.Source == filepath.Clean(src) {
delete(compiled, key)
}
}
}
// Build expects a developer-controlled local path, not a URL or uploaded filename.
// It fills the requested dimensions by scaling proportionally and cropping centrally.
func Build(src string, width, height int) (string, error) {
if width < 1 || height < 1 || width > 2048 || height > 2048 {
return "", fmt.Errorf("image dimensions must be between 1 and 2048 pixels")
}
variant := ImageKey{Source: filepath.Clean(src), Width: width, Height: height}
cacheMu.RLock()
cached, ready := compiled[variant]
cacheMu.RUnlock()
if ready {
return cached.URL, nil
}
preparation.Lock()
defer preparation.Unlock()
// Another request may have generated this variant while we were waiting.
cacheMu.RLock()
cached, ready = compiled[variant]
cacheMu.RUnlock()
if ready {
return cached.URL, nil
}
file, err := os.Open(src)
if err != nil {
return "", fmt.Errorf("open image: %w", err)
}
defer file.Close()
const maxBytes = 20 << 20
data, err := io.ReadAll(io.LimitReader(file, maxBytes+1))
if err != nil {
return "", err
}
if len(data) > maxBytes {
return "", fmt.Errorf("source image exceeds 20 MiB")
}
// On a memory miss, content hashing identifies the persistent disk variant.
key := sha256.Sum256(append(data, []byte(fmt.Sprintf("/webp-v1/%d/%d/q80", width, height))...))
name := fmt.Sprintf("%x.webp", key)
directory := filepath.Join("public", "generated", "images")
destination := filepath.Join(directory, name)
url := "/generated/images/" + name
if info, err := os.Stat(destination); err == nil && info.Mode().IsRegular() && info.Size() > 0 {
cacheMu.Lock()
compiled[variant] = &ImageJob{URL: url, Ready: true}
cacheMu.Unlock()
return url, nil
}
job := &ImageJob{URL: url}
cacheMu.Lock()
compiled[variant] = job
cacheMu.Unlock()
// Register the pending URL before starting work. Do not wait for conversion.
Start(variant, job, func() error {
config, _, err := image.DecodeConfig(bytes.NewReader(data))
if err != nil {
return fmt.Errorf("decode image header: %w", err)
}
if config.Width <= 0 || config.Height <= 0 || int64(config.Width)*int64(config.Height) > 25_000_000 {
return fmt.Errorf("source image exceeds 25 megapixels")
}
original, err := imaging.Decode(bytes.NewReader(data), imaging.AutoOrientation(true))
if err != nil {
return fmt.Errorf("decode image: %w", err)
}
resized := imaging.Fill(original, width, height, imaging.Center, imaging.Lanczos)
if err := os.MkdirAll(directory, 0755); err != nil {
return err
}
output, err := os.CreateTemp(directory, ".image-*")
if err != nil {
return err
}
defer os.Remove(output.Name())
defer output.Close()
if err := webp.Encode(output, resized, webp.Options{Quality: 80, Method: 4}); err != nil {
return fmt.Errorf("encode WebP: %w", err)
}
if err := output.Chmod(0644); err != nil {
return err
}
if err := output.Close(); err != nil {
return err
}
// Atomic replacement prevents browsers from reading a partial file.
return os.Rename(output.Name(), destination)
})
return url, nil
}
// Finish ignores jobs invalidated or replaced while they were running.
func Finish(variant ImageKey, job *ImageJob, err error) {
if err != nil {
log.Printf("Image %s (%dx%d): %v", variant.Source, variant.Width, variant.Height, err)
}
cacheMu.Lock()
defer cacheMu.Unlock()
if compiled[variant] != job {
return
}
if err != nil {
delete(compiled, variant)
return
}
job.Ready = true
}
func Start(variant ImageKey, job *ImageJob, work func() error) {
finish := Finish
lock := &conversion
go func() (err error) {
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("image conversion panicked: %v", recovered)
}
finish(variant, job, err)
}()
// Serialize CPU-heavy work without holding the request or cache lock.
lock.Lock()
defer lock.Unlock()
return work()
}()
}