---
url: /internals/images-and-media.md
---
# Images and media internals

Uploads live in the media library (`Media` model, folders, tags, usage index); images are rendered into variants on request or by a queue worker. The user side is on [Media library](/panel/media-library) and [Image Rendering](/frontend/image-rendering).

## Storage

Media files are stored on the `uploads` disk the core provider defines (`storage/app/evolve/uploads`). Since 2.12 `storage/app` of every release is a symlink to `shared/storage/app` next to `releases/` (`Deployment\SharedStorage`), so a deploy keeps uploads and `evolve:env:setup` and the Envoy media tasks move the one directory. Rendered variants are written under `public/img` and served statically by the web server on the next hit; `/index.php/img/...` answers 301 to `/img/...` (2.12).

## Visibility and the private variant cache&#x20;

`Contracts\MediaVisibility` (default `Services\Media\OpenMediaVisibility`, bound with `singletonIf`) is asked by `MediaController` and `ImageController` before anything is delivered; a `false` answers 404 like a missing hash. `isPubliclyCacheable()` decides the cache disk of a variant: `ImageVariant::cacheDisk()` returns `public` or `image-cache-private` (`storage/app/evolve/image-cache`), `ImageRenderer::setCacheDisk()` writes there, and `Services\Media\MediaCacheControl` turns the answer into `public, max-age=N` or `private, max-age=600`. `ImageRenderer::clearCache()` and `evolve:clear-images` clear both disks. `Events\MediaFileStored` fires after the file is on the uploads disk (upload, picker, replace). The module side of this is on [Media extension points](/custom-modules/media-extension-points); without a module every variant renders into `public/img` with the headers it always had (`MediaDeliveryWithoutModuleTest`).

## Upload rules

`evolve.media-manager.max-file-size` and `allowed-extensions` in `core/config/evolve.php`, enforced in `Http\Livewire\MediaManager`. The allowlist excludes active content (svg, html, xml, js, php) because files are served from the media URL and would execute in the browser (2.4).

## Metadata and recognition

`Jobs\UpdateImageMetadata` reads dimensions and EXIF after upload and, with `image.enable-image-recognition` (`IMAGE_RECOGNITION`), asks `Services\CloudVision` (Google Cloud Vision, credentials from `GOOGLE_APPLICATION_CREDENTIALS`) for labels, logos, text and objects, which become suggested tags. `evolve:update-image-metadata` runs the job for files missing metadata. Extensions are stored lowercased (2.8).

## Rendering pipeline

```mermaid
flowchart TD
    UP["Upload in MediaManager<br/>uploads disk, shared/storage/app"] --> META["UpdateImageMetadata job<br/>dimensions, EXIF, Cloud Vision tags"]
    IMG["x-image component in a template<br/>size, crop, focal point"] --> HIT{"variant exists<br/>under public/img?"}
    HIT -->|yes| STATIC["served statically by the web server"]
    HIT -->|no, QUEUE_CONNECTION=sync| INLINE["ImageRenderer renders inline<br/>gd or imagick, image.compression"]
    HIT -->|no, queue worker| ORIG["original answered,<br/>GenerateImageVariant queued"]
    WARM["evolve:images:warm --missing<br/>after a deploy"] --> CONV["GenerateImageConversions<br/>every size in image.conversions"]
    INLINE --> OUT["variant written under public/img"]
    ORIG --> OUT
    CONV --> OUT
```

| Class | Role |
|---|---|
| `Services\ImageRenderer` | takes a source path and `Media`, `setSize()`, `setFilters()`, `setFormat()`, `useCache()`, `setCachePath()`, `render()` and `toResponse()`; uses the `image.driver` (`gd` or `imagick`) and `image.compression` per format |
| `Services\ImageConversions` | the list in `image.conversions`: every size the theme's `<x-image>` requests, declared once so the queue knows what to pre-render |
| `Services\ImageVariant` | one rendered size, format and filter set of one image |
| `ImageFilters\*` | the filters a template may request |
| `Jobs\GenerateImageConversions`, `Jobs\GenerateImageVariant` | queued rendering; a cold variant is answered with the original and rendered by the worker (2.12) |

The theme's `<x-image>` component asks for a variant by size and crop (focal point from the media record). With `QUEUE_CONNECTION=sync` the first request renders inline; with a worker, `evolve:images:warm --missing` after a deploy pre-renders what is missing and the queue health check reports a stalled worker. `evolve:clear-images` deletes every variant; `image.allow-ids` (`IMAGE_ALLOW_IDS`) lets a template request an image by ID. WebP derivatives of ICC-tagged images honour the configured quality since 2.12.1.

## File URLs

`core/routes/routes.php` serves files under `media/{media}/download`, `media/{media}/preview/{slug?}` and `media/{media}/{slug?}` (`media.get`), registered in that order. The slug of `media.get` is constrained to anything except `download` and `preview`, so neither the order nor a route cache lets it shadow the other two. `MediaController::get()` ignores the slug and resolves by hash. `Services\Media\FilePathOutputter` (what `Media::path()` uses for every type except images) emits `/media/{hash}/{slug}.{ext}` with `Str::slug()` of the name, falling back to the hash when the slug is empty, and ignores image options. `Media::filePath()` calls it directly, which is how the downloads block links images to their original file. The URL patterns of `MediaUsageIndexer` and `MediaReferenceAnnotator` only read the hash, so they match the old and the new shape (2.19).

## Usage index

`Services\Media\MediaUsageIndexer` writes `MediaUsage` rows for every blueprint field and inline link in block content that references a media ID; `evolve:index-media-usages` rebuilds it. The panel shows the count on the card and the list in the file drawer, and `evolve:prune-media-attachments` removes attachments whose owner model or class no longer exists.

## Favicons

`FaviconController` generates the favicon set through RealFaviconGenerator (`services.realfavicongenerator.api-key`, a shared public key) into `public/favicons`, hardened against SSRF and arbitrary writes (2.4). `head.blade.php` includes `public/favicons/markup.html` when it exists.
