JustFlows

Documentation is English-only for now. The rest of the site follows your language.

Media

Upload and list files, allowed types, per-site folders, local or S3-compatible storage, responsive variants, and placeholders.

6 min read

Admin → Media. Uploads go through POST /api/media (multer, memory) as administrator, editor, or author. The defaults are 100 MB per file (JF_MAX_UPLOAD_MB) and 5120 MB per site library (JF_MAX_LIBRARY_MB); oversized files return 413. GET /api/media?limit= lists rows. Files are served from GET /uploads/*.

Allowed types (admin route)

  • Images: JPEG, PNG, GIF, WebP, AVIF.
  • Documents: PDF.
  • Video: MP4, WebM.
  • Audio: MPEG, Ogg.
  • SVG is not on the server allow-list.

Where files go

Every file a site uploads lives under that site's own folder, <siteId>/<uuid>.<ext>, with responsive variants in <siteId>/<mediaId>/ and trashed files in <siteId>/.trash/. The layout is the same for both storage drivers, and each upload also writes a database row. Hooks: media.beforeUpload / media.beforeDelete (gates), media.uploaded / media.deleted (actions), filter media.metadata.

Storage: local disk or S3

STORAGE_DRIVER picks where the files go. local (default) uses the uploads/ folder (STORAGE_LOCAL_PATH). s3 uses an S3-compatible bucket such as AWS S3, Cloudflare R2, or MinIO: set STORAGE_S3_BUCKET, STORAGE_S3_ACCESS_KEY_ID, STORAGE_S3_SECRET_ACCESS_KEY, STORAGE_S3_REGION (auto for R2), and STORAGE_S3_ENDPOINT for anything that is not AWS. STORAGE_S3_PREFIX puts every key under a prefix so several installs can share one bucket. See Configuration.

Media URLs stay /uploads/<key> with either driver, so switching never rewrites content. With S3, Justflows streams /uploads from the bucket, passing range and conditional requests through. Set STORAGE_S3_PUBLIC_URL to redirect there instead. The static exporter still receives files inline, so exports never contain redirects for assets.

Public buckets and multisite

A public bucket serves any key to anyone who knows it, including another site's files. Leave STORAGE_S3_PUBLIC_URL unset if each site's files must only be reachable on its own host. See Multisite.

Before switching an existing install to S3, copy the local files into the bucket. The command needs a compiled server, skips keys already in the bucket, and never deletes local files. The storage test under Admin → System → Diagnostics writes, reads, and deletes a probe file through the configured driver.

shell
pnpm storage:sync --dry-run   # list what would be copied
pnpm storage:sync             # copy; keys already in the bucket are skipped

Limits

Media counts toward the website's media.files, media.bytes, and total storage.bytes limits. Each site also has a limit on uploads in progress, and generated image sizes count from the moment an upload is accepted.

Responsive images and formats

Raster uploads generate a configurable set of width-scaled variants plus WebP (and AVIF when enabled) alongside the untouched original — inline on upload, and backfillable from Admin → Tools → Responsive images with progress and per-file failures (POST /api/media/regenerate, GET /api/media/regenerate/status). Every public surface that renders an uploaded image — core.image, the Gallery block, blog-post-list featured thumbnails, and the Featured Image theme block — goes through one resolver (renderResponsiveImage / renderMediaImage) that emits <picture> / srcset / sizes with format fallback, intrinsic width / height to prevent layout shift, and loading="lazy" / decoding="async" defaults with an eager opt-out.

`.env` keyControls
JF_IMAGE_DERIVATIVESGenerate variants on upload.
JF_IMAGE_RESPONSIVE_MARKUPEmit <picture>/srcset on public pages (off serves originals everywhere without deleting variants).
JF_IMAGE_WIDTHSComma-separated widths to generate, default 320,640,960,1280,1920.
JF_IMAGE_MAX_WIDTHUpper bound for any variant, default 2560.
JF_IMAGE_FORMATSFormats to generate alongside the original, default webp.
JF_IMAGE_QUALITY_WEBP / _AVIF / _JPEGPer-format encode quality.
JF_IMAGE_STRIP_METADATAStrip EXIF/GPS from generated variants.
JF_IMAGE_THUMBThumbnail size, default 400x400.
JF_IMAGE_KEEP_ORIGINALFilename allowlist that skips variant generation.

All of it is configurable from the same admin panel and written to .env with no restart. Each asset carries a focal point — set by clicking the subject in the Media Library — that drives thumbnail crops and object-position. SVGs are never rasterised, and variants live next to the original under the site's folder, so S3 storage, a CDN, and static export cover them for free. Migration 0027_media_responsive supports all database dialects.

The Gallery block (justflows.gallery.grid) renders as grid, masonry, carousel, slideshow, or list, each emitting a layout-aware sizes value so the browser downloads the right variant.

Placeholder images

Justflows ships neutral placeholders for a generic image, a featured image, a thumbnail, an avatar, and the social share image. Featured Image and Post List show the placeholder when a post has no image, and an Image block without an image shows the generic one. A page with no share image falls back to the site logo, then the share placeholder; og:image is always an absolute URL.

Settings → Placeholders replaces any placeholder with your own image or turns them all off. Featured Image and Post List can each turn them off per block. Plugins can ship their own kind (Shop adds Product image) — see Plugins.

Package: `@justflows/media`

MediaService supports local and S3 adapters and Sharp derivatives: thumbnail 150², small 400, medium 800, large 1600, as WebP. Admin uploads use the storage driver described above.