Skip to content

Assets + Image Transforms

Upload images, videos, PDFs, anything else; serve them through a signed CDN URL with on-the-fly transforms (resize, crop, format conversion).

Upload

http
POST /v1/spaces/{slug}/assets
Content-Type: multipart/form-data

Form field file. Auth: auth:sanctum + space.scope (editor+).

Response:

json
{
  "data": {
    "uuid": "abc-...",
    "filename": "hero.jpg",
    "mime": "image/jpeg",
    "bytes": 4096712,
    "width": 4800,
    "height": 3200,
    "url": "https://peacock-cms.webhoch.com/v1/cdn/demo/assets/abc-...jpg"
  }
}

Object storage backend: S3-compatible (Hetzner / R2 / MinIO). Configurable via FILESYSTEM_DISK=s3 in the API env.

http
GET /v1/spaces/{slug}/assets?type=image&search=hero&page=1

Returns paginated assets. Filterable by type (image|video|other) and free-text search against filename + tags.

Image transforms (public CDN)

http
GET /v1/cdn/{space}/assets/{uuid}.{format}?w=600&h=300&fit=crop&q=85
ParamValuesDefault
format (path)jpg, png, webp, aviforiginal
w1–4096 pxoriginal width
h1–4096 pxoriginal height
fitcontain, crop, pad, stretchcontain
q1–10085
gravity (when fit=crop)center, north, north-east, east, … autocenter
blur0–100 (optional)0

Cached at edge for 1 year (max-age=31536000, immutable) — the URL fully encodes the transform so each variant is its own cache entry.

Powered by Glide on top of the storage backend.

Signed URLs (anti-resize-attack)

By default, Peacock signs every transform URL with HMAC to prevent an attacker from filling the cache with ?w=4097&h=4096&q=100 combinations.

text
https://peacock-cms.webhoch.com/v1/cdn/demo/assets/abc.jpg
  ?w=600&h=300&fit=crop&s=<8-char-hmac>

The Astro/Next/Nuxt SDKs append s= automatically when you use their peacock.imageUrl() helper. Hand-rolled clients can compute it:

ts
import { createHmac } from 'node:crypto';
const sig = createHmac('sha256', SIGNING_KEY)
  .update('/v1/cdn/demo/assets/abc.jpg?w=600&h=300&fit=crop')
  .digest('hex').slice(0, 8);

PEACOCK_ASSET_SIGNING_KEY env var holds the secret on the API host; obtain it from the operator. Set PEACOCK_ASSET_REQUIRE_SIGNING=false for dev / open-CMS deployments.

Asset CRUD (authenticated)

http
GET    /v1/spaces/{slug}/assets/{uuid}      # metadata + signed URLs
PATCH  /v1/spaces/{slug}/assets/{uuid}      # rename, add tags
DELETE /v1/spaces/{slug}/assets/{uuid}      # soft-delete (recoverable)

Storage layout

text
s3://<bucket>/<space-slug>/<uuid-first-2-chars>/<uuid>/original.<ext>
s3://<bucket>/<space-slug>/<uuid-first-2-chars>/<uuid>/transforms/<hash>.<fmt>

Two-char fan-out prevents single-prefix hot-spots when a Space has many thousands of assets.

See also